Initialize repo and add session changes (2025-09-08)
This commit is contained in:
99
resources/js/guest/pages/UploadPage.tsx
Normal file
99
resources/js/guest/pages/UploadPage.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user