From c1bceae5e4773350f458b52d40cc91fd7d5feb0f Mon Sep 17 00:00:00 2001 From: stefiosif Date: Mon, 1 Jun 2026 19:12:21 +0300 Subject: [PATCH] refactor(frontend): extract types, API, helpers, and preview from +page.svelte --- frontend/src/lib/api.ts | 74 +++++++ .../src/lib/components/FilePreview.svelte | 34 +++ frontend/src/lib/components/FileRow.svelte | 91 ++++++++ frontend/src/lib/format.ts | 13 ++ frontend/src/lib/types.ts | 6 + frontend/src/routes/+page.svelte | 194 +++--------------- frontend/src/routes/auth/+page.svelte | 21 +- frontend/vite.config.ts | 5 + 8 files changed, 264 insertions(+), 174 deletions(-) create mode 100644 frontend/src/lib/api.ts create mode 100644 frontend/src/lib/components/FilePreview.svelte create mode 100644 frontend/src/lib/components/FileRow.svelte create mode 100644 frontend/src/lib/format.ts create mode 100644 frontend/src/lib/types.ts diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts new file mode 100644 index 0000000..9efac37 --- /dev/null +++ b/frontend/src/lib/api.ts @@ -0,0 +1,74 @@ +import type { FileRecord } from "$lib/types"; + +export async function loadFiles(): Promise { + const res = await fetch('/api/files', { + credentials: 'include' + }); + + if (res.status === 401) { + window.location.href = '/auth'; + return []; + } + + if (!res.ok) { + throw new Error(`Failed to load files: ${res.status}`); + } + + return await res.json() +} + + +export async function uploadFile(file: File): Promise { + const formData = new FormData(); + formData.append('file', file); + const res = await fetch('/api/files', { + method: 'POST', + credentials: 'include', + body: formData + }); + + if (!res.ok) { + throw new Error(`Upload failed: ${res.status}`) + } + + return await res.json() +} + +export async function downloadFile(fileId: number): Promise { + const res = await fetch(`/api/files/${fileId}/download`, { + credentials: 'include' + }); + if (!res.ok) { + throw new Error(`Download failed: ${res.status}`) + } + + return await res.blob() +} + +export async function deleteFile(fileId: number): Promise { + const res = await fetch(`/api/files/${fileId}`, { + method: 'DELETE', + credentials: 'include' + }); + if (!res.ok) { + throw new Error(`Delete failed: ${res.status}`); + } +} + +export async function submit(activeTab: string, username: string, password: string): Promise { + const endpoint = activeTab === 'login' ? '/api/auth/login' : '/api/auth/register'; + const res = await fetch(`${endpoint}`, { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, password }) + }); + + if (!res.ok) { + throw new Error('Invalid credentials'); + } +} + +export async function logout(): Promise { + await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' }); +} diff --git a/frontend/src/lib/components/FilePreview.svelte b/frontend/src/lib/components/FilePreview.svelte new file mode 100644 index 0000000..a471d03 --- /dev/null +++ b/frontend/src/lib/components/FilePreview.svelte @@ -0,0 +1,34 @@ + + + { if (e.key === 'Escape') onclose(); }} /> + + + +
+
e.stopPropagation()} + > +
+ {file.name} + +
+
+ preview coming soon +
+
+
diff --git a/frontend/src/lib/components/FileRow.svelte b/frontend/src/lib/components/FileRow.svelte new file mode 100644 index 0000000..955ec49 --- /dev/null +++ b/frontend/src/lib/components/FileRow.svelte @@ -0,0 +1,91 @@ + + +
{ + if (e.key === 'Enter') { + onpreview(); + } else if (e.key === ' ') { + e.preventDefault(); onselect(); + } + } + } + onclick={onselect} + ondblclick={onpreview} + class="flex items-stretch border-l hover:bg-white/2 transition-all {selected ? 'border-sky-400/50 bg-white/2 ring-1 ring-inset ring-sky-400/50' : 'border-sky-200/20 border-l-transparent hover:border-l-sky-400/50'}" +> + {formatName(file.name)} + {formatSize(file.size)} + {new Date(file.uploadedAt).toLocaleString()} +
+
+ + + + + +
+ + {#if confirming} +
+ +
+ Confirm action + + +
+
+ {/if} +
+
+
diff --git a/frontend/src/lib/format.ts b/frontend/src/lib/format.ts new file mode 100644 index 0000000..5a048f7 --- /dev/null +++ b/frontend/src/lib/format.ts @@ -0,0 +1,13 @@ +export function formatName(name: string): string { + if (name.length > 65) { + name = name.slice(0, 65).concat("..."); + } + return name; + } + +export function formatSize(bytes: number): string { + if (bytes < 1000) return bytes + 'B'; + if (bytes < 1000 * 1000) return (bytes / 1000).toFixed(1) + 'KB'; + if (bytes < 1000 * 1000 * 1000) return (bytes / 1000 / 1000).toFixed(1) + 'MB'; + return (bytes / 1000 / 1000 / 1000).toFixed(1) + 'GB'; +} \ No newline at end of file diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts new file mode 100644 index 0000000..931bc8e --- /dev/null +++ b/frontend/src/lib/types.ts @@ -0,0 +1,6 @@ +export type FileRecord = { + id: number; + name: string; + size: number; + uploadedAt: string; +}; \ No newline at end of file diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index baa3c27..5ea6f7a 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -2,69 +2,40 @@ import { onMount } from 'svelte'; import { fade } from 'svelte/transition'; import { flip } from 'svelte/animate' - - type FileRecord = { - id: number; - name: string; - size: number; - uploadedAt: string; - }; + import type { FileRecord } from '$lib/types'; + import { deleteFile, downloadFile, loadFiles, logout, uploadFile } from '$lib/api'; + import FileRow from '$lib/components/FileRow.svelte'; + import FilePreview from '$lib/components/FilePreview.svelte'; let fileRecords = $state([]); - let confirmDeleteId = $state(null); let selectedFileId = $state(null); let previewFile = $state(null); let search = $state(''); let loading = $state(true); + async function refresh() { + fileRecords = await loadFiles(); + loading = false; + } + let filteredFileRecords = $derived(fileRecords.filter(f => f.name.toLowerCase().includes(search.toLowerCase()) )); let fileInput: HTMLInputElement; - function openFilePicker() { - fileInput.click(); - } - - async function loadFiles() { - const res = await fetch('http://localhost:3000/api/files', { - credentials: 'include' - }); - - if (res.status === 401) { - window.location.href = '/auth'; - return; - } - - fileRecords = await res.json(); - loading = false; - } - - onMount(loadFiles); - - async function uploadFile(event: Event) { + async function handleUpload(event: Event) { const input = event.target as HTMLInputElement; const file = input.files?.[0]; - if (!file) return; - const formData = new FormData(); - formData.append('file', file); - await fetch('http://localhost:3000/api/files', { - method: 'POST', - credentials: 'include', - body: formData - }); + await uploadFile(file) input.value = ''; - await loadFiles(); + await refresh() } - async function downloadFile(fileId: number, fileName: string) { - const res = await fetch(`http://localhost:3000/api/files/${fileId}/download`, { - credentials: 'include' - }); - const blob = await res.blob(); + async function handleDownload(fileId: number, fileName: string) { + const blob = await downloadFile(fileId); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; @@ -73,40 +44,17 @@ URL.revokeObjectURL(url); } - async function copyLink() { - await loadFiles(); - //TODO: Generate sharable link with expiration date, add param fileId: number + async function handleDelete(fileId: number) { + await deleteFile(fileId); + await refresh(); } - - async function deleteFile(fileId: number) { - await fetch(`http://localhost:3000/api/files/${fileId}`, { - method: 'DELETE', - credentials: 'include' - }); - await loadFiles(); - } - - function formatName(name: string): string { - if (name.length > 65) { - name = name.slice(0, 65).concat("..."); - } - return name; - } - - function formatSize(bytes: number): string { - if (bytes < 1000) return bytes + 'B'; - if (bytes < 1000 * 1000) return (bytes / 1000).toFixed(1) + 'KB'; - if (bytes < 1000 * 1000 * 1000) return (bytes / 1000 / 1000).toFixed(1) + 'MB'; - return (bytes / 1000 / 1000 / 1000).toFixed(1) + 'GB'; - } - + async function handleLogout() { - await fetch('http://localhost:3000/api/auth/logout', { - method: 'POST', - credentials: 'include', - }); + await logout(); window.location.href = '/auth'; } + + onMount(refresh)
@@ -133,9 +81,9 @@ />
- +
-
@@ -151,96 +99,22 @@ {#each filteredFileRecords as fileRecord (fileRecord.id)} -
selectedFileId = fileRecord.id} - ondblclick={() => previewFile = fileRecord} - class="flex items-stretch border-l hover:bg-white/2 transition-all {selectedFileId === fileRecord.id ? 'border-sky-400/50 bg-white/2 ring-1 ring-inset ring-sky-400/50' : 'border-sky-200/20 border-l-transparent hover:border-l-sky-400/50'}" - > - {formatName(fileRecord.name)} - {formatSize(fileRecord.size)} - {new Date(fileRecord.uploadedAt).toLocaleString()} -
-
- - - - - -
- - {#if confirmDeleteId === fileRecord.id} -
- -
- Confirm action - - -
-
- {/if} -
-
+
+ selectedFileId = fileRecord.id} + onpreview={() => previewFile = fileRecord} + ondownload={() => handleDownload(fileRecord.id, fileRecord.name)} + ondelete={() => handleDelete(fileRecord.id)} + />
{:else} - {#if !loading} -

No files yet.

- {/if} + {#if !loading}

No files yet.

{/if} {/each}
{#if previewFile} -
previewFile = null} - transition:fade={{ duration: 150 }} - > -
e.stopPropagation()} - > -
- {previewFile.name} - -
-
- preview coming soon -
-
-
+ previewFile = null} /> {/if} diff --git a/frontend/src/routes/auth/+page.svelte b/frontend/src/routes/auth/+page.svelte index d0ae1ac..d4257fe 100644 --- a/frontend/src/routes/auth/+page.svelte +++ b/frontend/src/routes/auth/+page.svelte @@ -1,26 +1,19 @@
@@ -50,7 +43,7 @@ bind:value={username} type="text" autocomplete="username" - class="px-4 py-2 rounded-sm !bg-[#0f1117] border border-sky-200/15 transition-colors hover:bg-black/20 hover:border-sky-200/30 text-white/80 text-sm focus:outline-none focus:border-sky-200/40" + class="px-4 py-2 rounded-sm bg-[#0f1117]! border border-sky-200/15 transition-colors hover:bg-black/20 hover:border-sky-200/30 text-white/80 text-sm focus:outline-none focus:border-sky-200/40" />
@@ -60,7 +53,7 @@ bind:value={password} type="password" autocomplete="current-password" - class="px-4 py-2 rounded-sm !bg-[#0f1117] border border-sky-200/15 transition-colors hover:bg-black/20 hover:border-sky-200/30 text-white/80 text-sm focus:outline-none focus:border-sky-200/40" + class="px-4 py-2 rounded-sm bg-[#0f1117]! border border-sky-200/15 transition-colors hover:bg-black/20 hover:border-sky-200/30 text-white/80 text-sm focus:outline-none focus:border-sky-200/40" />
{#if error}

{error}

{/if} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index c2d4e68..23d2f96 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -5,6 +5,11 @@ import { sveltekit } from '@sveltejs/kit/vite'; export default defineConfig({ plugins: [tailwindcss(), sveltekit()], + server: { + proxy: { + '/api': 'http://localhost:3000' + } + }, test: { expect: { requireAssertions: true }, projects: [