Aufgabenkarten in der Gäste-pwa als swipe-barer Stapel umgesetzt. Sofortiges Freigeben von Foto-Uploads als Event-Einstellung implementiert.

This commit is contained in:
Codex Agent
2025-12-16 15:30:52 +01:00
parent f2473c6f6d
commit 9e4e9a0d87
19 changed files with 22590 additions and 21687 deletions

View File

@@ -0,0 +1,150 @@
import { useCallback, useState } from 'react';
import { compressPhoto, formatBytes } from '../lib/image';
import { uploadPhoto, type UploadError } from '../services/photosApi';
import { useGuestIdentity } from '../context/GuestIdentityContext';
import { useGuestTaskProgress } from '../hooks/useGuestTaskProgress';
import { resolveUploadErrorDialog, type UploadErrorDialog } from '../lib/uploadErrorDialog';
type DirectUploadResult = {
success: boolean;
photoId?: number;
warning?: string | null;
error?: string | null;
dialog?: UploadErrorDialog | null;
};
type UseDirectUploadOptions = {
eventToken: string;
taskId?: number | null;
emotionSlug?: string;
onCompleted?: (photoId: number) => void;
};
export function useDirectUpload({ eventToken, taskId, emotionSlug, onCompleted }: UseDirectUploadOptions) {
const { name } = useGuestIdentity();
const { markCompleted } = useGuestTaskProgress(eventToken);
const [uploading, setUploading] = useState(false);
const [progress, setProgress] = useState(0);
const [warning, setWarning] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [errorDialog, setErrorDialog] = useState<UploadErrorDialog | null>(null);
const [canUpload, setCanUpload] = useState(true);
const reset = useCallback(() => {
setProgress(0);
setWarning(null);
setError(null);
setErrorDialog(null);
}, []);
const preparePhoto = useCallback(async (file: File) => {
reset();
let prepared = file;
try {
prepared = await compressPhoto(file, {
maxEdge: 2400,
targetBytes: 4_000_000,
qualityStart: 0.82,
});
if (prepared.size < file.size - 50_000) {
const saved = formatBytes(file.size - prepared.size);
setWarning(`Wir haben dein Foto verkleinert, damit der Upload schneller klappt. Eingespart: ${saved}`);
}
} catch (err) {
console.warn('Direct upload: optimization failed, using original', err);
setWarning('Optimierung nicht möglich wir laden das Original hoch.');
}
if (prepared.size > 12_000_000) {
setError('Das Foto war zu groß. Bitte erneut versuchen wir verkleinern es automatisch.');
return { ok: false as const };
}
return { ok: true as const, prepared };
}, [reset]);
const upload = useCallback(
async (file: File): Promise<DirectUploadResult> => {
if (!canUpload || uploading) return { success: false, warning, error };
const preparedResult = await preparePhoto(file);
if (!preparedResult.ok) {
return { success: false, warning, error };
}
const prepared = preparedResult.prepared;
setUploading(true);
setProgress(2);
setError(null);
setErrorDialog(null);
try {
const photoId = await uploadPhoto(eventToken, prepared, taskId ?? undefined, emotionSlug || undefined, {
maxRetries: 2,
guestName: name || undefined,
onProgress: (percent) => {
setProgress(Math.max(10, Math.min(98, percent)));
},
onRetry: (attempt) => {
setWarning(`Verbindung holperig neuer Versuch (${attempt}).`);
},
});
setProgress(100);
if (taskId) {
markCompleted(taskId);
}
try {
const raw = localStorage.getItem('my-photo-ids');
const arr: number[] = raw ? JSON.parse(raw) : [];
if (photoId && !arr.includes(photoId)) {
localStorage.setItem('my-photo-ids', JSON.stringify([photoId, ...arr]));
}
} catch (persistErr) {
console.warn('Direct upload: persist my-photo-ids failed', persistErr);
}
onCompleted?.(photoId);
return { success: true, photoId, warning };
} catch (err) {
console.error('Direct upload failed', err);
const uploadErr = err as UploadError;
const meta = uploadErr.meta as Record<string, unknown> | undefined;
const dialog = resolveUploadErrorDialog(uploadErr.code, meta, (v: string) => v);
setErrorDialog(dialog);
setError(dialog?.description ?? uploadErr.message ?? 'Upload fehlgeschlagen.');
setWarning(null);
if (
uploadErr.code === 'photo_limit_exceeded'
|| uploadErr.code === 'upload_device_limit'
|| uploadErr.code === 'event_package_missing'
|| uploadErr.code === 'event_not_found'
|| uploadErr.code === 'gallery_expired'
) {
setCanUpload(false);
}
if (uploadErr.status === 422 || uploadErr.code === 'validation_error') {
setWarning('Das Foto war zu groß. Bitte erneut versuchen wir verkleinern es automatisch.');
}
return { success: false, warning, error: dialog?.description ?? uploadErr.message, dialog };
} finally {
setUploading(false);
setProgress((p) => (p === 100 ? p : 0));
}
},
[canUpload, emotionSlug, eventToken, markCompleted, name, preparePhoto, taskId, uploading, warning, onCompleted]
);
return {
upload,
uploading,
progress,
warning,
error,
errorDialog,
reset,
};
}