feat: extend event toolkit and polish guest pwa
This commit is contained in:
@@ -54,7 +54,7 @@ export default function BottomNav() {
|
||||
const isGalleryActive = currentPath.startsWith(`${base}/gallery`) || currentPath.startsWith(`${base}/photos`);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-x-0 bottom-0 z-30 border-t border-white/20 bg-black/30 px-2 py-2 backdrop-blur-xl shadow-xl dark:bg-black/40 dark:border-gray-800/50">
|
||||
<div className="fixed inset-x-0 bottom-0 z-30 border-t border-white/30 bg-white/70 px-2 py-2 shadow-xl backdrop-blur-xl dark:border-white/10 dark:bg-gradient-to-t dark:from-gray-950/85 dark:via-gray-900/70 dark:to-gray-900/35 dark:backdrop-blur-2xl dark:shadow-[0_18px_60px_rgba(15,15,30,0.6)]">
|
||||
<div className="mx-auto flex max-w-sm items-center justify-around">
|
||||
<TabLink to={`${base}`} isActive={isHomeActive}>
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
|
||||
@@ -16,8 +16,8 @@ export const messages: Record<LocaleCode, NestedMessages> = {
|
||||
common: {
|
||||
hi: 'Hi',
|
||||
actions: {
|
||||
close: 'Schliessen',
|
||||
loading: 'Laedt...',
|
||||
close: 'Schließen',
|
||||
loading: 'Lädt...',
|
||||
},
|
||||
},
|
||||
navigation: {
|
||||
@@ -30,34 +30,34 @@ export const messages: Record<LocaleCode, NestedMessages> = {
|
||||
loading: 'Lade Event...',
|
||||
stats: {
|
||||
online: 'online',
|
||||
tasksSolved: 'Aufgaben geloest',
|
||||
tasksSolved: 'Aufgaben gelöst',
|
||||
},
|
||||
},
|
||||
eventAccess: {
|
||||
loading: {
|
||||
title: 'Wir pruefen deinen Zugang...',
|
||||
title: 'Wir prüfen deinen Zugang...',
|
||||
subtitle: 'Einen Moment bitte.',
|
||||
},
|
||||
error: {
|
||||
invalid_token: {
|
||||
title: 'Zugriffscode ungueltig',
|
||||
title: 'Zugriffscode ungültig',
|
||||
description: 'Der eingegebene Code konnte nicht verifiziert werden.',
|
||||
ctaLabel: 'Neuen Code anfordern',
|
||||
},
|
||||
token_revoked: {
|
||||
title: 'Zugriffscode deaktiviert',
|
||||
description: 'Dieser Code wurde zurueckgezogen. Bitte fordere einen neuen Code an.',
|
||||
description: 'Dieser Code wurde zurückgezogen. Bitte fordere einen neuen Code an.',
|
||||
ctaLabel: 'Neuen Code anfordern',
|
||||
},
|
||||
token_expired: {
|
||||
title: 'Zugriffscode abgelaufen',
|
||||
description: 'Der Code ist nicht mehr gueltig. Aktualisiere deinen Code, um fortzufahren.',
|
||||
description: 'Der Code ist nicht mehr gültig. Aktualisiere deinen Code, um fortzufahren.',
|
||||
ctaLabel: 'Code aktualisieren',
|
||||
},
|
||||
token_rate_limited: {
|
||||
title: 'Zu viele Versuche',
|
||||
description: 'Es gab zu viele Eingaben in kurzer Zeit. Warte kurz und versuche es erneut.',
|
||||
hint: 'Tipp: Eine erneute Eingabe ist in wenigen Minuten moeglich.',
|
||||
hint: 'Tipp: Eine erneute Eingabe ist in wenigen Minuten möglich.',
|
||||
},
|
||||
access_rate_limited: {
|
||||
title: 'Zu viele Aufrufe',
|
||||
@@ -65,22 +65,22 @@ export const messages: Record<LocaleCode, NestedMessages> = {
|
||||
hint: 'Tipp: Du kannst es gleich noch einmal versuchen.',
|
||||
},
|
||||
gallery_expired: {
|
||||
title: 'Galerie nicht mehr verfuegbar',
|
||||
description: 'Die Galerie zu diesem Event ist nicht mehr zugaenglich.',
|
||||
title: 'Galerie nicht mehr verfügbar',
|
||||
description: 'Die Galerie zu diesem Event ist nicht mehr zugänglich.',
|
||||
ctaLabel: 'Neuen Code anfordern',
|
||||
},
|
||||
event_not_public: {
|
||||
title: 'Event nicht oeffentlich',
|
||||
description: 'Dieses Event ist aktuell nicht oeffentlich zugaenglich.',
|
||||
title: 'Event nicht öffentlich',
|
||||
description: 'Dieses Event ist aktuell nicht öffentlich zugänglich.',
|
||||
hint: 'Nimm Kontakt mit den Veranstalter:innen auf, um Zugang zu erhalten.',
|
||||
},
|
||||
network_error: {
|
||||
title: 'Verbindungsproblem',
|
||||
description: 'Wir konnten keine Verbindung zum Server herstellen. Pruefe deine Internetverbindung und versuche es erneut.',
|
||||
description: 'Wir konnten keine Verbindung zum Server herstellen. Prüfe deine Internetverbindung und versuche es erneut.',
|
||||
},
|
||||
server_error: {
|
||||
title: 'Server nicht erreichbar',
|
||||
description: 'Der Server reagiert derzeit nicht. Versuche es spaeter erneut.',
|
||||
description: 'Der Server reagiert derzeit nicht. Versuche es später erneut.',
|
||||
},
|
||||
default: {
|
||||
title: 'Event nicht erreichbar',
|
||||
@@ -93,10 +93,10 @@ export const messages: Record<LocaleCode, NestedMessages> = {
|
||||
loading: 'Lade Event...',
|
||||
error: {
|
||||
default: 'Event nicht gefunden.',
|
||||
backToStart: 'Zurueck zur Startseite',
|
||||
backToStart: 'Zurück zur Startseite',
|
||||
},
|
||||
card: {
|
||||
description: 'Fange den schoensten Moment ein!',
|
||||
description: 'Fange den schönsten Moment ein!',
|
||||
},
|
||||
form: {
|
||||
label: 'Dein Name (z.B. Anna)',
|
||||
@@ -108,12 +108,12 @@ export const messages: Record<LocaleCode, NestedMessages> = {
|
||||
landing: {
|
||||
pageTitle: 'Willkommen bei der Fotobox!',
|
||||
headline: 'Willkommen bei der Fotobox!',
|
||||
subheadline: 'Dein Schluessel zu unvergesslichen Momenten.',
|
||||
subheadline: 'Dein Schlüssel zu unvergesslichen Momenten.',
|
||||
join: {
|
||||
title: 'Event beitreten',
|
||||
description: 'Scanne den QR-Code oder gib den Code manuell ein.',
|
||||
button: 'Event beitreten',
|
||||
buttonLoading: 'Pruefe...',
|
||||
buttonLoading: 'Prüfe...',
|
||||
},
|
||||
scan: {
|
||||
start: 'QR-Code scannen',
|
||||
@@ -125,7 +125,7 @@ export const messages: Record<LocaleCode, NestedMessages> = {
|
||||
},
|
||||
errors: {
|
||||
eventClosed: 'Event nicht gefunden oder geschlossen.',
|
||||
network: 'Netzwerkfehler. Bitte spaeter erneut versuchen.',
|
||||
network: 'Netzwerkfehler. Bitte später erneut versuchen.',
|
||||
camera: 'Kamera-Zugriff fehlgeschlagen. Bitte erlaube den Zugriff und versuche es erneut.',
|
||||
},
|
||||
},
|
||||
@@ -134,27 +134,27 @@ export const messages: Record<LocaleCode, NestedMessages> = {
|
||||
hero: {
|
||||
subtitle: 'Willkommen zur Party',
|
||||
title: 'Hey {name}!',
|
||||
description: 'Du bist bereit fuer "{eventName}". Fang die Highlights des Events ein und teile sie mit allen Gaesten.',
|
||||
description: 'Du bist bereit für "{eventName}". Fang die Highlights des Events ein und teile sie mit allen Gästen.',
|
||||
progress: {
|
||||
some: 'Schon {count} Aufgaben erledigt - weiter so!',
|
||||
none: 'Starte mit deiner ersten Aufgabe - wir zaehlen auf dich!',
|
||||
some: 'Schon {count} Aufgaben erledigt – weiter so!',
|
||||
none: 'Starte mit deiner ersten Aufgabe – wir zählen auf dich!',
|
||||
},
|
||||
defaultEventName: 'Dein Event',
|
||||
},
|
||||
stats: {
|
||||
online: 'Gleichzeitig online',
|
||||
tasksSolved: 'Aufgaben geloest',
|
||||
tasksSolved: 'Aufgaben gelöst',
|
||||
lastUpload: 'Letzter Upload',
|
||||
completedTasks: 'Deine erledigten Aufgaben',
|
||||
},
|
||||
actions: {
|
||||
title: 'Deine Aktionen',
|
||||
subtitle: 'Waehle aus, womit du starten willst',
|
||||
subtitle: 'Wähle aus, womit du starten willst',
|
||||
queueButton: 'Uploads in Warteschlange ansehen',
|
||||
items: {
|
||||
tasks: {
|
||||
label: 'Aufgabe ziehen',
|
||||
description: 'Hol dir deine naechste Challenge',
|
||||
description: 'Hol dir deine nächste Challenge',
|
||||
},
|
||||
upload: {
|
||||
label: 'Direkt hochladen',
|
||||
@@ -168,10 +168,10 @@ export const messages: Record<LocaleCode, NestedMessages> = {
|
||||
},
|
||||
checklist: {
|
||||
title: 'Dein Fortschritt',
|
||||
description: 'Halte dich an diese drei kurzen Schritte fuer die besten Ergebnisse.',
|
||||
description: 'Halte dich an diese drei kurzen Schritte für die besten Ergebnisse.',
|
||||
steps: {
|
||||
first: 'Aufgabe auswaehlen oder starten',
|
||||
second: 'Emotion festhalten und Foto schiessen',
|
||||
first: 'Aufgabe auswählen oder starten',
|
||||
second: 'Emotion festhalten und Foto schießen',
|
||||
third: 'Bild hochladen und Credits sammeln',
|
||||
},
|
||||
},
|
||||
@@ -225,35 +225,35 @@ export const messages: Record<LocaleCode, NestedMessages> = {
|
||||
retry: 'Nochmal versuchen',
|
||||
},
|
||||
primer: {
|
||||
title: 'Bereit fuer dein Shooting?',
|
||||
title: 'Bereit für dein Shooting?',
|
||||
body: {
|
||||
part1: 'Lass uns sicherstellen, dass alles sitzt: pruefe das Licht, wisch die Kamera sauber und richte alle Personen im Bild aus.',
|
||||
part2: 'Du kannst zwischen Front- und Rueckkamera wechseln und bei Bedarf ein Raster aktivieren.',
|
||||
part1: 'Lass uns sicherstellen, dass alles sitzt: prüfe das Licht, wisch die Kamera sauber und richte alle Personen im Bild aus.',
|
||||
part2: 'Du kannst zwischen Front- und Rückkamera wechseln und bei Bedarf ein Raster aktivieren.',
|
||||
},
|
||||
dismiss: 'Verstanden',
|
||||
},
|
||||
cameraUnsupported: {
|
||||
title: 'Kamera nicht verfuegbar',
|
||||
message: 'Dein Geraet unterstuetzt keine Kameravorschau im Browser. Du kannst stattdessen Fotos aus deiner Galerie hochladen.',
|
||||
openGallery: 'Foto aus Galerie waehlen',
|
||||
title: 'Kamera nicht verfügbar',
|
||||
message: 'Dein Gerät unterstützt keine Kameravorschau im Browser. Du kannst stattdessen Fotos aus deiner Galerie hochladen.',
|
||||
openGallery: 'Foto aus Galerie wählen',
|
||||
},
|
||||
cameraDenied: {
|
||||
title: 'Kamera-Zugriff verweigert',
|
||||
explanation: 'Du musst den Zugriff auf die Kamera erlauben, um Fotos aufnehmen zu koennen.',
|
||||
reopenPrompt: 'Systemdialog erneut oeffnen',
|
||||
chooseFile: 'Foto aus Galerie waehlen',
|
||||
prompt: 'Wir brauchen Zugriff auf deine Kamera. Erlaube die Anfrage oder waehle alternativ ein Foto aus deiner Galerie.',
|
||||
explanation: 'Du musst den Zugriff auf die Kamera erlauben, um Fotos aufnehmen zu können.',
|
||||
reopenPrompt: 'Systemdialog erneut öffnen',
|
||||
chooseFile: 'Foto aus Galerie wählen',
|
||||
prompt: 'Wir brauchen Zugriff auf deine Kamera. Erlaube die Anfrage oder wähle alternativ ein Foto aus deiner Galerie.',
|
||||
},
|
||||
cameraError: {
|
||||
title: 'Kamera konnte nicht gestartet werden',
|
||||
explanation: 'Wir konnten keine Verbindung zur Kamera herstellen. Pruefe die Berechtigungen oder starte dein Geraet neu.',
|
||||
explanation: 'Wir konnten keine Verbindung zur Kamera herstellen. Prüfe die Berechtigungen oder starte dein Gerät neu.',
|
||||
tryAgain: 'Nochmals versuchen',
|
||||
},
|
||||
readyOverlay: {
|
||||
title: 'Kamera bereit',
|
||||
message: 'Wenn alle im Bild sind, kannst du den Countdown starten oder ein vorhandenes Foto waehlen.',
|
||||
message: 'Wenn alle im Bild sind, kannst du den Countdown starten oder ein vorhandenes Foto wählen.',
|
||||
start: 'Countdown starten',
|
||||
chooseFile: 'Foto auswaehlen',
|
||||
chooseFile: 'Foto auswählen',
|
||||
},
|
||||
taskInfo: {
|
||||
countdown: 'Countdown',
|
||||
@@ -266,7 +266,7 @@ export const messages: Record<LocaleCode, NestedMessages> = {
|
||||
},
|
||||
timeEstimate: '{count} Min',
|
||||
fallbackTitle: 'Aufgabe {id}',
|
||||
fallbackDescription: 'Halte den Moment fest und teile ihn mit allen Gaesten.',
|
||||
fallbackDescription: 'Halte den Moment fest und teile ihn mit allen Gästen.',
|
||||
fallbackInstructions: 'Positioniere alle im Bild, starte den Countdown und lass die Emotion wirken.',
|
||||
badge: 'Aufgabe #{id}',
|
||||
},
|
||||
@@ -276,7 +276,7 @@ export const messages: Record<LocaleCode, NestedMessages> = {
|
||||
review: {
|
||||
retake: 'Nochmal aufnehmen',
|
||||
keep: 'Foto verwenden',
|
||||
readyAnnouncement: 'Foto aufgenommen. Bitte Vorschau pruefen.',
|
||||
readyAnnouncement: 'Foto aufgenommen. Bitte Vorschau prüfen.',
|
||||
},
|
||||
status: {
|
||||
saving: 'Speichere Foto...',
|
||||
@@ -289,23 +289,23 @@ export const messages: Record<LocaleCode, NestedMessages> = {
|
||||
controls: {
|
||||
toggleGrid: 'Raster umschalten',
|
||||
toggleCountdown: 'Countdown umschalten',
|
||||
toggleMirror: 'Spiegelung fuer Frontkamera umschalten',
|
||||
toggleFlash: 'Blitzpraeferenz umschalten',
|
||||
toggleMirror: 'Spiegelung für Frontkamera umschalten',
|
||||
toggleFlash: 'Blitzpräferenz umschalten',
|
||||
capture: 'Foto aufnehmen',
|
||||
switchCamera: 'Kamera wechseln',
|
||||
chooseFile: 'Foto auswaehlen',
|
||||
chooseFile: 'Foto auswählen',
|
||||
},
|
||||
limitReached: 'Upload-Limit erreicht ({used} / {max} Fotos). Bitte kontaktiere die Veranstalter fuer ein Upgrade.',
|
||||
limitReached: 'Upload-Limit erreicht ({used} / {max} Fotos). Bitte kontaktiere die Veranstalter für ein Upgrade.',
|
||||
limitUnlimited: 'unbegrenzt',
|
||||
cameraInactive: 'Kamera ist nicht aktiv. {hint}',
|
||||
cameraInactiveHint: 'Tippe auf "{label}", um loszulegen.',
|
||||
captureError: 'Foto konnte nicht erstellt werden.',
|
||||
feedError: 'Kamera liefert kein Bild. Bitte starte die Kamera neu.',
|
||||
canvasError: 'Canvas konnte nicht initialisiert werden.',
|
||||
limitCheckError: 'Fehler beim Pruefen des Upload-Limits. Upload deaktiviert.',
|
||||
limitCheckError: 'Fehler beim Prüfen des Upload-Limits. Upload deaktiviert.',
|
||||
galleryPickError: 'Auswahl fehlgeschlagen. Bitte versuche es erneut.',
|
||||
captureButton: 'Foto aufnehmen',
|
||||
galleryButton: 'Foto aus Galerie waehlen',
|
||||
galleryButton: 'Foto aus Galerie wählen',
|
||||
switchCamera: 'Kamera wechseln',
|
||||
countdownLabel: 'Countdown: {seconds}s',
|
||||
countdownReady: 'Bereit machen ...',
|
||||
@@ -319,7 +319,7 @@ export const messages: Record<LocaleCode, NestedMessages> = {
|
||||
subtitle: 'Verwalte deinen Gastzugang, rechtliche Dokumente und lokale Daten.',
|
||||
language: {
|
||||
title: 'Sprache',
|
||||
description: 'Waehle deine bevorzugte Sprache fuer diese Veranstaltung.',
|
||||
description: 'Wähle deine bevorzugte Sprache für diese Veranstaltung.',
|
||||
activeBadge: 'aktiv',
|
||||
option: {
|
||||
de: 'Deutsch',
|
||||
@@ -328,12 +328,12 @@ export const messages: Record<LocaleCode, NestedMessages> = {
|
||||
},
|
||||
name: {
|
||||
title: 'Dein Name',
|
||||
description: 'Passe an, wie wir dich im Event begruessen. Der Name wird nur lokal gespeichert.',
|
||||
description: 'Passe an, wie wir dich im Event begrüßen. Der Name wird nur lokal gespeichert.',
|
||||
label: 'Anzeigename',
|
||||
placeholder: 'z.B. Anna',
|
||||
save: 'Name speichern',
|
||||
saving: 'Speichere...',
|
||||
reset: 'Zuruecksetzen',
|
||||
reset: 'Zurücksetzen',
|
||||
saved: 'Gespeichert (ok)',
|
||||
loading: 'Lade gespeicherten Namen...',
|
||||
},
|
||||
@@ -341,7 +341,7 @@ export const messages: Record<LocaleCode, NestedMessages> = {
|
||||
title: 'Rechtliches',
|
||||
description: 'Die rechtlich verbindlichen Texte sind jederzeit hier abrufbar.',
|
||||
loading: 'Dokument wird geladen...',
|
||||
error: 'Das Dokument konnte nicht geladen werden. Bitte versuche es spaeter erneut.',
|
||||
error: 'Das Dokument konnte nicht geladen werden. Bitte versuche es später erneut.',
|
||||
fallbackTitle: 'Rechtlicher Hinweis',
|
||||
section: {
|
||||
impressum: 'Impressum',
|
||||
@@ -350,19 +350,19 @@ export const messages: Record<LocaleCode, NestedMessages> = {
|
||||
},
|
||||
},
|
||||
cache: {
|
||||
title: 'Offline Cache',
|
||||
description: 'Loesche lokale Daten, falls Inhalte veraltet erscheinen oder Uploads haengen bleiben.',
|
||||
title: 'Offline-Cache',
|
||||
description: 'Lösche lokale Daten, falls Inhalte veraltet erscheinen oder Uploads hängen bleiben.',
|
||||
clear: 'Cache leeren',
|
||||
clearing: 'Leere Cache...',
|
||||
cleared: 'Cache geloescht.',
|
||||
note: 'Dies betrifft nur diesen Browser und muss pro Geraet erneut ausgefuehrt werden.',
|
||||
cleared: 'Cache gelöscht.',
|
||||
note: 'Dies betrifft nur diesen Browser und muss pro Gerät erneut ausgeführt werden.',
|
||||
},
|
||||
footer: {
|
||||
notice: 'Gastbereich - Daten werden lokal im Browser gespeichert.',
|
||||
},
|
||||
sheet: {
|
||||
openLabel: 'Einstellungen oeffnen',
|
||||
backLabel: 'Zurueck',
|
||||
openLabel: 'Einstellungen öffnen',
|
||||
backLabel: 'Zurück',
|
||||
legalDescription: 'Rechtlicher Hinweis',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
import Header from '../components/Header';
|
||||
import BottomNav from '../components/BottomNav';
|
||||
@@ -7,7 +7,6 @@ import { Badge } from '@/components/ui/badge';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { uploadPhoto } from '../services/photosApi';
|
||||
import { useGuestTaskProgress } from '../hooks/useGuestTaskProgress';
|
||||
import { useAppearance } from '../../hooks/use-appearance';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
AlertTriangle,
|
||||
@@ -46,6 +45,39 @@ type CameraPreferences = {
|
||||
flashPreferred: boolean;
|
||||
};
|
||||
|
||||
type TaskPayload = Partial<Task> & { id: number };
|
||||
|
||||
function isTaskPayload(value: unknown): value is TaskPayload {
|
||||
if (typeof value !== 'object' || value === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const candidate = value as { id?: unknown };
|
||||
return typeof candidate.id === 'number';
|
||||
}
|
||||
|
||||
function getErrorName(error: unknown): string | undefined {
|
||||
if (typeof error === 'object' && error !== null && 'name' in error) {
|
||||
const name = (error as { name?: unknown }).name;
|
||||
return typeof name === 'string' ? name : undefined;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getErrorMessage(error: unknown): string | undefined {
|
||||
if (error instanceof Error && typeof error.message === 'string') {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
if (typeof error === 'object' && error !== null && 'message' in error) {
|
||||
const message = (error as { message?: unknown }).message;
|
||||
return typeof message === 'string' ? message : undefined;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const DEFAULT_PREFS: CameraPreferences = {
|
||||
facingMode: 'environment',
|
||||
countdownSeconds: 3,
|
||||
@@ -60,8 +92,6 @@ export default function UploadPage() {
|
||||
const eventKey = token ?? '';
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { appearance } = useAppearance();
|
||||
const isDarkMode = appearance === 'dark';
|
||||
const { markCompleted } = useGuestTaskProgress(token);
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -75,7 +105,6 @@ export default function UploadPage() {
|
||||
|
||||
const [task, setTask] = useState<Task | null>(null);
|
||||
const [loadingTask, setLoadingTask] = useState(true);
|
||||
const [taskError, setTaskError] = useState<string | null>(null);
|
||||
|
||||
const [permissionState, setPermissionState] = useState<PermissionState>('idle');
|
||||
const [permissionMessage, setPermissionMessage] = useState<string | null>(null);
|
||||
@@ -138,8 +167,7 @@ export default function UploadPage() {
|
||||
|
||||
// Load task metadata
|
||||
useEffect(() => {
|
||||
if (!token || !taskId) {
|
||||
setTaskError(t('upload.loadError.title'));
|
||||
if (!token || taskId === null) {
|
||||
setLoadingTask(false);
|
||||
return;
|
||||
}
|
||||
@@ -147,18 +175,19 @@ export default function UploadPage() {
|
||||
let active = true;
|
||||
|
||||
async function loadTask() {
|
||||
const fallbackTitle = t('upload.taskInfo.fallbackTitle').replace('{id}', `${taskId!}`);
|
||||
const currentTaskId = taskId;
|
||||
const fallbackTitle = t('upload.taskInfo.fallbackTitle').replace('{id}', `${currentTaskId}`);
|
||||
const fallbackDescription = t('upload.taskInfo.fallbackDescription');
|
||||
const fallbackInstructions = t('upload.taskInfo.fallbackInstructions');
|
||||
|
||||
try {
|
||||
setLoadingTask(true);
|
||||
setTaskError(null);
|
||||
|
||||
const res = await fetch(`/api/v1/events/${encodeURIComponent(eventKey)}/tasks`);
|
||||
if (!res.ok) throw new Error('Tasks konnten nicht geladen werden');
|
||||
const tasks = await res.json();
|
||||
const found = Array.isArray(tasks) ? tasks.find((entry: any) => entry.id === taskId!) : null;
|
||||
const payload = (await res.json()) as unknown;
|
||||
const entries = Array.isArray(payload) ? payload.filter(isTaskPayload) : [];
|
||||
const found = entries.find((entry) => entry.id === currentTaskId) ?? null;
|
||||
|
||||
if (!active) return;
|
||||
|
||||
@@ -174,7 +203,7 @@ export default function UploadPage() {
|
||||
});
|
||||
} else {
|
||||
setTask({
|
||||
id: taskId!,
|
||||
id: currentTaskId,
|
||||
title: fallbackTitle,
|
||||
description: fallbackDescription,
|
||||
instructions: fallbackInstructions,
|
||||
@@ -188,9 +217,8 @@ export default function UploadPage() {
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch task', error);
|
||||
if (active) {
|
||||
setTaskError(t('upload.loadError.title'));
|
||||
setTask({
|
||||
id: taskId!,
|
||||
id: currentTaskId,
|
||||
title: fallbackTitle,
|
||||
description: fallbackDescription,
|
||||
instructions: fallbackInstructions,
|
||||
@@ -210,7 +238,7 @@ export default function UploadPage() {
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [eventKey, taskId, emotionSlug, t]);
|
||||
}, [eventKey, taskId, emotionSlug, t, token]);
|
||||
|
||||
// Check upload limits
|
||||
useEffect(() => {
|
||||
@@ -294,14 +322,15 @@ export default function UploadPage() {
|
||||
streamRef.current = stream;
|
||||
attachStreamToVideo(stream);
|
||||
setPermissionState('granted');
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
console.error('Camera access error', error);
|
||||
stopStream();
|
||||
|
||||
if (error?.name === 'NotAllowedError') {
|
||||
const errorName = getErrorName(error);
|
||||
if (errorName === 'NotAllowedError') {
|
||||
setPermissionState('denied');
|
||||
setPermissionMessage(t('upload.cameraDenied.explanation'));
|
||||
} else if (error?.name === 'NotFoundError') {
|
||||
} else if (errorName === 'NotFoundError') {
|
||||
setPermissionState('error');
|
||||
setPermissionMessage(t('upload.cameraUnsupported.message'));
|
||||
} else {
|
||||
@@ -489,9 +518,9 @@ export default function UploadPage() {
|
||||
markCompleted(task.id);
|
||||
stopStream();
|
||||
navigateAfterUpload(photoId);
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
console.error('Upload failed', error);
|
||||
setUploadError(error?.message || t('upload.status.failed'));
|
||||
setUploadError(getErrorMessage(error) || t('upload.status.failed'));
|
||||
setMode('review');
|
||||
} finally {
|
||||
if (uploadProgressTimerRef.current) {
|
||||
@@ -533,7 +562,6 @@ export default function UploadPage() {
|
||||
const isCameraActive = permissionState === 'granted' && mode !== 'uploading';
|
||||
const showTaskOverlay = task && mode !== 'uploading';
|
||||
|
||||
const isUploadDisabled = !canUpload || !task;
|
||||
|
||||
useEffect(() => () => {
|
||||
resetCountdownTimer();
|
||||
@@ -542,49 +570,41 @@ export default function UploadPage() {
|
||||
}
|
||||
}, [resetCountdownTimer]);
|
||||
|
||||
const renderPage = (content: ReactNode, mainClassName = 'px-4 py-6') => (
|
||||
<div className="pb-16">
|
||||
<Header eventToken={eventKey} title={t('upload.cameraTitle')} />
|
||||
<main className={mainClassName}>{content}</main>
|
||||
<BottomNav />
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!supportsCamera && !task) {
|
||||
return (
|
||||
<div className="pb-16">
|
||||
<Header eventToken={eventKey} title={t('upload.cameraTitle')} />
|
||||
<main className="px-4 py-6">
|
||||
<Alert>
|
||||
<AlertDescription>{t('upload.cameraUnsupported.message')}</AlertDescription>
|
||||
</Alert>
|
||||
</main>
|
||||
<BottomNav />
|
||||
</div>
|
||||
return renderPage(
|
||||
<Alert>
|
||||
<AlertDescription>{t('upload.cameraUnsupported.message')}</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (loadingTask) {
|
||||
return (
|
||||
<div className="pb-16">
|
||||
<Header eventToken={eventKey} title={t('upload.cameraTitle')} />
|
||||
<main className="px-4 py-6 flex flex-col items-center justify-center text-center">
|
||||
<Loader2 className="h-10 w-10 animate-spin text-pink-500 mb-4" />
|
||||
<p className="text-sm text-muted-foreground">{t('upload.preparing')}</p>
|
||||
</main>
|
||||
<BottomNav />
|
||||
return renderPage(
|
||||
<div className="flex flex-col items-center justify-center gap-4 text-center">
|
||||
<Loader2 className="h-10 w-10 animate-spin text-pink-500" />
|
||||
<p className="text-sm text-muted-foreground">{t('upload.preparing')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!canUpload) {
|
||||
return (
|
||||
<div className="pb-16">
|
||||
<Header eventToken={eventKey} title={t('upload.cameraTitle')} />
|
||||
<main className="px-4 py-6">
|
||||
<Alert variant="destructive">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
{t('upload.limitReached')
|
||||
.replace('{used}', `${eventPackage?.used_photos || 0}`)
|
||||
.replace('{max}', `${eventPackage?.package.max_photos || 0}`)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</main>
|
||||
<BottomNav />
|
||||
</div>
|
||||
return renderPage(
|
||||
<Alert variant="destructive">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
{t('upload.limitReached')
|
||||
.replace('{used}', `${eventPackage?.used_photos || 0}`)
|
||||
.replace('{max}', `${eventPackage?.package.max_photos || 0}`)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -636,18 +656,16 @@ export default function UploadPage() {
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="pb-16">
|
||||
<Header eventToken={eventKey} title={t('upload.cameraTitle')} />
|
||||
<main className="relative flex flex-col gap-4 pb-4">
|
||||
<div className="absolute left-0 right-0 top-0" aria-hidden="true">
|
||||
{renderPrimer()}
|
||||
</div>
|
||||
<div className="pt-32" />
|
||||
{permissionState !== 'granted' && renderPermissionNotice()}
|
||||
return renderPage(
|
||||
<>
|
||||
<div className="absolute left-0 right-0 top-0" aria-hidden="true">
|
||||
{renderPrimer()}
|
||||
</div>
|
||||
<div className="pt-32" />
|
||||
{permissionState !== 'granted' && renderPermissionNotice()}
|
||||
|
||||
<section className="relative mx-4 overflow-hidden rounded-2xl border border-white/10 bg-black text-white shadow-xl">
|
||||
<div className="relative aspect-[3/4] sm:aspect-video">
|
||||
<section className="relative mx-4 overflow-hidden rounded-2xl border border-white/10 bg-black text-white shadow-xl">
|
||||
<div className="relative aspect-[3/4] sm:aspect-video">
|
||||
<video
|
||||
ref={videoRef}
|
||||
className={cn(
|
||||
@@ -863,9 +881,10 @@ export default function UploadPage() {
|
||||
/>
|
||||
|
||||
<div ref={liveRegionRef} className="sr-only" aria-live="assertive" />
|
||||
</main>
|
||||
<BottomNav />
|
||||
<canvas ref={canvasRef} className="hidden" />
|
||||
</div>
|
||||
|
||||
<canvas ref={canvasRef} className="hidden" />
|
||||
</>
|
||||
,
|
||||
'relative flex flex-col gap-4 pb-4'
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user