Import source: cshare-app/src/pages/FileViewPage

Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
arsam
2026-09-18 23:14:52 +03:30
parent 759b29cd9f
commit 83d33ed660
+576
View File
@@ -0,0 +1,576 @@
import { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { DownloadIcon, CopyIcon, CheckIcon, ClockIcon, ShieldIcon } from '../components/Icons';
import { sharedFile, formatFileSize, formatDate, getFileIcon, getFileIconColor } from '../data/mockData';
import { useToast } from '../components/ToastContext';
import { downloadUrl, previewUrl, resolveLink } from '../lib/api';
function useCountdown(targetDate) {
const [timeLeft, setTimeLeft] = useState('');
useEffect(() => {
const update = () => {
const diff = new Date(targetDate) - new Date();
if (diff <= 0) { setTimeLeft('Expired'); return; }
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
const hours = Math.floor((diff / (1000 * 60 * 60)) % 24);
const mins = Math.floor((diff / (1000 * 60)) % 60);
const secs = Math.floor((diff / 1000) % 60);
if (days > 0) {
setTimeLeft(`${days}d ${hours}h ${mins}m ${secs}s`);
} else {
setTimeLeft(`${hours}h ${mins}m ${secs}s`);
}
};
update();
const id = setInterval(update, 1000);
return () => clearInterval(id);
}, [targetDate]);
return timeLeft;
}
function FilePreview({ file, linkId }) {
const [text, setText] = useState('');
const [textError, setTextError] = useState('');
const [zoom, setZoom] = useState(1);
const [rotation, setRotation] = useState(0);
const [copiedText, setCopiedText] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const addToast = useToast();
const preview = file.preview;
useEffect(() => {
if (!linkId || preview?.kind !== 'text') return;
let active = true;
fetch(previewUrl(linkId))
.then((response) => {
if (!response.ok) throw new Error('Preview unavailable');
return response.text();
})
.then((body) => {
if (active) setText(body.slice(0, 120_000));
})
.catch((err) => {
if (active) setTextError(err.message);
});
return () => {
active = false;
};
}, [linkId, preview?.kind]);
const copyTextContent = () => {
navigator.clipboard?.writeText(text);
setCopiedText(true);
addToast('Preview text copied to clipboard', 'success');
setTimeout(() => setCopiedText(false), 2000);
};
if (!linkId || !preview?.available) {
return (
<div style={{
padding: '14px 16px',
background: 'var(--bg-surface)',
border: '1px solid var(--border-subtle)',
borderRadius: 'var(--radius-md)',
color: 'var(--text-muted)',
fontSize: '0.875rem',
marginBottom: '20px',
}}>
Preview is available for small images, PDFs, text, audio, and video files.
</div>
);
}
const src = previewUrl(linkId);
const shellStyle = {
marginBottom: '20px',
background: 'var(--bg-surface)',
border: '1px solid var(--border-subtle)',
borderRadius: 'var(--radius-md)',
overflow: 'hidden',
};
if (preview.kind === 'image') {
return (
<div style={{ ...shellStyle, display: 'flex', flexDirection: 'column' }}>
{/* Toolbar */}
<div style={{
display: 'flex',
justifyContent: 'flex-end',
gap: '8px',
padding: '8px 12px',
background: 'var(--bg-elevated)',
borderBottom: '1px solid var(--border-subtle)'
}}>
<button className="btn btn-ghost btn-sm" onClick={() => setZoom(z => Math.max(0.5, z - 0.25))} style={{ padding: '4px 8px', fontSize: '0.75rem' }}>Zoom -</button>
<button className="btn btn-ghost btn-sm" onClick={() => { setZoom(1); setRotation(0); }} style={{ padding: '4px 8px', fontSize: '0.75rem' }}>Reset</button>
<button className="btn btn-ghost btn-sm" onClick={() => setZoom(z => Math.min(3, z + 0.25))} style={{ padding: '4px 8px', fontSize: '0.75rem' }}>Zoom +</button>
<button className="btn btn-ghost btn-sm" onClick={() => setRotation(r => (r + 90) % 360)} style={{ padding: '4px 8px', fontSize: '0.75rem' }}>Rotate ↻</button>
</div>
{/* Canvas container */}
<div style={{
overflow: 'hidden',
maxHeight: '520px',
height: '420px',
display: 'grid',
placeItems: 'center',
background: '#09090b',
position: 'relative',
}}>
<img
src={src}
alt={file.name}
style={{
maxWidth: '100%',
maxHeight: '100%',
objectFit: 'contain',
transform: `scale(${zoom}) rotate(${rotation}deg)`,
transition: 'transform 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
}}
/>
{/* Zoom badge */}
<div style={{
position: 'absolute',
bottom: '12px',
right: '12px',
background: 'rgba(0, 0, 0, 0.75)',
border: '1px solid var(--border)',
borderRadius: 'var(--radius-sm)',
padding: '3px 8px',
fontSize: '0.6875rem',
color: 'var(--text-secondary)',
}}>
{Math.round(zoom * 100)}%
</div>
</div>
</div>
);
}
if (preview.kind === 'video') {
return (
<div style={{ ...shellStyle, position: 'relative' }}>
<video controls preload="metadata" src={src} style={{ display: 'block', width: '100%', maxHeight: '520px', background: '#000' }} />
</div>
);
}
if (preview.kind === 'audio') {
return (
<div style={{ ...shellStyle, padding: '18px', background: 'var(--bg-surface)' }}>
<audio controls preload="metadata" src={src} style={{ display: 'block', width: '100%' }} />
</div>
);
}
if (preview.kind === 'pdf') {
return (
<div style={{ ...shellStyle, height: '520px' }}>
<iframe src={src} title={`Preview of ${file.name}`} style={{ border: 0, width: '100%', height: '100%' }} />
</div>
);
}
if (preview.kind === 'text') {
const lines = text.split('\n');
const filteredLines = searchTerm
? lines.map((line, idx) => ({ line, idx })).filter(item => item.line.toLowerCase().includes(searchTerm.toLowerCase()))
: lines.map((line, idx) => ({ line, idx }));
return (
<div style={{ ...shellStyle, display: 'flex', flexDirection: 'column' }}>
{/* Toolbar */}
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
gap: '12px',
padding: '8px 12px',
background: 'var(--bg-elevated)',
borderBottom: '1px solid var(--border-subtle)',
flexWrap: 'wrap',
}}>
<input
type="text"
className="input"
placeholder="Search text..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
style={{
padding: '4px 10px',
fontSize: '0.75rem',
maxWidth: '180px',
height: '28px',
}}
/>
<button className="btn btn-ghost btn-sm" onClick={copyTextContent} style={{ padding: '4px 8px', fontSize: '0.75rem', display: 'flex', alignItems: 'center', gap: '4px' }}>
<CopyIcon size={12} /> {copiedText ? 'Copied!' : 'Copy Text'}
</button>
</div>
{/* Text Area */}
<div style={{
maxHeight: '420px',
overflow: 'auto',
background: 'var(--bg-surface)',
display: 'flex',
fontFamily: 'var(--font-mono, monospace)',
fontSize: '0.8125rem',
lineHeight: 1.6,
}}>
{textError ? (
<div style={{ padding: '16px', color: 'var(--danger)' }}>{textError}</div>
) : !text ? (
<div style={{ padding: '16px', color: 'var(--text-muted)' }}>Loading preview...</div>
) : (
<div style={{ display: 'flex', width: '100%' }}>
{/* Line Numbers gutter */}
<div style={{
padding: '16px 12px',
borderRight: '1px solid var(--border-subtle)',
background: 'var(--bg-elevated)',
color: 'var(--text-muted)',
textAlign: 'right',
userSelect: 'none',
minWidth: '40px',
}}>
{filteredLines.map((item) => (
<div key={item.idx}>{item.idx + 1}</div>
))}
</div>
{/* Lines Content */}
<pre style={{
margin: 0,
padding: '16px',
flex: 1,
overflow: 'auto',
whiteSpace: 'pre-wrap',
wordBreak: 'break-all',
color: 'var(--text-secondary)',
}}>
{filteredLines.map((item) => (
<div key={item.idx} style={{
background: searchTerm && item.line.toLowerCase().includes(searchTerm.toLowerCase()) ? 'rgba(74,144,255,0.15)' : 'transparent',
}}>
{item.line || ' '}
</div>
))}
</pre>
</div>
)}
</div>
</div>
);
}
return null;
}
export default function FileViewPage() {
const { linkId } = useParams();
const [file, setFile] = useState(sharedFile);
const [error, setError] = useState('');
const [copied, setCopied] = useState(false);
const [downloading, setDownloading] = useState(false);
const [downloadProgress, setDownloadProgress] = useState(0);
const addToast = useToast();
const countdown = useCountdown(file.expiresAt);
useEffect(() => {
if (!linkId) return;
let active = true;
resolveLink(linkId)
.then((remoteFile) => {
if (!active) return;
setFile(remoteFile);
setError('');
})
.catch((err) => {
if (!active) return;
setError(err.message);
});
return () => {
active = false;
};
}, [linkId]);
const copyLink = () => {
navigator.clipboard?.writeText(file.link);
setCopied(true);
addToast('Link copied to clipboard', 'success');
setTimeout(() => setCopied(false), 2000);
};
const handleDownload = () => {
if (linkId) {
setDownloading(true);
setDownloadProgress(100);
window.location.href = downloadUrl(linkId);
setTimeout(() => {
setDownloading(false);
setDownloadProgress(0);
}, 1200);
return;
}
setDownloading(true);
setDownloadProgress(0);
let progress = 0;
const interval = setInterval(() => {
progress += Math.random() * 20 + 10;
if (progress >= 100) {
progress = 100;
clearInterval(interval);
setDownloadProgress(100);
setTimeout(() => {
setDownloading(false);
setDownloadProgress(0);
addToast('Download complete — this is a demo', 'success');
}, 400);
} else {
setDownloadProgress(Math.min(progress, 99));
}
}, 200);
};
return (
<div className="page-enter" style={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '24px',
position: 'relative',
overflow: 'hidden',
}}>
{/* Animated background orbs */}
<div className="orb orb-1" style={{ top: '10%', left: '-5%', opacity: 0.4 }} />
<div className="orb orb-3" style={{ bottom: '10%', right: '-5%', opacity: 0.3 }} />
<div style={{ maxWidth: file.preview?.available ? '840px' : '480px', width: '100%', position: 'relative', zIndex: 1 }}>
{/* Header badge */}
<div style={{ textAlign: 'center', marginBottom: '32px', animation: 'fadeIn 0.4s ease' }}>
<div style={{
width: '56px',
height: '56px',
margin: '0 auto 16px',
borderRadius: '16px',
background: 'var(--accent-muted)',
border: '1px solid var(--accent-border)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'var(--accent)',
animation: 'pulseGlow 3s ease-in-out infinite',
}}>
<ShieldIcon size={24} />
</div>
<h1 className="heading-md" style={{ marginBottom: '8px' }}>
Shared with you
</h1>
<p className="text-secondary" style={{ fontSize: '0.9375rem' }}>
{error || 'Someone shared this file via CShare. Download it before the link expires.'}
</p>
</div>
{/* File card with glow */}
<div
className="card glow-card"
style={{
padding: '28px',
animation: 'scaleIn 0.4s 0.1s ease both',
position: 'relative',
overflow: 'hidden',
}}
>
{/* Accent bar with glow */}
<div style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '3px',
background: 'linear-gradient(90deg, var(--accent), var(--accent-hover), var(--accent))',
backgroundSize: '200% 100%',
animation: 'gradientMove 3s ease infinite',
boxShadow: '0 2px 12px rgba(74, 144, 255, 0.3)',
}} />
{/* File icon + name */}
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '16px', marginBottom: '24px' }}>
<div style={{
width: '56px',
height: '56px',
borderRadius: 'var(--radius-md)',
background: 'var(--bg-surface)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontFamily: 'var(--font-heading)',
fontWeight: 700,
fontSize: '0.75rem',
letterSpacing: '0.05em',
color: getFileIconColor(file.type),
flexShrink: 0,
border: '1px solid var(--border-subtle)',
}}>
{getFileIcon(file.type)}
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<h3 style={{
fontFamily: 'var(--font-heading)',
fontWeight: 600,
fontSize: '1.0625rem',
marginBottom: '4px',
wordBreak: 'break-word',
}}>
{file.name}
</h3>
<div style={{ fontSize: '0.8125rem', color: 'var(--text-muted)' }}>
{formatFileSize(file.size)}
</div>
</div>
</div>
{/* Metadata grid */}
<div style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '16px',
marginBottom: '24px',
padding: '16px',
background: 'var(--bg-surface)',
borderRadius: 'var(--radius-md)',
border: '1px solid var(--border-subtle)',
}}>
<div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-muted)', marginBottom: '4px', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
Uploaded by
</div>
<div style={{ fontFamily: 'var(--font-heading)', fontWeight: 500, fontSize: '0.9375rem' }}>
{file.uploader}
</div>
</div>
<div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-muted)', marginBottom: '4px', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
Uploaded on
</div>
<div style={{ fontFamily: 'var(--font-heading)', fontWeight: 500, fontSize: '0.9375rem' }}>
{formatDate(file.uploadedAt)}
</div>
</div>
<div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-muted)', marginBottom: '4px', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
Expires
</div>
<div style={{
fontFamily: 'var(--font-heading)',
fontWeight: 600,
fontSize: '0.9375rem',
color: 'var(--accent)',
display: 'flex',
alignItems: 'center',
gap: '6px',
}}>
<ClockIcon size={14} />
{countdown}
</div>
</div>
<div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-muted)', marginBottom: '4px', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
Views
</div>
<div style={{ fontFamily: 'var(--font-heading)', fontWeight: 500, fontSize: '0.9375rem' }}>
{file.views}
</div>
</div>
</div>
<FilePreview file={file} linkId={linkId} />
{/* Actions */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
<button
className="btn btn-primary btn-lg"
onClick={handleDownload}
disabled={downloading || Boolean(error)}
style={{ width: '100%', position: 'relative', overflow: 'hidden' }}
>
{downloading ? (
<>
<span style={{
width: '16px',
height: '16px',
border: '2px solid var(--bg-primary)',
borderTopColor: 'transparent',
borderRadius: '50%',
animation: 'spin 0.6s linear infinite',
display: 'inline-block',
}} />
{downloadProgress >= 100 ? 'Complete!' : `${Math.round(downloadProgress)}%`}
</>
) : (
<>
<DownloadIcon size={18} />
Download File
</>
)}
</button>
{/* Download progress bar */}
{downloading && (
<div style={{
height: '3px',
borderRadius: '2px',
background: 'var(--bg-surface)',
overflow: 'hidden',
animation: 'fadeIn 0.2s ease',
}}>
<div
className="shimmer-progress"
style={{
height: '100%',
width: `${downloadProgress}%`,
borderRadius: '2px',
background: 'linear-gradient(90deg, var(--accent), var(--accent-hover))',
transition: 'width 0.2s ease',
boxShadow: '0 0 8px rgba(74, 144, 255, 0.3)',
}}
/>
</div>
)}
<button
className="btn btn-secondary"
onClick={copyLink}
style={{ width: '100%', transition: 'all var(--transition-base)' }}
>
{copied ? <CheckIcon size={16} style={{ color: 'var(--success)' }} /> : <CopyIcon size={16} />}
{copied ? 'Link Copied' : 'Copy Share Link'}
</button>
</div>
</div>
{/* Trust footer */}
<div style={{
marginTop: '24px',
textAlign: 'center',
animation: 'fadeIn 0.4s 0.3s ease both',
}}>
<div style={{
display: 'inline-flex',
alignItems: 'center',
gap: '6px',
fontSize: '0.75rem',
color: 'var(--text-muted)',
}}>
<ShieldIcon size={14} />
End-to-end encrypted and auto-expiring
</div>
</div>
</div>
</div>
);
}