diff --git a/cshare-app/src/pages/DashboardPage.jsx b/cshare-app/src/pages/DashboardPage.jsx new file mode 100644 index 0000000..e11f668 --- /dev/null +++ b/cshare-app/src/pages/DashboardPage.jsx @@ -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 ( +
Manage your shared files and track engagement.
++ {loading ? 'Loading your files…' : searchQuery ? 'No files match your search' : 'No files yet'} +
+ {!loading && !searchQuery && ( + + )} ++ {loading ? 'Loading your files…' : searchQuery ? 'No files match your search' : 'No files yet'} +
+ {!loading && !searchQuery && ( + + )} +