Add lightbox retries and queue removal
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user