This commit is contained in:
Anaïs Saraiva
2026-08-25 12:32:54 +02:00
parent b1b4f03814
commit 1b3b45b348
12 changed files with 193 additions and 95 deletions
+10 -1
View File
@@ -4,9 +4,18 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="referrer" content="no-referrer" />
<meta name="theme-color" content="#0b0d12" />
<meta name="theme-color" content="#fafafa" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>File Drop</title>
<script>
(function () {
var t = localStorage.getItem('filehost.theme');
if (t !== 'dark' && t !== 'light') {
t = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
document.documentElement.classList.toggle('dark', t === 'dark');
})();
</script>
</head>
<body>
<div id="app"></div>
+50 -11
View File
@@ -1,11 +1,14 @@
<script>
import { onMount } from 'svelte';
import Dropzone from './components/Dropzone.svelte';
import ResultPanel from './components/ResultPanel.svelte';
import FileList from './components/FileList.svelte';
import Toast from './components/Toast.svelte';
import { uploadFile } from './lib/api.js';
import { addUpload } from './lib/store.js';
import { currentTheme, toggleTheme } from './lib/theme.js';
let theme = 'light';
let currentFile = null;
let uploading = false;
let progress = 0;
@@ -13,6 +16,10 @@
let password = '';
let toast = { message: '', kind: 'info', id: 0 };
onMount(() => {
theme = currentTheme();
});
function notify(message, kind = 'info') {
toast = { message, kind, id: Date.now() };
}
@@ -21,13 +28,17 @@
const file = e.detail;
if (!file) return;
if (file.size === 0) {
notify('Les fichiers vides ne sont pas pris en charge.', 'error');
notify('Empty files are not supported.', 'error');
return;
}
currentFile = file;
notify('', 'info');
}
function onToggleTheme() {
theme = toggleTheme();
}
async function submit() {
if (!currentFile || uploading) return;
uploading = true;
@@ -49,11 +60,11 @@
createdAt: new Date().toISOString(),
protected: !!data.protected
});
notify('Envoi terminé.', 'success');
notify('Upload complete.', 'success');
currentFile = null;
password = '';
} catch (e) {
notify(e.message || 'Échec de l\'envoi.', 'error');
notify(e.message || 'Upload failed.', 'error');
} finally {
uploading = false;
progress = 0;
@@ -64,31 +75,59 @@
<div class="mx-auto max-w-6xl px-4 pb-16 sm:px-6">
<header class="flex items-center justify-between gap-4 py-6">
<div class="flex items-center gap-3">
<img src="/logo.svg" alt="File Drop" class="h-8 w-8" />
<img src="/logo.svg" alt="File Drop" class="h-8 w-8 dark:invert" />
<div>
<h1 class="text-base font-semibold leading-tight text-ink">File Drop</h1>
<p class="text-xs text-muted">Hébergement de fichiers privé</p>
<p class="text-xs text-muted">Private file hosting</p>
</div>
</div>
<a href="/" class="btn btn-ghost px-3 py-1.5 text-xs">Nouvel envoi</a>
<div class="flex items-center gap-2">
<button
type="button"
class="btn btn-ghost p-1.5"
aria-label={theme === 'dark' ? 'Switch to light theme' : 'Switch to dark theme'}
on:click={onToggleTheme}
>
{#if theme === 'dark'}
<svg viewBox="0 0 24 24" fill="none" class="h-4 w-4" stroke="currentColor" stroke-width="1.5">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z"
/>
</svg>
{:else}
<svg viewBox="0 0 24 24" fill="none" class="h-4 w-4" stroke="currentColor" stroke-width="1.5">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z"
/>
</svg>
{/if}
</button>
<a href="/" class="btn btn-ghost px-3 py-1.5 text-xs">New upload</a>
</div>
</header>
<main class="grid grid-cols-1 items-start gap-4 lg:grid-cols-[360px_1fr]">
<aside class="card p-5 lg:sticky lg:top-6">
<h2 class="mb-4 text-sm font-semibold text-ink">Envoyer un fichier</h2>
<h2 class="mb-4 text-sm font-semibold text-ink">Upload a file</h2>
<Dropzone bind:file={currentFile} disabled={uploading} on:file={onFile} />
<div class="mt-4">
<label for="pw" class="mb-1.5 block text-xs font-medium text-muted">
Mot de passe <span class="font-normal opacity-70">(optionnel)</span>
Password <span class="font-normal opacity-70">(optional)</span>
</label>
<input
id="pw"
type="password"
class="field"
bind:value={password}
placeholder="Protéger le téléchargement"
placeholder="Protect the download"
autocomplete="new-password"
on:keydown={(e) => {
if (e.key === 'Enter') submit();
@@ -99,7 +138,7 @@
{#if uploading}
<div class="mt-4">
<div class="mb-1.5 flex items-center justify-between gap-2 text-xs text-muted">
<span class="truncate">{currentFile ? currentFile.name : 'Envoi…'}</span>
<span class="truncate">{currentFile ? currentFile.name : 'Uploading…'}</span>
<span class="tabular-nums">{progress}%</span>
</div>
<div class="h-1 w-full overflow-hidden rounded-full bg-raised">
@@ -114,7 +153,7 @@
disabled={!currentFile || uploading}
on:click={submit}
>
{uploading ? 'Envoi…' : 'Envoyer'}
{uploading ? 'Uploading…' : 'Upload'}
</button>
<div class="mt-4">
+32 -2
View File
@@ -2,6 +2,32 @@
@tailwind components;
@tailwind utilities;
:root {
--c-base: 250 250 250;
--c-surface: 255 255 255;
--c-raised: 245 245 245;
--c-edge: 229 229 229;
--c-ink: 23 23 23;
--c-muted: 115 115 115;
--c-accent: 23 23 23;
--c-accent-strong: 10 10 10;
--c-danger: 220 38 38;
--c-ok: 22 163 74;
}
.dark {
--c-base: 10 10 10;
--c-surface: 23 23 23;
--c-raised: 38 38 38;
--c-edge: 64 64 64;
--c-ink: 250 250 250;
--c-muted: 163 163 163;
--c-accent: 250 250 250;
--c-accent-strong: 229 229 229;
--c-danger: 248 113 113;
--c-ok: 74 222 128;
}
@layer base {
html {
-webkit-font-smoothing: antialiased;
@@ -14,7 +40,11 @@
}
::selection {
background: rgba(23, 23, 23, 0.08);
background: rgba(0, 0, 0, 0.12);
}
.dark ::selection {
background: rgba(255, 255, 255, 0.2);
}
summary {
@@ -37,7 +67,7 @@
}
.btn-primary {
@apply bg-accent text-white hover:bg-accent-strong;
@apply bg-accent text-surface hover:bg-accent-strong;
}
.btn-ghost {
+3 -3
View File
@@ -83,9 +83,9 @@
{#if file}
<p class="max-w-full truncate text-sm font-medium text-ink">{file.name}</p>
<p class="mt-1 text-xs text-muted">Déposez un autre fichier pour le remplacer</p>
<p class="mt-1 text-xs text-muted">Drop another file to replace it</p>
{:else}
<p class="text-sm font-medium text-ink">Glissez-déposez un fichier ici</p>
<p class="mt-1 text-xs text-muted">ou cliquez pour parcourir</p>
<p class="text-sm font-medium text-ink">Drag &amp; drop a file here</p>
<p class="mt-1 text-xs text-muted">or click to browse</p>
{/if}
</div>
+8 -8
View File
@@ -8,22 +8,22 @@
async function onDelete(item) {
const ok = window.confirm(
`Supprimer définitivement « ${item.originalName || item.downloadName} » ?`
`Delete "${item.originalName || item.downloadName}" permanently? This cannot be undone.`
);
if (!ok) return;
try {
await deleteFile(item);
removeUpload(item.id);
dispatch('notify', { message: 'Fichier supprimé.', kind: 'success' });
dispatch('notify', { message: 'File deleted.', kind: 'success' });
} catch (e) {
dispatch('notify', { message: e.message || 'Suppression impossible.', kind: 'error' });
dispatch('notify', { message: e.message || 'Deletion failed.', kind: 'error' });
}
}
function onClear() {
if ($uploads.length === 0) return;
const ok = window.confirm(
'Vider cette liste ? Seules les entrées de CE navigateur sont supprimées. Les fichiers restent sur le serveur.'
'Clear this list? This only removes the entries from THIS browser. Files on the server are not touched.'
);
if (!ok) return;
clearUploads();
@@ -33,14 +33,14 @@
<section class="card p-5">
<div class="mb-4 flex items-center justify-between gap-3">
<div class="flex items-baseline gap-2">
<h2 class="text-sm font-semibold text-ink">Fichiers</h2>
<h2 class="text-sm font-semibold text-ink">Files</h2>
{#if $uploads.length}
<span class="text-xs text-muted">{$uploads.length}</span>
{/if}
</div>
{#if $uploads.length}
<button type="button" class="text-xs font-medium text-muted transition-colors hover:text-danger" on:click={onClear}>
Vider la liste
Clear list
</button>
{/if}
</div>
@@ -62,8 +62,8 @@
/>
</svg>
</div>
<p class="text-sm text-muted">Aucun fichier pour l'instant.</p>
<p class="mt-1 text-xs text-muted">Envoyez votre premier fichier pour le voir ici.</p>
<p class="text-sm text-muted">No files yet.</p>
<p class="mt-1 text-xs text-muted">Upload your first file to see it here.</p>
</div>
{/if}
</section>
+4 -4
View File
@@ -50,18 +50,18 @@
<div class="flex flex-col gap-2 md:items-end">
<div class="flex flex-wrap gap-2">
<a href={forcedDownloadUrl(item)} class="btn btn-primary px-3 py-1.5 text-xs">Télécharger</a>
<a href={forcedDownloadUrl(item)} class="btn btn-primary px-3 py-1.5 text-xs">Download</a>
<button type="button" class="btn btn-ghost px-3 py-1.5 text-xs" on:click={copy}>
{copied ? 'Copié' : 'Copier'}
{copied ? 'Copied' : 'Copy'}
</button>
<button type="button" class="btn btn-danger px-3 py-1.5 text-xs" on:click={() => dispatch('delete', item)}>
Supprimer
Delete
</button>
</div>
<details>
<summary class="cursor-pointer select-none text-xs font-medium text-muted transition-colors hover:text-ink">
Liens
Links
</summary>
<div class="mt-2 md:flex md:justify-end">
<LinkFormats {item} />
+1 -1
View File
@@ -36,7 +36,7 @@
</div>
<div class="mt-3">
<p class="mb-2 text-[11px] font-medium uppercase tracking-wide text-muted">Partager</p>
<p class="mb-2 text-[11px] font-medium uppercase tracking-wide text-muted">Share links</p>
<LinkFormats item={result} size="md" />
</div>
</div>
+20
View File
@@ -0,0 +1,20 @@
const KEY = 'filehost.theme';
export function currentTheme() {
return document.documentElement.classList.contains('dark') ? 'dark' : 'light';
}
export function applyTheme(theme) {
document.documentElement.classList.toggle('dark', theme === 'dark');
try {
localStorage.setItem(KEY, theme);
} catch {
/* ignore */
}
}
export function toggleTheme() {
const next = currentTheme() === 'dark' ? 'light' : 'dark';
applyTheme(next);
return next;
}
+11 -10
View File
@@ -1,19 +1,20 @@
/** @type {import('tailwindcss').Config} */
export default {
darkMode: 'class',
content: ['./index.html', './src/**/*.{svelte,js}'],
theme: {
extend: {
colors: {
base: '#fafafa',
surface: '#ffffff',
raised: '#f5f5f5',
edge: '#e5e5e5',
ink: '#171717',
muted: '#737373',
accent: '#171717',
'accent-strong': '#0a0a0a',
danger: '#dc2626',
ok: '#16a34a'
base: 'rgb(var(--c-base) / <alpha-value>)',
surface: 'rgb(var(--c-surface) / <alpha-value>)',
raised: 'rgb(var(--c-raised) / <alpha-value>)',
edge: 'rgb(var(--c-edge) / <alpha-value>)',
ink: 'rgb(var(--c-ink) / <alpha-value>)',
muted: 'rgb(var(--c-muted) / <alpha-value>)',
accent: 'rgb(var(--c-accent) / <alpha-value>)',
'accent-strong': 'rgb(var(--c-accent-strong) / <alpha-value>)',
danger: 'rgb(var(--c-danger) / <alpha-value>)',
ok: 'rgb(var(--c-ok) / <alpha-value>)'
},
fontFamily: {
sans: ['system-ui', '-apple-system', 'Segoe UI', 'Roboto', 'sans-serif']