Files
fotospiel-app/resources/js/guest/pages/UploadPage.tsx
2025-09-08 14:03:43 +02:00

100 lines
4.1 KiB
TypeScript

import React from 'react';
import { Page } from './_util';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { useParams } from 'react-router-dom';
import { getDeviceId } from '../lib/device';
import { compressPhoto, formatBytes } from '../lib/image';
import { useUploadQueue } from '../queue/hooks';
import React from 'react';
type Item = { file: File; out?: File; progress: number; done?: boolean; error?: string; id?: number };
export default function UploadPage() {
const { slug } = useParams();
const [items, setItems] = React.useState<Item[]>([]);
const queue = useUploadQueue();
const [progressMap, setProgressMap] = React.useState<Record<number, number>>({});
React.useEffect(() => {
const onProg = (e: any) => {
const { id, progress } = e.detail || {};
if (typeof id === 'number') setProgressMap((m) => ({ ...m, [id]: progress }));
};
window.addEventListener('queue-progress', onProg);
return () => window.removeEventListener('queue-progress', onProg);
}, []);
async function onPick(e: React.ChangeEvent<HTMLInputElement>) {
const files = Array.from(e.target.files ?? []).slice(0, 10);
const results: Item[] = [];
for (const f of files) {
try {
const out = await compressPhoto(f, { targetBytes: 1_500_000, maxEdge: 2560, qualityStart: 0.85 });
results.push({ file: f, out, progress: 0 });
} catch (err: any) {
results.push({ file: f, progress: 0, error: err?.message || 'Komprimierung fehlgeschlagen' });
}
}
setItems(results);
}
async function startUpload() {
// Enqueue items for offline-friendly processing
for (const it of items) {
await queue.add({ slug: slug!, fileName: it.out?.name ?? it.file.name, blob: it.out ?? it.file });
}
setItems([]);
}
return (
<Page title="Foto aufnehmen/hochladen">
<Input type="file" accept="image/*" multiple capture="environment" onChange={onPick} />
<div className="h-3" />
<Button onClick={startUpload} disabled={items.length === 0}>Hochladen</Button>
<div className="mt-4 space-y-2">
{items.map((it, i) => (
<div key={i} className="rounded border p-2 text-sm">
<div className="flex items-center justify-between">
<div className="truncate">{it.file.name}</div>
<div className="text-xs text-muted-foreground">
{formatBytes(it.out?.size ?? it.file.size)}
</div>
</div>
{it.done && <div className="text-xs text-muted-foreground">Fertig</div>}
{it.error && <div className="text-xs text-red-500">{it.error}</div>}
</div>
))}
</div>
<div className="mt-6">
<div className="mb-2 text-sm font-medium">Warteschlange</div>
{queue.loading ? (
<div className="text-sm text-muted-foreground">Lade</div>
) : queue.items.length === 0 ? (
<div className="text-sm text-muted-foreground">Keine offenen Uploads.</div>
) : (
<div className="space-y-2">
{queue.items.map((q) => (
<div key={q.id} className="rounded border p-2 text-sm">
<div className="flex items-center justify-between">
<div className="truncate">{q.fileName}</div>
<div className="text-xs text-muted-foreground">{q.status}{q.status==='uploading' && typeof q.id==='number' ? `${progressMap[q.id] ?? 0}%` : ''}</div>
</div>
{q.status === 'uploading' && typeof q.id==='number' && (
<div className="mt-2 h-2 w-full rounded bg-gray-200 dark:bg-gray-700">
<div className="h-2 rounded bg-emerald-500" style={{ width: `${progressMap[q.id] ?? 0}%` }} />
</div>
)}
</div>
))}
<div className="flex gap-2">
<Button variant="secondary" onClick={queue.retryAll}>Erneut versuchen</Button>
<Button variant="secondary" onClick={queue.clearFinished}>Erledigte entfernen</Button>
</div>
</div>
)}
</div>
</Page>
);
}