'use client'; import { useSession } from 'next-auth/react'; import { useState, useEffect, useRef } from 'react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Progress } from '@/components/ui/progress'; import { Textarea } from '@/components/ui/textarea'; import { Card, CardContent } from '@/components/ui/card'; import { Upload, File, X, Send } from 'lucide-react'; import { toast } from 'sonner'; import { formatFileSize } from '@/lib/utils'; import { Loader2 } from 'lucide-react'; const PLAN_LIMITS = { free: { maxFileSizeMB: 2048, maxMonthlyTransferSizeMB: 10240, expiryText: '48 hours after sending', }, rookie: { maxFileSizeMB: Infinity, maxMonthlyTransferSizeMB: 30720, expiryText: '7 days after sending', }, pro: { maxFileSizeMB: Infinity, maxMonthlyTransferSizeMB: 1024 * 1024, expiryText: '30 days after sending', }, }; const CHUNK_SIZE = 10 * 1024 * 1024; // 10MB chunks export default function SendPage() { const { data: session } = useSession(); const [files, setFiles] = useState([]); const [recipient, setRecipient] = useState(''); const [password, setPassword] = useState(''); const [message, setMessage] = useState(''); const [plan, setPlan] = useState<'free' | 'rookie' | 'pro'>('free'); const [remainingMB, setRemainingMB] = useState(Infinity); const [usedMB, setUsedMB] = useState(0); const [uploadProgress, setUploadProgress] = useState(0); const [uploadStep, setUploadStep] = useState(''); const [uploadMessage, setUploadMessage] = useState(''); const [currentChunk, setCurrentChunk] = useState(0); const [totalChunks, setTotalChunks] = useState(0); const dropRef = useRef(null); const totalSize = files.reduce((acc, file) => acc + file.size, 0); const totalSizeMB = totalSize / 1024 / 1024; const isOverLimit = totalSizeMB > PLAN_LIMITS[plan].maxFileSizeMB || totalSizeMB > remainingMB; useEffect(() => { const fetchPlan = async () => { const res = await fetch('/api/usage'); if (res.ok) { const data = await res.json(); setPlan(data.plan); setRemainingMB(data.remainingMB); setUsedMB(data.usedMB); } }; if (session?.user?.email) fetchPlan(); }, [session]); const handleFileAdd = (newFiles: File[]) => { const unique = newFiles.filter( (newFile) => !files.find( (existing) => existing.name === newFile.name && existing.size === newFile.size ) ); setFiles((prev) => [...prev, ...unique]); }; const handleFileChange = (e: React.ChangeEvent) => { if (!e.target.files) return; handleFileAdd(Array.from(e.target.files)); }; const handleDrop = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); if (e.dataTransfer.files) { handleFileAdd(Array.from(e.dataTransfer.files)); } dropRef.current?.classList.remove('border-blue-500'); }; const removeFile = (index: number) => { setFiles((prev) => prev.filter((_, i) => i !== index)); }; const fileInputRef = useRef(null); const triggerFileInput = () => fileInputRef.current?.click(); const handleSubmit = async () => { if (!files.length) return setUploadMessage('Please select at least one file'); if (!recipient || !password) return setUploadMessage('All fields are required'); if (isOverLimit) return setUploadMessage('File size exceeds your plan limits'); const senderEmail = session?.user?.email; if (!senderEmail) return setUploadMessage('Unable to get sender email. Please log in.'); try { setUploadStep('Preparing files'); const file = files[0]; // Upload first file for now // Calculate chunks from raw file (no encryption) const totalChunksCount = Math.ceil(file.size / CHUNK_SIZE); setTotalChunks(totalChunksCount); // Generate upload ID const uploadId = crypto.randomUUID(); // Upload raw chunks - server will encrypt each chunk setUploadStep('Uploading'); for (let i = 0; i < totalChunksCount; i++) { setCurrentChunk(i + 1); const start = i * CHUNK_SIZE; const end = Math.min(start + CHUNK_SIZE, file.size); const chunkFile = file.slice(start, end); const formData = new FormData(); formData.append('chunk', chunkFile); formData.append('uploadId', uploadId); formData.append('chunkIndex', i.toString()); formData.append('totalChunks', totalChunksCount.toString()); formData.append('chunkSize', CHUNK_SIZE.toString()); // Only send metadata on first chunk if (i === 0) { formData.append('email', recipient); formData.append('password', password); formData.append('sender', senderEmail); formData.append('originalFilename', file.name); const filenames = files.map((f) => f.name); formData.append('filenames', JSON.stringify(filenames)); formData.append('message', message); } const uploadRes = await fetch('/api/chunk-upload-v2', { method: 'POST', body: formData, }); if (!uploadRes.ok) { const error = await uploadRes.json(); throw new Error(error.message || `Chunk ${i} upload failed`); } // Update progress const chunkProgress = ((i + 1) / totalChunksCount) * 100; setUploadProgress(Math.round(chunkProgress)); } // Finalize upload setUploadStep('Finalizing'); const finalRes = await fetch(`/api/chunk-upload-v2?uploadId=${uploadId}`, { method: 'PUT', }); if (!finalRes.ok) { const error = await finalRes.json(); throw new Error(error.message || 'Failed to finalize upload'); } const result = await finalRes.json(); setUploadStep(''); setUploadMessage('Success! 🎉'); toast.success('Your transfer has been sent successfully!'); setFiles([]); setRecipient(''); setPassword(''); setUploadProgress(100); setTimeout(() => setUploadMessage(''), 3000); } catch (err: any) { console.error(err); setUploadStep(''); setUploadMessage('Error: ' + err.message); } }; function UploadStatusToast({ step, progress, message, currentChunk, totalChunks, }: { step: string; progress: number; message: string; currentChunk: number; totalChunks: number; }) { const showSpinner = ['Uploading', 'Finalizing'].includes(step); const isError = message.toLowerCase().includes('error') || message.includes('fail'); if (!step && !message) return null; return (
{showSpinner && ( )}

{step}{currentChunk && totalChunks ? ` (${currentChunk}/${totalChunks})` : ''}

{!isError && step === 'Uploading' && ( <>

⚠️ Please do not close this page during upload!

)}
); } return (
{/* File Upload Section */}

Upload Files

{ e.preventDefault(); dropRef.current?.classList.add('border-blue-500'); }} onDragLeave={() => dropRef.current?.classList.remove('border-blue-500') } className="border-2 border-dashed rounded-xl p-8 text-center cursor-pointer transition-all" >

Drag & drop files here

or click to select files

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

Selected Files ({files.length})

{files.map((file, i) => (

{file.name}

{formatFileSize(file.size)}

))}
)} {/* Size Info */}

Total Size: {formatFileSize(totalSize)}

{remainingMB !== Infinity && (

Remaining: {remainingMB.toFixed(2)} MB

)}
{/* Transfer Details Section */}

Transfer Details

setRecipient(e.target.value)} placeholder="recipient@example.com" className="mt-1" />
setPassword(e.target.value)} placeholder="Create a strong password" className="mt-1" />