Add lightbox retries and queue removal
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled

This commit is contained in:
Codex Agent
2026-02-05 17:42:44 +01:00
parent 4e0d156065
commit 5f75c7ca6a
10 changed files with 282 additions and 49 deletions

View File

@@ -16,6 +16,8 @@ import { resolveUploadErrorDialog, type UploadErrorDialog } from '@/guest/lib/up
import { fetchTasks, type TaskItem } from '../services/tasksApi';
import { pushGuestToast } from '../lib/toast';
import { getBentoSurfaceTokens } from '../lib/bento';
import { buildEventPath } from '../lib/routes';
import { compressPhoto, formatBytes } from '@/guest/lib/image';
function getTaskValue(task: TaskItem, key: string): string | undefined {
const value = task?.[key as keyof TaskItem];
@@ -78,6 +80,8 @@ export default function UploadScreen() {
const [pendingItems, setPendingItems] = React.useState<PendingUpload[]>([]);
const [pendingCount, setPendingCount] = React.useState(0);
const [pendingLoading, setPendingLoading] = React.useState(false);
const optimizeTargetBytes = 1_500_000;
const optimizeMaxEdge = 2560;
const previewQueueItems = React.useMemo(() => items.filter((item) => item.status !== 'done').slice(0, 3), [items]);
const previewQueueUrls = React.useMemo(() => {
@@ -183,6 +187,42 @@ export default function UploadScreen() {
}
}, []);
const prepareUploadFile = React.useCallback(
async (file: File) => {
const shouldOptimize = file.size > optimizeTargetBytes || file.type !== 'image/jpeg';
if (!shouldOptimize) {
return file;
}
try {
const optimized = await compressPhoto(file, { targetBytes: optimizeTargetBytes, maxEdge: optimizeMaxEdge });
if (optimized.size >= file.size) {
return file;
}
if (optimized.size < file.size - 50_000) {
const saved = formatBytes(file.size - optimized.size);
pushGuestToast({
text: t(
'upload.optimizedNotice',
{ saved },
'Wir haben dein Foto verkleinert, damit der Upload schneller klappt. Eingespart: {saved}'
),
type: 'info',
});
}
return optimized;
} catch (error) {
console.warn('Image optimization failed, uploading original', error);
pushGuestToast({
text: t('upload.optimizedFallback', 'Optimierung nicht möglich wir laden das Original hoch.'),
type: 'info',
});
return file;
}
},
[optimizeMaxEdge, optimizeTargetBytes, t]
);
const uploadFiles = React.useCallback(
async (files: File[]) => {
if (!token || files.length === 0) return;
@@ -193,17 +233,19 @@ export default function UploadScreen() {
setError(null);
setUploadDialog(null);
let redirectPhotoId: number | null = null;
for (const file of files) {
const preparedFile = await prepareUploadFile(file);
if (!navigator.onLine) {
await enqueueFile(file);
await enqueueFile(preparedFile);
pushGuestToast({ text: t('uploadV2.toast.queued', 'Offline — added to upload queue.'), type: 'info' });
continue;
}
try {
setUploading({ name: file.name, progress: 0 });
await uploadPhoto(token, file, taskId, undefined, {
const photoId = await uploadPhoto(token, preparedFile, taskId, undefined, {
guestName: identity?.name ?? undefined,
onProgress: (percent) => {
setUploading((prev) => (prev ? { ...prev, progress: percent } : prev));
@@ -218,26 +260,56 @@ export default function UploadScreen() {
}
pushGuestToast({ text: t('uploadV2.toast.uploaded', 'Upload complete.'), type: 'success' });
void loadPending();
if (autoApprove && photoId) {
redirectPhotoId = photoId;
}
} catch (err) {
const uploadErr = err as { code?: string; meta?: Record<string, unknown> };
console.error('Upload failed, enqueueing', err);
setUploadDialog(resolveUploadErrorDialog(uploadErr?.code, uploadErr?.meta, t));
await enqueueFile(file);
await enqueueFile(preparedFile);
} finally {
setUploading(null);
}
}
if (autoApprove && redirectPhotoId) {
navigate(buildEventPath(token, `/gallery?photo=${redirectPhotoId}`));
}
},
[autoApprove, enqueueFile, identity?.name, loadPending, markCompleted, t, taskId, token, triggerConfetti]
[
autoApprove,
enqueueFile,
identity?.name,
loadPending,
markCompleted,
navigate,
prepareUploadFile,
t,
taskId,
token,
triggerConfetti,
]
);
const handleFiles = React.useCallback(
async (fileList: FileList | null) => {
if (!fileList) return;
const files = Array.from(fileList).filter((file) => file.type.startsWith('image/'));
await uploadFiles(files);
const next = files[0];
if (!next) {
setError(t('uploadV2.errors.invalidFile', 'Please choose a photo file.'));
return;
}
if (previewUrl) {
URL.revokeObjectURL(previewUrl);
}
const url = URL.createObjectURL(next);
setPreviewFile(next);
setPreviewUrl(url);
setCameraState('preview');
},
[uploadFiles]
[previewUrl, t]
);
const handlePick = React.useCallback(() => {
@@ -409,8 +481,10 @@ export default function UploadScreen() {
}
setPreviewFile(null);
setPreviewUrl(null);
await startCamera();
}, [previewUrl, startCamera]);
if (cameraState === 'preview') {
await startCamera();
}
}, [cameraState, previewUrl, startCamera]);
const handleUseImage = React.useCallback(async () => {
if (!previewFile) return;
@@ -651,7 +725,7 @@ export default function UploadScreen() {
>
<X size={22} color="#FFFFFF" />
<Text fontSize="$3" fontWeight="$7" color="#FFFFFF">
{t('upload.review.retake', 'Nochmal aufnehmen')}
{t('upload.review.retakeGallery', 'Ein anderes Foto auswählen')}
</Text>
</Button>
<Button