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:
150
resources/js/guest/hooks/useDirectUpload.ts
Normal file
150
resources/js/guest/hooks/useDirectUpload.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user