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

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:47 +03:30
parent 536c9c6bce
commit fd7dfd91c5
+717
View File
@@ -0,0 +1,717 @@
import { useState, useMemo, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { CopyIcon, CheckIcon, TrashIcon, EyeIcon, ChartIcon, ClockIcon, FileIcon, DownloadIcon } from '../components/Icons';
import { formatFileSize, formatDateRelative, getFileIcon, getFileIconColor } from '../data/mockData';
import { useToast } from '../components/ToastContext';
import { deleteFile as deleteRemoteFile, getDashboardStats, listFiles, getDashboardActivity, getDashboardStorage, downloadUrl } from '../lib/api';
import { SkeletonStatCards, SkeletonFileRows } from '../components/SkeletonLoader';
import { StorageBar } from '../components/StorageBar';
import { ActivityFeed } from '../components/ActivityFeed';
import FileDetailSlideOver from '../components/FileDetailSlideOver';
function AnimatedNumber({ value, duration = 1200 }) {
const [display, setDisplay] = useState(0);
const prevValue = useRef(0);
useEffect(() => {
const start = prevValue.current;
const end = typeof value === 'number' ? value : 0;
if (start === end) { setDisplay(end); return; }
const startTime = performance.now();
const animate = (now) => {
const elapsed = now - startTime;
const progress = Math.min(elapsed / duration, 1);
const eased = 1 - Math.pow(1 - progress, 3);
setDisplay(Math.round(start + (end - start) * eased));
if (progress < 1) requestAnimationFrame(animate);
};
requestAnimationFrame(animate);
prevValue.current = end;
return () => {};
}, [value, duration]);
return <>{typeof value === 'string' ? value : display.toLocaleString()}</>;
}
export default function DashboardPage() {
const navigate = useNavigate();
const [files, setFiles] = useState([]);
const [copiedId, setCopiedId] = useState(null);
const [deleteConfirm, setDeleteConfirm] = useState(null);
const [selectedFile, setSelectedFile] = useState(null);
const [searchQuery, setSearchQuery] = useState('');
const [loading, setLoading] = useState(true);
const [stats, setStats] = useState(null);
const [sortBy, setSortBy] = useState('date');
const [filterType, setFilterType] = useState('all');
const [selectedIds, setSelectedIds] = useState(new Set());
const [activities, setActivities] = useState([]);
const [storageBreakdownReal, setStorageBreakdownReal] = useState(null);
const [showBatchDeleteConfirm, setShowBatchDeleteConfirm] = useState(false);
const addToast = useToast();
useEffect(() => {
if (selectedIds.size === 0) {
setShowBatchDeleteConfirm(false);
}
}, [selectedIds.size]);
useEffect(() => {
let active = true;
Promise.all([
listFiles(),
getDashboardStats(),
getDashboardActivity(),
getDashboardStorage(),
])
.then(([remoteFiles, remoteStats, remoteActivity, remoteStorage]) => {
if (!active) return;
const CATEGORY_MAP = {
document: { label: 'Documents', color: '#E84848' },
image: { label: 'Images', color: '#38C87A' },
video: { label: 'Videos', color: '#F5A623' },
audio: { label: 'Audio', color: '#9B59B6' },
archive: { label: 'Archives', color: '#4A90FF' },
other: { label: 'Other', color: '#9494A0' },
};
const byType = {
'Documents': { size: 0, color: '#E84848' },
'Archives': { size: 0, color: '#4A90FF' },
'Images': { size: 0, color: '#38C87A' },
'Videos': { size: 0, color: '#F5A623' },
'Audio': { size: 0, color: '#9B59B6' },
'Other': { size: 0, color: '#9494A0' },
};
let used = 0;
if (remoteStorage?.breakdown) {
remoteStorage.breakdown.forEach((item) => {
const config = CATEGORY_MAP[item.category] || CATEGORY_MAP.other;
byType[config.label] = {
size: item.sizeBytes,
color: config.color,
};
used += item.sizeBytes;
});
}
const mappedStorage = {
used,
total: 536870912, // 512 MB
byType,
};
const mappedActivities = (remoteActivity || []).map((act) => ({
...act,
at: act.createdAt,
}));
setFiles(remoteFiles);
setStats(remoteStats);
setActivities(mappedActivities);
setStorageBreakdownReal(mappedStorage);
})
.catch((err) => {
console.error(err);
if (active) addToast('Could not load files. Check your connection.', 'error');
})
.finally(() => {
if (active) setLoading(false);
});
return () => {
active = false;
};
}, [addToast]);
const filteredFiles = useMemo(() => {
let result = files;
// Search
if (searchQuery) {
const q = searchQuery.toLowerCase();
result = result.filter(
(f) => f.name.toLowerCase().includes(q) || f.uploader.toLowerCase().includes(q)
);
}
// Type filter
if (filterType !== 'all') {
result = result.filter((f) => {
if (filterType === 'image') return f.type.startsWith('image/');
if (filterType === 'document') return f.type === 'application/pdf' || f.type.startsWith('text/');
if (filterType === 'archive') return f.type === 'application/zip';
return f.type === 'application/octet-stream';
});
}
// Sort
result = [...result].sort((a, b) => {
if (sortBy === 'name') return a.name.localeCompare(b.name);
if (sortBy === 'size') return b.size - a.size;
if (sortBy === 'views') return b.views - a.views;
return new Date(b.uploadedAt) - new Date(a.uploadedAt);
});
return result;
}, [files, searchQuery, filterType, sortBy]);
const totalViews = stats?.totalViews ?? files.reduce((sum, f) => sum + f.views, 0);
const totalDownloads = stats?.totalDownloads ?? files.reduce((sum, f) => sum + f.downloads, 0);
const copyLink = (file) => {
navigator.clipboard?.writeText(file.link);
setCopiedId(file.id);
addToast('Link copied to clipboard', 'success');
setTimeout(() => setCopiedId(null), 2000);
};
const deleteFile = async (fileId) => {
const previous = files;
setFiles((prev) => prev.filter((f) => f.id !== fileId));
setDeleteConfirm(null);
try {
await deleteRemoteFile(fileId);
addToast('File deleted', 'info');
} catch (error) {
setFiles(previous);
addToast(error.message, 'error');
}
};
const toggleSelect = (fileId) => {
setSelectedIds((prev) => {
const next = new Set(prev);
if (next.has(fileId)) next.delete(fileId);
else next.add(fileId);
return next;
});
};
const toggleSelectAll = () => {
if (selectedIds.size === filteredFiles.length) {
setSelectedIds(new Set());
} else {
setSelectedIds(new Set(filteredFiles.map((f) => f.id)));
}
};
const batchDelete = async () => {
const ids = [...selectedIds];
const previous = files;
setFiles((prev) => prev.filter((f) => !selectedIds.has(f.id)));
setSelectedIds(new Set());
try {
await Promise.all(ids.map((id) => deleteRemoteFile(id)));
addToast(`${ids.length} file${ids.length > 1 ? 's' : ''} deleted`, 'info');
} catch {
setFiles(previous);
addToast('Some files could not be deleted', 'error');
}
};
const batchCopyLinks = () => {
const links = filteredFiles
.filter((f) => selectedIds.has(f.id))
.map((f) => f.link)
.join('\n');
navigator.clipboard?.writeText(links);
addToast(`${selectedIds.size} link${selectedIds.size > 1 ? 's' : ''} copied`, 'success');
setSelectedIds(new Set());
};
const batchDownload = () => {
const selectedFiles = files.filter((f) => selectedIds.has(f.id));
selectedFiles.forEach((file, index) => {
setTimeout(() => {
const link = document.createElement('a');
link.href = downloadUrl(file.linkId || file.id);
link.setAttribute('download', file.name);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}, index * 400);
});
addToast(`Initiating download for ${selectedFiles.length} file${selectedFiles.length > 1 ? 's' : ''}`, 'success');
setSelectedIds(new Set());
};
return (
<div className="page-enter" style={{ maxWidth: '1120px', margin: '0 auto', padding: '48px 24px' }}>
{/* Header */}
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: '40px',
flexWrap: 'wrap',
gap: '16px',
}}>
<div>
<h1 className="heading-lg" style={{ marginBottom: '8px' }}>Dashboard</h1>
<p className="text-secondary">Manage your shared files and track engagement.</p>
</div>
<button className="btn btn-primary" onClick={() => navigate('/upload')}>
<FileIcon size={16} />
Upload New
</button>
</div>
{/* Stats — with animated counters */}
{loading ? (
<div style={{ marginBottom: '40px' }}>
<SkeletonStatCards count={3} />
</div>
) : (
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
gap: '16px',
marginBottom: '40px',
}}>
{[
{ label: 'Total Files', value: files.length, icon: <FileIcon size={20} />, color: 'var(--accent)', trend: '+2 this week' },
{ label: 'Total Views', value: totalViews, icon: <EyeIcon size={20} />, color: 'var(--accent)', trend: '+18 today' },
{ label: 'Total Downloads', value: totalDownloads, icon: <ChartIcon size={20} />, color: 'var(--success)', trend: '+5 today' },
].map((stat, i) => (
<div
key={i}
className="card glow-card"
style={{
padding: '20px 24px',
animation: `fadeInUp 0.4s ${0.1 * i}s ease both`,
position: 'relative',
overflow: 'hidden',
}}
>
{/* Subtle gradient top edge */}
<div style={{
position: 'absolute', top: 0, left: 0, right: 0, height: '2px',
background: `linear-gradient(90deg, transparent, ${stat.color}, transparent)`,
opacity: 0.5,
}} />
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '12px' }}>
<span style={{ fontSize: '0.6875rem', color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.08em', fontFamily: 'var(--font-heading)', fontWeight: 600 }}>
{stat.label}
</span>
<span style={{
color: stat.color, width: '32px', height: '32px',
borderRadius: 'var(--radius-sm)', background: `${stat.color}18`,
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
{stat.icon}
</span>
</div>
<div style={{ fontFamily: 'var(--font-heading)', fontWeight: 700, fontSize: '2.25rem', letterSpacing: '-0.04em', lineHeight: 1, marginBottom: '8px' }}>
<AnimatedNumber value={stat.value} />
</div>
<div style={{ fontSize: '0.75rem', color: stat.color, fontFamily: 'var(--font-body)', fontWeight: 500, opacity: 0.8 }}>
↑ {stat.trend}
</div>
</div>
))}
</div>
)}
{/* Storage + Activity sidebar */}
{!loading && (
<div
className="dashboard-sidebar-grid"
style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '16px',
marginBottom: '32px',
}}
>
<StorageBar
used={storageBreakdownReal?.used ?? 0}
total={storageBreakdownReal?.total ?? 536870912}
byType={storageBreakdownReal?.byType ?? {}}
/>
<ActivityFeed activities={activities} />
</div>
)}
{/* Search & Filter */}
{/* Section label */}
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '16px' }}>
<span style={{ color: 'var(--accent)', fontFamily: 'var(--font-heading)', fontWeight: 700, fontSize: '0.875rem' }}>//</span>
<span style={{ color: 'var(--text-muted)', fontFamily: 'var(--font-heading)', fontWeight: 600, fontSize: '0.75rem', letterSpacing: '0.12em', textTransform: 'uppercase' }}>Your Files</span>
<span style={{ marginLeft: 'auto', fontSize: '0.75rem', color: 'var(--text-muted)', fontFamily: 'var(--font-heading)' }}>
{loading ? 'Loading...' : `${filteredFiles.length} of ${files.length}`}
</span>
</div>
<div style={{
display: 'flex',
alignItems: 'center',
gap: '12px',
marginBottom: '12px',
flexWrap: 'wrap',
}}>
<div style={{ flex: 1, position: 'relative', minWidth: '200px' }}>
<input
className="input"
type="text"
placeholder="Search files..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
style={{ paddingLeft: '40px' }}
/>
<div style={{
position: 'absolute',
left: '14px',
top: '50%',
transform: 'translateY(-50%)',
color: 'var(--text-muted)',
pointerEvents: 'none',
}}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="11" cy="11" r="8" /><line x1="21" y1="21" x2="16.65" y2="16.65" />
</svg>
</div>
</div>
{/* Sort dropdown */}
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
style={{
padding: '8px 12px', fontSize: '0.8125rem', fontFamily: 'var(--font-body)',
background: 'var(--bg-surface)', color: 'var(--text-secondary)',
border: '1px solid var(--border)', borderRadius: 'var(--radius-sm)',
outline: 'none', cursor: 'pointer',
}}
>
<option value="date">Newest first</option>
<option value="name">Name A-Z</option>
<option value="size">Largest first</option>
<option value="views">Most views</option>
</select>
</div>
{/* Filter chips */}
<div className="filter-chips" style={{ display: 'flex', gap: '6px', marginBottom: '20px', flexWrap: 'wrap' }}>
{[
{ key: 'all', label: 'All' },
{ key: 'image', label: 'Images' },
{ key: 'document', label: 'Documents' },
{ key: 'archive', label: 'Archives' },
{ key: 'design', label: 'Design' },
].map(({ key, label }) => (
<button
key={key}
onClick={() => setFilterType(key)}
style={{
padding: '5px 14px', fontSize: '0.75rem', fontFamily: 'var(--font-body)', fontWeight: 500,
background: filterType === key ? 'var(--accent-muted)' : 'var(--bg-surface)',
color: filterType === key ? 'var(--accent)' : 'var(--text-muted)',
border: `1px solid ${filterType === key ? 'var(--accent-border)' : 'var(--border-subtle)'}`,
borderRadius: '100px', cursor: 'pointer',
transition: 'all var(--transition-fast)',
}}
>
{label}
</button>
))}
</div>
{/* Sleek, Glass-morphic Floating Bulk Actions Bar */}
<div style={{
position: 'fixed',
bottom: selectedIds.size > 0 ? '24px' : '-100px',
left: '50%',
transform: 'translateX(-50%)',
background: 'rgba(22, 22, 26, 0.85)',
backdropFilter: 'blur(12px)',
WebkitBackdropFilter: 'blur(12px)',
border: '1px solid var(--accent-border)',
boxShadow: '0 20px 40px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(74, 144, 255, 0.08)',
padding: '12px 24px',
borderRadius: '100px',
display: 'flex',
alignItems: 'center',
gap: '16px',
zIndex: 500,
width: 'max-content',
maxWidth: 'calc(100% - 32px)',
opacity: selectedIds.size > 0 ? 1 : 0,
pointerEvents: selectedIds.size > 0 ? 'auto' : 'none',
transition: 'bottom 0.4s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.3s ease',
}}>
<div style={{
background: 'var(--accent-muted)',
border: '1px solid var(--accent-border)',
borderRadius: '100px',
padding: '4px 12px',
fontFamily: 'var(--font-heading)',
fontWeight: 700,
fontSize: '0.75rem',
color: 'var(--accent)',
whiteSpace: 'nowrap',
}}>
{selectedIds.size} Selected
</div>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<button className="btn btn-ghost btn-sm" onClick={batchCopyLinks} style={{ fontSize: '0.75rem', height: '32px' }}>
<CopyIcon size={12} /> Copy All Links
</button>
<button className="btn btn-ghost btn-sm" onClick={batchDownload} style={{ fontSize: '0.75rem', height: '32px' }}>
<DownloadIcon size={12} /> Download Selected
</button>
{showBatchDeleteConfirm ? (
<div style={{ display: 'flex', gap: '4px', alignItems: 'center', background: 'var(--danger-muted)', border: '1px solid rgba(232, 72, 72, 0.3)', borderRadius: '100px', padding: '2px 8px' }}>
<span style={{ fontSize: '0.6875rem', color: 'var(--danger)', fontWeight: 600, paddingRight: '4px' }}>Confirm?</span>
<button className="btn btn-danger btn-sm" onClick={batchDelete} style={{ fontSize: '0.6875rem', height: '24px', padding: '0 8px', minWidth: '40px' }}>
Yes
</button>
<button className="btn btn-ghost btn-sm" onClick={() => setShowBatchDeleteConfirm(false)} style={{ fontSize: '0.6875rem', height: '24px', padding: '0 8px', minWidth: '40px' }}>
No
</button>
</div>
) : (
<button className="btn btn-danger btn-sm" onClick={() => setShowBatchDeleteConfirm(true)} style={{ fontSize: '0.75rem', height: '32px', borderRadius: '100px' }}>
<TrashIcon size={12} /> Delete
</button>
)}
<div style={{ width: '1px', height: '16px', background: 'var(--border-subtle)', margin: '0 4px' }} />
<button className="btn btn-ghost btn-sm" onClick={() => setSelectedIds(new Set())} style={{ fontSize: '0.75rem', height: '32px', color: 'var(--text-muted)' }}>
Cancel
</button>
</div>
</div>
{/* File Cards (mobile-friendly) */}
{loading ? (
<>
<div className="file-table-desktop"><SkeletonFileRows count={4} /></div>
<div className="file-table-mobile" style={{ display: 'none', flexDirection: 'column', gap: '12px' }}>
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="card" style={{ padding: '20px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '16px' }}>
<div className="skeleton" style={{ width: '44px', height: '44px', borderRadius: 'var(--radius-md)' }} />
<div style={{ flex: 1 }}>
<div className="skeleton" style={{ width: '65%', height: '14px', marginBottom: '6px' }} />
<div className="skeleton" style={{ width: '40%', height: '10px' }} />
</div>
</div>
<div className="skeleton" style={{ width: '100%', height: '10px', marginBottom: '8px' }} />
<div className="skeleton" style={{ width: '80%', height: '10px' }} />
</div>
))}
</div>
</>
) : (
<>
<div className="file-table-desktop" style={{
background: 'var(--bg-elevated)',
border: '1px solid var(--border-subtle)',
borderRadius: 'var(--radius-lg)',
overflow: 'hidden',
}}>
{/* Table header */}
<div style={{
display: 'grid',
gridTemplateColumns: '36px 1fr 100px 120px 80px 80px 120px',
gap: '12px',
padding: '14px 20px',
borderBottom: '1px solid var(--border-subtle)',
fontSize: '0.75rem',
color: 'var(--text-muted)',
textTransform: 'uppercase',
letterSpacing: '0.06em',
fontFamily: 'var(--font-heading)',
fontWeight: 600,
alignItems: 'center',
}}>
<input
type="checkbox"
checked={selectedIds.size === filteredFiles.length && filteredFiles.length > 0}
onChange={toggleSelectAll}
style={{ accentColor: 'var(--accent)', cursor: 'pointer', width: '16px', height: '16px' }}
/>
<span>File</span>
<span>Size</span>
<span>Expiry</span>
<span>Views</span>
<span>DL</span>
<span>Actions</span>
</div>
{filteredFiles.length === 0 && (
<div style={{ padding: '64px 20px', textAlign: 'center', color: 'var(--text-muted)' }}>
<FileIcon size={36} style={{ marginBottom: '16px', opacity: 0.3 }} />
<p style={{ fontFamily: 'var(--font-heading)', fontWeight: 600, marginBottom: '8px', color: 'var(--text-secondary)' }}>
{loading ? 'Loading your files…' : searchQuery ? 'No files match your search' : 'No files yet'}
</p>
{!loading && !searchQuery && (
<button className="btn btn-primary btn-sm" style={{ marginTop: '12px' }} onClick={() => navigate('/upload')}>
Upload your first file
</button>
)}
</div>
)}
{filteredFiles.map((file, i) => (
<div
key={file.id}
style={{
display: 'grid',
gridTemplateColumns: '36px 1fr 100px 120px 80px 80px 120px',
gap: '12px',
alignItems: 'center',
padding: '16px 20px',
borderBottom: '1px solid var(--border-subtle)',
transition: 'all var(--transition-fast)',
animation: `fadeIn 0.3s ${0.05 * i}s ease both`,
cursor: 'default',
background: selectedIds.has(file.id) ? 'var(--accent-muted)' : 'transparent',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'var(--bg-hover)';
e.currentTarget.style.boxShadow = 'inset 3px 0 0 var(--accent)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = selectedIds.has(file.id) ? 'var(--accent-muted)' : 'transparent';
e.currentTarget.style.boxShadow = 'none';
}}
>
<input
type="checkbox"
checked={selectedIds.has(file.id)}
onChange={() => toggleSelect(file.id)}
style={{ accentColor: 'var(--accent)', cursor: 'pointer', width: '16px', height: '16px' }}
onClick={(e) => e.stopPropagation()}
/>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', minWidth: 0 }}>
<div style={{
width: '36px', height: '36px', borderRadius: 'var(--radius-sm)',
background: 'var(--bg-surface)', display: 'flex', alignItems: 'center', justifyContent: 'center',
fontFamily: 'var(--font-heading)', fontWeight: 700, fontSize: '0.5625rem',
letterSpacing: '0.05em', color: getFileIconColor(file.type), flexShrink: 0,
border: '1px solid var(--border-subtle)',
}}>
{getFileIcon(file.type)}
</div>
<div style={{ minWidth: 0 }}>
<div style={{
fontFamily: 'var(--font-heading)', fontWeight: 600, fontSize: '0.875rem',
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
}}>
{file.name}
</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-muted)' }}>by {file.uploader}</div>
</div>
</div>
<span style={{ fontSize: '0.8125rem', color: 'var(--text-secondary)' }}>{formatFileSize(file.size)}</span>
<span style={{
fontSize: '0.8125rem', color: formatDateRelative(file.expiresAt) === 'Expired' ? 'var(--danger)' : 'var(--accent)',
fontFamily: 'var(--font-heading)', fontWeight: 500,
}}>
<ClockIcon size={12} style={{ marginRight: '4px', verticalAlign: '-1px' }} />
{formatDateRelative(file.expiresAt)}
</span>
<span style={{ fontSize: '0.8125rem', color: 'var(--text-secondary)' }}>{file.views}</span>
<span style={{ fontSize: '0.8125rem', color: 'var(--text-secondary)' }}>{file.downloads}</span>
<div style={{ display: 'flex', gap: '4px' }}>
<button className="btn btn-ghost btn-icon btn-sm" onClick={() => copyLink(file)} title="Copy link">
{copiedId === file.id ? <CheckIcon size={14} style={{ color: 'var(--success)' }} /> : <CopyIcon size={14} />}
</button>
<button className="btn btn-ghost btn-icon btn-sm" onClick={() => setSelectedFile(file)} title="View details">
<ChartIcon size={14} />
</button>
{deleteConfirm === file.id ? (
<div style={{ display: 'flex', gap: '4px', animation: 'fadeIn 0.2s ease' }}>
<button className="btn btn-danger btn-sm" onClick={() => deleteFile(file.id)} style={{ fontSize: '0.6875rem', padding: '4px 8px' }}>Yes</button>
<button className="btn btn-ghost btn-sm" onClick={() => setDeleteConfirm(null)} style={{ fontSize: '0.6875rem', padding: '4px 8px' }}>No</button>
</div>
) : (
<button className="btn btn-ghost btn-icon btn-sm" onClick={() => setDeleteConfirm(file.id)} title="Delete" style={{ color: 'var(--danger)' }}>
<TrashIcon size={14} />
</button>
)}
</div>
</div>
))}
</div>
{/* Mobile card layout */}
<div className="file-table-mobile" style={{ display: 'none', flexDirection: 'column', gap: '12px' }}>
{filteredFiles.length === 0 && (
<div style={{ padding: '64px 20px', textAlign: 'center', color: 'var(--text-muted)' }}>
<FileIcon size={36} style={{ marginBottom: '16px', opacity: 0.3 }} />
<p style={{ fontFamily: 'var(--font-heading)', fontWeight: 600, marginBottom: '8px', color: 'var(--text-secondary)' }}>
{loading ? 'Loading your files…' : searchQuery ? 'No files match your search' : 'No files yet'}
</p>
{!loading && !searchQuery && (
<button className="btn btn-primary btn-sm" style={{ marginTop: '12px' }} onClick={() => navigate('/upload')}>
Upload your first file
</button>
)}
</div>
)}
{filteredFiles.map((file, i) => (
<div
key={file.id}
className="card"
style={{ padding: '20px', animation: `fadeIn 0.3s ${0.05 * i}s ease both` }}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '16px' }}>
<div style={{
width: '44px', height: '44px', borderRadius: 'var(--radius-md)',
background: 'var(--bg-surface)', display: 'flex', alignItems: 'center', justifyContent: 'center',
fontFamily: 'var(--font-heading)', fontWeight: 700, fontSize: '0.625rem',
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 }}>
<div style={{ fontFamily: 'var(--font-heading)', fontWeight: 600, fontSize: '0.9375rem', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{file.name}
</div>
<div style={{ fontSize: '0.8125rem', color: 'var(--text-muted)' }}>by {file.uploader}</div>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '8px', marginBottom: '16px' }}>
<div><div style={{ fontSize: '0.6875rem', color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Size</div><div style={{ fontSize: '0.875rem', fontFamily: 'var(--font-heading)', fontWeight: 500 }}>{formatFileSize(file.size)}</div></div>
<div><div style={{ fontSize: '0.6875rem', color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Views</div><div style={{ fontSize: '0.875rem', fontFamily: 'var(--font-heading)', fontWeight: 500 }}>{file.views}</div></div>
<div><div style={{ fontSize: '0.6875rem', color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Expiry</div><div style={{ fontSize: '0.875rem', fontFamily: 'var(--font-heading)', fontWeight: 500, color: 'var(--accent)' }}>{formatDateRelative(file.expiresAt)}</div></div>
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<button className="btn btn-secondary btn-sm" onClick={() => copyLink(file)} style={{ flex: 1 }}>
{copiedId === file.id ? <CheckIcon size={14} /> : <CopyIcon size={14} />} {copiedId === file.id ? 'Copied' : 'Copy Link'}
</button>
<button className="btn btn-ghost btn-sm" onClick={() => setSelectedFile(file)}>
<ChartIcon size={14} /> Details
</button>
<button className="btn btn-ghost btn-sm" style={{ color: 'var(--danger)' }} onClick={() => setDeleteConfirm(deleteConfirm === file.id ? null : file.id)}>
<TrashIcon size={14} />
</button>
</div>
</div>
))}
</div>
</>
)}
{/* File detail slide-over */}
<FileDetailSlideOver
file={selectedFile}
onClose={() => setSelectedFile(null)}
onDelete={(fileId) => {
deleteFile(fileId);
setSelectedFile(null);
}}
/>
</div>
);
}