From 9902a5ed363a567cff84765056a38911d731a4f8 Mon Sep 17 00:00:00 2001 From: b3ni15 Date: Thu, 16 Oct 2025 10:00:52 +0200 Subject: [PATCH] refactor: Clean up and enhance upload component with improved UI and progress handling --- src/app/page.tsx | 193 ++++++++++++++++++++++++++--------------------- 1 file changed, 105 insertions(+), 88 deletions(-) diff --git a/src/app/page.tsx b/src/app/page.tsx index bf96422..fe9b3d9 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -5,7 +5,6 @@ import { useState, useEffect, useRef } from 'react'; const CHUNK_SIZE = 8 * 1024 * 1024; // 8 MB const PARALLEL_UPLOADS = 4; -// Helper to convert ArrayBuffer to Base64 string function bufferToBase64(buffer: ArrayBuffer) { let binary = ''; const bytes = new Uint8Array(buffer); @@ -16,14 +15,12 @@ function bufferToBase64(buffer: ArrayBuffer) { return window.btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); } -// Helper to format seconds into MM:SS function formatTime(seconds: number) { const minutes = Math.floor(seconds / 60); const remainingSeconds = Math.floor(seconds % 60); return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`; } -// Helper to format bytes into a readable string function formatBytes(bytes: number, decimals = 2) { if (bytes === 0) return '0 Bytes'; const k = 1024; @@ -38,15 +35,19 @@ export default function Home() { const [uploadState, setUploadState] = useState<'idle' | 'processing' | 'uploading' | 'complete'>('idle'); const [progress, setProgress] = useState(0); const [downloadUrl, setDownloadUrl] = useState(''); - - // New state for detailed progress const [uploadedSize, setUploadedSize] = useState(0); const [uploadSpeed, setUploadSpeed] = useState(0); const [elapsedTime, setElapsedTime] = useState(0); const [etr, setEtr] = useState(0); + const [copyState, setCopyState] = useState<'idle' | 'copied'>('idle'); const timerRef = useRef(null); const lastUploadedSizeRef = useRef(0); + const uploadedSizeRef = useRef(0); + + useEffect(() => { + uploadedSizeRef.current = uploadedSize; + }, [uploadedSize]); useEffect(() => { if (uploadState === 'uploading') { @@ -54,23 +55,17 @@ export default function Home() { lastUploadedSizeRef.current = 0; timerRef.current = setInterval(() => { - // elapsed time is based on a fixed start time setElapsedTime((Date.now() - startTime) / 1000); - - // Speed calculation - const currentUploadedSize = uploadedSizeRef.current; // Get latest size from ref + const currentUploadedSize = uploadedSizeRef.current; const speed = currentUploadedSize - lastUploadedSizeRef.current; setUploadSpeed(speed > 0 ? speed : 0); - // ETR calculation if (speed > 0 && selectedFile) { const remainingSize = selectedFile.size - currentUploadedSize; setEtr(remainingSize / speed); } - lastUploadedSizeRef.current = currentUploadedSize; - }, 1000); // Update every second - + }, 1000); } else if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; @@ -84,12 +79,6 @@ export default function Home() { }; }, [uploadState, selectedFile]); - // Ref to hold the latest uploaded size for the timer to access - const uploadedSizeRef = useRef(0); - useEffect(() => { - uploadedSizeRef.current = uploadedSize; - }, [uploadedSize]); - const handleFileChange = (event: React.ChangeEvent) => { const files = event.target.files; if (files && files.length > 0) { @@ -114,6 +103,12 @@ export default function Home() { event.preventDefault(); }; + const handleCopy = () => { + navigator.clipboard.writeText(downloadUrl); + setCopyState('copied'); + setTimeout(() => setCopyState('idle'), 2000); + }; + const startUploadProcess = async (file: File) => { if (!file) return; @@ -124,14 +119,7 @@ export default function Home() { setElapsedTime(0); setEtr(0); - // 1. Generate encryption key - const fileKey = await window.crypto.subtle.generateKey( - { name: 'AES-GCM', length: 256 }, - true, - ['encrypt', 'decrypt'] - ); - - // 2. Call create-file API + const fileKey = await window.crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']); const numParts = Math.ceil(file.size / CHUNK_SIZE); const createResponse = await fetch('/api/create-file', { method: 'POST', @@ -140,7 +128,6 @@ export default function Home() { }); const { file_id } = await createResponse.json(); - // 3. Chunk and encrypt file setUploadState('uploading'); const chunks: { index: number; data: Blob }[] = []; for (let i = 0; i < numParts; i++) { @@ -148,17 +135,11 @@ export default function Home() { const end = Math.min(start + CHUNK_SIZE, file.size); const chunk = file.slice(start, end); const iv = window.crypto.getRandomValues(new Uint8Array(12)); - const encryptedChunk = await window.crypto.subtle.encrypt( - { name: 'AES-GCM', iv: iv }, - fileKey, - await chunk.arrayBuffer() - ); - // Prepend IV to the encrypted data + const encryptedChunk = await window.crypto.subtle.encrypt({ name: 'AES-GCM', iv: iv }, fileKey, await chunk.arrayBuffer()); const combinedData = new Blob([iv, new Uint8Array(encryptedChunk)]); chunks.push({ index: i, data: combinedData }); } - // 4. Upload parts with concurrency limit let uploadedCount = 0; const uploadChunk = async (chunk: { index: number; data: Blob }) => { const formData = new FormData(); @@ -186,7 +167,6 @@ export default function Home() { await Promise.race(active); } - // 5. Complete upload const completeResponse = await fetch('/api/complete-upload', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -194,7 +174,6 @@ export default function Home() { }); const { download_token } = await completeResponse.json(); - // 6. Generate and show download link const exportedKey = await window.crypto.subtle.exportKey('raw', fileKey); const keyString = bufferToBase64(exportedKey); setUploadState('complete'); @@ -202,59 +181,97 @@ export default function Home() { }; return ( -
-
-

Discord Storage

-

Upload large files, encrypted and stored on Discord. 7-day retention.

- - {uploadState === 'idle' && ( -
- - -
- )} - - {(uploadState === 'processing' || uploadState === 'uploading') && ( -
-

{selectedFile?.name}

-
-
+
+
+
+

Discord Storage

+

Upload large files, encrypted and stored on Discord. 7-day retention.

-
- {formatBytes(uploadedSize)} / {selectedFile ? formatBytes(selectedFile.size) : '0 Bytes'} - {progress}% -
-
- Speed: {formatBytes(uploadSpeed)}/s - Elapsed: {formatTime(elapsedTime)} - ETR: {etr > 0 && etr < Infinity ? formatTime(etr) : '--:--'} -
-
- )} - {uploadState === 'complete' && ( -
-

Upload Complete!

-

Save this link to download your file. The link contains the decryption key.

- (e.target as HTMLInputElement).select()} - className="w-full p-2 bg-gray-900 rounded border border-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500" - /> -
- )} +
+ {uploadState === 'idle' && ( +
+ + +
+ )} -
+ {(uploadState === 'processing' || uploadState === 'uploading') && ( +
+
+

Uploading...

+ + + + + + +
+

{selectedFile?.name}

+ +
+
+
+
+
+ {progress}% + {formatBytes(uploadedSize)} / {selectedFile ? formatBytes(selectedFile.size) : '0 Bytes'} +
+
+ +
+
+

Speed

+

{formatBytes(uploadSpeed)}/s

+
+
+

Elapsed

+

{formatTime(elapsedTime)}

+
+
+

ETR

+

{etr > 0 && etr < Infinity ? formatTime(etr) : '--:--'}

+
+
+
+ )} + + {uploadState === 'complete' && ( +
+ + + +

Upload Complete!

+

Your file is ready. Save the link below.

+
+ + +
+

This link contains the decryption key. Keep it safe!

+
+ )} +
+
); -} \ No newline at end of file +}