From 451f3597349c62ff5b6fdb04b0fe9e2e310e5629 Mon Sep 17 00:00:00 2001 From: marc Date: Wed, 26 Aug 2026 19:14:13 +0200 Subject: [PATCH] feat(uploads): fertige Meldungen verschwinden von selbst MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nach einem Stapel-Upload blieb unten rechts für jede Datei eine Karte stehen, bis man sie einzeln wegklickte oder die Seite neu lud. Bei vielen Dateien war der Bildschirm zu. Fertige Meldungen räumen sich jetzt nach vier Sekunden selbst ab. Fehler bleiben: Sie tragen die einzige Auskunft, die es sonst nirgends gibt. Zusätzlich stehen höchstens vier Karten gleichzeitig, der Rest wird gezählt – der Bildschirm läuft ja schon während des Hochladens voll, nicht erst danach. Gescheiterte Dateien stehen dabei oben. Co-Authored-By: Claude Opus 5 --- frontend/src/components/UploadQueue.tsx | 71 ++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/UploadQueue.tsx b/frontend/src/components/UploadQueue.tsx index b69e071..8950919 100644 --- a/frontend/src/components/UploadQueue.tsx +++ b/frontend/src/components/UploadQueue.tsx @@ -1,3 +1,5 @@ +import { useEffect, useRef } from 'react' + interface UploadItem { id: string name: string @@ -13,12 +15,68 @@ interface Props { export type { UploadItem } +/** + * Wie lange eine fertige Meldung stehen bleibt. + * + * Lang genug, um sie im Vorbeigehen zu lesen, kurz genug, dass beim Hochladen + * vieler Dateien nicht der halbe Bildschirm zuwächst. + */ +const dismissAfter = 4000 + +/** + * So viele Meldungen zeigen wir gleichzeitig, der Rest wird gezählt. + * + * Bei fünfzig Dateien auf einmal wäre die Liste sonst länger als das Fenster – + * und das gilt schon während des Hochladens, nicht erst danach. + */ +const maxVisible = 4 + export default function UploadQueue({ items, onDismiss }: Props) { + // Der Aufrufer gibt bei jedem Rendern eine neue Funktion herein. Über eine + // Referenz gelesen, muss der Effekt darauf nicht erneut laufen – sonst + // begännen die Fristen bei jedem Fortschrittsschritt von vorn. + const dismiss = useRef(onDismiss) + dismiss.current = onDismiss + + const timers = useRef(new Map>()) + + useEffect(() => { + for (const item of items) { + // Fehler bleiben stehen. Sie sind das Einzige, was hier eine Auskunft + // trägt, die man sonst nirgends wiederfindet – die verschwindet nicht + // von selbst. + if (item.status !== 'done') continue + if (timers.current.has(item.id)) continue + timers.current.set(item.id, setTimeout(() => { + timers.current.delete(item.id) + dismiss.current(item.id) + }, dismissAfter)) + } + + // Aufräumen, wenn eine Meldung von Hand weggeklickt wurde: Ihre Frist läuft + // sonst weiter und rührte später an eine Kennung, die es nicht mehr gibt. + const live = new Set(items.map(i => i.id)) + for (const [id, timer] of timers.current) { + if (!live.has(id)) { clearTimeout(timer); timers.current.delete(id) } + } + }, [items]) + + // Beim Verlassen der Seite alle Fristen abräumen. + const all = timers.current + useEffect(() => () => { for (const t of all.values()) clearTimeout(t) }, [all]) + if (items.length === 0) return null + // Fehler zuerst: Wenn mehr Meldungen anstehen als Platz ist, darf gerade die + // eine gescheiterte Datei nicht diejenige sein, die unter den Tisch fällt. + const failed = items.filter(i => i.status === 'error') + const rest = items.filter(i => i.status !== 'error') + const visible = [...failed, ...rest].slice(0, maxVisible) + const hidden = items.length - visible.length + return (
- {items.map(item => ( + {visible.map(item => (
))} + + {hidden > 0 && ( +
+ +{hidden} more +
+ )}
) }