diff --git a/cshare-app/src/pages/UploadPage.jsx b/cshare-app/src/pages/UploadPage.jsx new file mode 100644 index 0000000..5a00be7 --- /dev/null +++ b/cshare-app/src/pages/UploadPage.jsx @@ -0,0 +1,453 @@ +import { useState, useCallback, useRef } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { UploadIcon, CopyIcon, CheckIcon, XIcon, TrashIcon } from '../components/Icons'; +import { formatFileSize, getFileIcon, getFileIconColor } from '../data/mockData'; +import { useToast } from '../components/ToastContext'; +import { uploadFile } from '../lib/api'; + +export default function UploadPage() { + const navigate = useNavigate(); + const [files, setFiles] = useState([]); + const [isDragging, setIsDragging] = useState(false); + const [sharedLinks, setSharedLinks] = useState({}); + const [copiedId, setCopiedId] = useState(null); + const dropRef = useRef(null); + const addToast = useToast(); + + const addFiles = useCallback((fileList) => { + const newFiles = fileList.map((f) => ({ + id: 'f' + Date.now() + Math.random().toString(36).slice(2, 6), + rawFile: f, + name: f.name, + size: f.size, + type: f.type || 'application/octet-stream', + progress: 0, + status: 'uploading', + })); + setFiles((prev) => [...prev, ...newFiles]); + + newFiles.forEach((file) => { + uploadFile(file.rawFile, { + onProgress: (progress) => { + setFiles((prev) => + prev.map((f) => + f.id === file.id ? { ...f, progress: Math.min(progress, 99) } : f + ) + ); + }, + }) + .then((payload) => { + setFiles((prev) => + prev.map((f) => + f.id === file.id ? { ...f, progress: 100, status: 'done', apiFile: payload.data } : f + ) + ); + setSharedLinks((prev) => ({ ...prev, [file.id]: payload.data.link })); + addToast(`${file.name} uploaded`, 'success'); + }) + .catch((error) => { + setFiles((prev) => + prev.map((f) => + f.id === file.id ? { ...f, progress: 0, status: 'error', error: error.message } : f + ) + ); + addToast(error.message, 'error'); + }); + }); + }, [addToast]); + + const handleDrop = useCallback((e) => { + e.preventDefault(); + setIsDragging(false); + const droppedFiles = Array.from(e.dataTransfer.files); + addFiles(droppedFiles); + }, [addFiles]); + + const generateShareLink = (fileId) => { + const file = files.find((item) => item.id === fileId); + if (file?.apiFile?.link) { + setSharedLinks((prev) => ({ ...prev, [fileId]: file.apiFile.link })); + addToast('Link ready', 'success'); + } + }; + + const copyLink = (fileId) => { + const link = sharedLinks[fileId]; + if (!link) return; + navigator.clipboard?.writeText(link); + setCopiedId(fileId); + addToast('Link copied to clipboard', 'success'); + setTimeout(() => setCopiedId(null), 2000); + }; + + const removeFile = (fileId) => { + setFiles((prev) => prev.filter((f) => f.id !== fileId)); + setSharedLinks((prev) => { + const next = { ...prev }; + delete next[fileId]; + return next; + }); + }; + + const handleFileInput = (e) => { + const fileList = Array.from(e.target.files || []); + if (fileList.length) addFiles(fileList); + e.target.value = ''; + }; + + return ( +
+
+

Upload files

+

Drop your files below and share them with expiring links.

+
+ + {/* Drop Zone — animated dashed border */} +
{ e.preventDefault(); setIsDragging(true); }} + onDragLeave={() => setIsDragging(false)} + onDrop={handleDrop} + onClick={() => document.getElementById('file-input')?.click()} + className={`drop-zone-animated ${isDragging ? 'dragging' : ''}`} + style={{ + border: 'none', + borderRadius: 'var(--radius-xl)', + background: isDragging ? 'var(--accent-muted)' : 'var(--bg-elevated)', + padding: 'clamp(48px, 6vh, 80px) 24px', + textAlign: 'center', + cursor: 'pointer', + position: 'relative', + overflow: 'hidden', + }} + > + + + {/* Ambient glow behind upload icon when dragging */} + {isDragging && ( +
+ )} + +
+ +
+ +

+ {isDragging ? 'Drop your files here' : 'Drag files here to upload'} +

+

+ or click to browse — up to 500MB per file +

+
+ + {/* File List */} + {files.length > 0 && ( +
+
+

+ {files.filter((f) => f.status === 'done').length} of {files.length} uploaded +

+ {files.every((f) => f.status === 'done') && ( + + )} +
+ +
+ {files.map((file, index) => ( +
{ + e.currentTarget.style.boxShadow = 'var(--shadow-md)'; + }} + onMouseLeave={(e) => { + e.currentTarget.style.boxShadow = 'none'; + }} + > +
+ {/* File icon */} +
+ {getFileIcon(file.type)} +
+ + {/* File info */} +
+
+ {file.name} +
+
+ {formatFileSize(file.size)} + {file.status === 'done' && ( + + + Complete + + )} + {file.status === 'error' && ( + + {file.error || 'Upload failed'} + + )} +
+
+ + {/* Actions */} +
+ {file.status === 'done' && sharedLinks[file.id] && ( + + )} + {file.status === 'done' && !sharedLinks[file.id] && ( + + )} + +
+
+ + {/* Progress bar — with shimmer */} + {file.status === 'uploading' && ( +
+
+
+
+
+ {Math.round(file.progress)}% +
+
+ )} + + {/* Share link display */} + {sharedLinks[file.id] && ( +
+
+ {sharedLinks[file.id]} +
+ {/* One-Click Quick Share Buttons */} +
+ + Quick Share: + + {/* Slack */} + + Slack + + {/* WhatsApp */} + + WhatsApp + + {/* Telegram */} + + Telegram + + {/* Email */} + + Email + +
+
+ )} +
+ ))} +
+
+ )} + + {/* Preview link — go to file view */} +
+

+ Want to see what recipients see? +

+ +
+
+ ); +}