import { useEffect, useRef } from 'react' interface UploadItem { id: string name: string progress: number // 0-100 status: 'uploading' | 'done' | 'error' error?: string } interface Props { items: UploadItem[] onDismiss: (id: string) => void } 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 (
{visible.map(item => (
{item.name} {item.status !== 'uploading' && ( )}
{item.status === 'uploading' && (
)} {item.status === 'done' && (

✓ Uploaded

)} {item.status === 'error' && (

✕ {item.error}

)}
))} {hidden > 0 && (
+{hidden} more
)}
) }