Guest PWA vollständig lokalisiert
This commit is contained in:
@@ -22,6 +22,7 @@ import {
|
||||
ZapOff,
|
||||
} from 'lucide-react';
|
||||
import { getEventPackage, type EventPackage } from '../services/eventApi';
|
||||
import { useTranslation } from '../i18n/useTranslation';
|
||||
|
||||
interface Task {
|
||||
id: number;
|
||||
@@ -62,6 +63,7 @@ export default function UploadPage() {
|
||||
const { appearance } = useAppearance();
|
||||
const isDarkMode = appearance === 'dark';
|
||||
const { markCompleted } = useGuestTaskProgress(slug);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const taskIdParam = searchParams.get('task');
|
||||
const emotionSlug = searchParams.get('emotion') || '';
|
||||
@@ -137,7 +139,7 @@ export default function UploadPage() {
|
||||
// Load task metadata
|
||||
useEffect(() => {
|
||||
if (!slug || !taskId) {
|
||||
setTaskError('Keine Aufgabeninformationen gefunden.');
|
||||
setTaskError(t('upload.loadError.title'));
|
||||
setLoadingTask(false);
|
||||
return;
|
||||
}
|
||||
@@ -145,6 +147,10 @@ export default function UploadPage() {
|
||||
let active = true;
|
||||
|
||||
async function loadTask() {
|
||||
const fallbackTitle = t('upload.taskInfo.fallbackTitle').replace('{id}', `${taskId!}`);
|
||||
const fallbackDescription = t('upload.taskInfo.fallbackDescription');
|
||||
const fallbackInstructions = t('upload.taskInfo.fallbackInstructions');
|
||||
|
||||
try {
|
||||
setLoadingTask(true);
|
||||
setTaskError(null);
|
||||
@@ -159,9 +165,9 @@ export default function UploadPage() {
|
||||
if (found) {
|
||||
setTask({
|
||||
id: found.id,
|
||||
title: found.title || `Aufgabe ${taskId!}`,
|
||||
description: found.description || 'Halte den Moment fest und teile ihn mit allen Gästen.',
|
||||
instructions: found.instructions,
|
||||
title: found.title || fallbackTitle,
|
||||
description: found.description || fallbackDescription,
|
||||
instructions: found.instructions ?? fallbackInstructions,
|
||||
duration: found.duration || 2,
|
||||
emotion: found.emotion,
|
||||
difficulty: found.difficulty ?? 'medium',
|
||||
@@ -169,9 +175,9 @@ export default function UploadPage() {
|
||||
} else {
|
||||
setTask({
|
||||
id: taskId!,
|
||||
title: `Aufgabe ${taskId!}`,
|
||||
description: 'Halte den Moment fest und teile ihn mit allen Gästen.',
|
||||
instructions: 'Positioniere alle im Bild, starte den Countdown und lass die Emotion wirken.',
|
||||
title: fallbackTitle,
|
||||
description: fallbackDescription,
|
||||
instructions: fallbackInstructions,
|
||||
duration: 2,
|
||||
emotion: emotionSlug
|
||||
? { slug: emotionSlug, name: emotionSlug.replace('-', ' ').replace(/\b\w/g, (l) => l.toUpperCase()) }
|
||||
@@ -182,12 +188,12 @@ export default function UploadPage() {
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch task', error);
|
||||
if (active) {
|
||||
setTaskError('Aufgabe konnte nicht geladen werden. Du kannst trotzdem ein Foto machen.');
|
||||
setTaskError(t('upload.loadError.title'));
|
||||
setTask({
|
||||
id: taskId!,
|
||||
title: `Aufgabe ${taskId!}`,
|
||||
description: 'Halte den Moment fest und teile ihn mit allen Gästen.',
|
||||
instructions: 'Positioniere alle im Bild, starte den Countdown und lass die Emotion wirken.',
|
||||
title: fallbackTitle,
|
||||
description: fallbackDescription,
|
||||
instructions: fallbackInstructions,
|
||||
duration: 2,
|
||||
emotion: emotionSlug
|
||||
? { slug: emotionSlug, name: emotionSlug.replace('-', ' ').replace(/\b\w/g, (l) => l.toUpperCase()) }
|
||||
@@ -204,7 +210,7 @@ export default function UploadPage() {
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [eventKey, taskId, emotionSlug]);
|
||||
}, [eventKey, taskId, emotionSlug, t]);
|
||||
|
||||
// Check upload limits
|
||||
useEffect(() => {
|
||||
@@ -216,19 +222,27 @@ export default function UploadPage() {
|
||||
setEventPackage(pkg);
|
||||
if (pkg && pkg.used_photos >= pkg.package.max_photos) {
|
||||
setCanUpload(false);
|
||||
setUploadError('Upload-Limit erreicht. Kontaktieren Sie den Organisator für ein Upgrade.');
|
||||
const maxLabel = pkg.package.max_photos == null
|
||||
? t('upload.limitUnlimited')
|
||||
: `${pkg.package.max_photos}`;
|
||||
setUploadError(
|
||||
t('upload.limitReached')
|
||||
.replace('{used}', `${pkg.used_photos}`)
|
||||
.replace('{max}', maxLabel)
|
||||
);
|
||||
} else {
|
||||
setCanUpload(true);
|
||||
setUploadError(null);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to check package limits', err);
|
||||
setCanUpload(false);
|
||||
setUploadError('Fehler beim Prüfen des Limits. Upload deaktiviert.');
|
||||
setUploadError(t('upload.limitCheckError'));
|
||||
}
|
||||
};
|
||||
|
||||
checkLimits();
|
||||
}, [eventKey, task]);
|
||||
}, [eventKey, task, t]);
|
||||
|
||||
const stopStream = useCallback(() => {
|
||||
if (streamRef.current) {
|
||||
@@ -265,7 +279,7 @@ export default function UploadPage() {
|
||||
const startCamera = useCallback(async () => {
|
||||
if (!supportsCamera) {
|
||||
setPermissionState('unsupported');
|
||||
setPermissionMessage('Dieses Gerät oder der Browser unterstützt keine Kamera-Zugriffe.');
|
||||
setPermissionMessage(t('upload.cameraUnsupported.message'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -286,18 +300,16 @@ export default function UploadPage() {
|
||||
|
||||
if (error?.name === 'NotAllowedError') {
|
||||
setPermissionState('denied');
|
||||
setPermissionMessage(
|
||||
'Kamera-Zugriff wurde blockiert. Prüfe die Berechtigungen deines Browsers und versuche es erneut.'
|
||||
);
|
||||
setPermissionMessage(t('upload.cameraDenied.explanation'));
|
||||
} else if (error?.name === 'NotFoundError') {
|
||||
setPermissionState('error');
|
||||
setPermissionMessage('Keine Kamera gefunden. Du kannst stattdessen ein Foto aus deiner Galerie wählen.');
|
||||
setPermissionMessage(t('upload.cameraUnsupported.message'));
|
||||
} else {
|
||||
setPermissionState('error');
|
||||
setPermissionMessage(`Kamera konnte nicht gestartet werden: ${error?.message || 'Unbekannter Fehler'}`);
|
||||
setPermissionMessage(t('upload.cameraError.explanation'));
|
||||
}
|
||||
}
|
||||
}, [attachStreamToVideo, createConstraint, mode, preferences.facingMode, stopStream, supportsCamera, task]);
|
||||
}, [attachStreamToVideo, createConstraint, mode, preferences.facingMode, stopStream, supportsCamera, task, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!task || loadingTask) return;
|
||||
@@ -311,15 +323,15 @@ export default function UploadPage() {
|
||||
useEffect(() => {
|
||||
if (!liveRegionRef.current) return;
|
||||
if (mode === 'countdown') {
|
||||
liveRegionRef.current.textContent = `Foto wird in ${countdownValue} Sekunden aufgenommen.`;
|
||||
liveRegionRef.current.textContent = t('upload.countdown.ready').replace('{count}', `${countdownValue}`);
|
||||
} else if (mode === 'review') {
|
||||
liveRegionRef.current.textContent = 'Foto aufgenommen. <20>berpr<70>fe die Vorschau.';
|
||||
liveRegionRef.current.textContent = t('upload.review.readyAnnouncement');
|
||||
} else if (mode === 'uploading') {
|
||||
liveRegionRef.current.textContent = 'Foto wird hochgeladen.';
|
||||
liveRegionRef.current.textContent = t('upload.status.uploading');
|
||||
} else {
|
||||
liveRegionRef.current.textContent = '';
|
||||
}
|
||||
}, [mode, countdownValue]);
|
||||
}, [mode, countdownValue, t]);
|
||||
|
||||
const dismissPrimer = useCallback(() => {
|
||||
setShowPrimer(false);
|
||||
@@ -360,7 +372,7 @@ export default function UploadPage() {
|
||||
|
||||
const performCapture = useCallback(() => {
|
||||
if (!videoRef.current || !canvasRef.current) {
|
||||
setUploadError('Kamera nicht bereit. Bitte versuche es erneut.');
|
||||
setUploadError(t('upload.captureError'));
|
||||
setMode('preview');
|
||||
return;
|
||||
}
|
||||
@@ -371,7 +383,7 @@ export default function UploadPage() {
|
||||
const height = video.videoHeight;
|
||||
|
||||
if (!width || !height) {
|
||||
setUploadError('Kamera liefert kein Bild. Bitte starte die Kamera neu.');
|
||||
setUploadError(t('upload.feedError'));
|
||||
setMode('preview');
|
||||
startCamera();
|
||||
return;
|
||||
@@ -381,7 +393,7 @@ export default function UploadPage() {
|
||||
canvas.height = height;
|
||||
const context = canvas.getContext('2d');
|
||||
if (!context) {
|
||||
setUploadError('Canvas konnte nicht initialisiert werden.');
|
||||
setUploadError(t('upload.canvasError'));
|
||||
setMode('preview');
|
||||
return;
|
||||
}
|
||||
@@ -399,7 +411,7 @@ export default function UploadPage() {
|
||||
canvas.toBlob(
|
||||
(blob) => {
|
||||
if (!blob) {
|
||||
setUploadError('Foto konnte nicht erstellt werden.');
|
||||
setUploadError(t('upload.captureError'));
|
||||
setMode('preview');
|
||||
return;
|
||||
}
|
||||
@@ -413,7 +425,7 @@ export default function UploadPage() {
|
||||
'image/jpeg',
|
||||
0.92
|
||||
);
|
||||
}, [preferences.facingMode, preferences.mirrorFrontPreview, startCamera]);
|
||||
}, [preferences.facingMode, preferences.mirrorFrontPreview, startCamera, t]);
|
||||
|
||||
const beginCapture = useCallback(() => {
|
||||
setUploadError(null);
|
||||
@@ -461,7 +473,7 @@ export default function UploadPage() {
|
||||
setMode('uploading');
|
||||
setUploadProgress(5);
|
||||
setUploadError(null);
|
||||
setStatusMessage('Foto wird vorbereitet...');
|
||||
setStatusMessage(t('upload.status.preparing'));
|
||||
|
||||
if (uploadProgressTimerRef.current) {
|
||||
window.clearInterval(uploadProgressTimerRef.current);
|
||||
@@ -473,13 +485,13 @@ export default function UploadPage() {
|
||||
try {
|
||||
const photoId = await uploadPhoto(eventKey, reviewPhoto.file, task.id, emotionSlug || undefined);
|
||||
setUploadProgress(100);
|
||||
setStatusMessage('Upload abgeschlossen.');
|
||||
setStatusMessage(t('upload.status.completed'));
|
||||
markCompleted(task.id);
|
||||
stopStream();
|
||||
navigateAfterUpload(photoId);
|
||||
} catch (error: any) {
|
||||
console.error('Upload failed', error);
|
||||
setUploadError(error?.message || 'Upload fehlgeschlagen. Bitte versuche es erneut.');
|
||||
setUploadError(error?.message || t('upload.status.failed'));
|
||||
setMode('review');
|
||||
} finally {
|
||||
if (uploadProgressTimerRef.current) {
|
||||
@@ -488,7 +500,7 @@ export default function UploadPage() {
|
||||
}
|
||||
setStatusMessage('');
|
||||
}
|
||||
}, [emotionSlug, markCompleted, navigateAfterUpload, reviewPhoto, eventKey, stopStream, task, canUpload]);
|
||||
}, [emotionSlug, markCompleted, navigateAfterUpload, reviewPhoto, eventKey, stopStream, task, canUpload, t]);
|
||||
|
||||
const handleGalleryPick = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!canUpload) return;
|
||||
@@ -501,10 +513,10 @@ export default function UploadPage() {
|
||||
setMode('review');
|
||||
};
|
||||
reader.onerror = () => {
|
||||
setUploadError('Auswahl fehlgeschlagen. Bitte versuche es erneut.');
|
||||
setUploadError(t('upload.galleryPickError'));
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}, [canUpload]);
|
||||
}, [canUpload, t]);
|
||||
|
||||
const difficultyBadgeClass = useMemo(() => {
|
||||
if (!task) return 'text-white';
|
||||
@@ -533,12 +545,10 @@ export default function UploadPage() {
|
||||
if (!supportsCamera && !task) {
|
||||
return (
|
||||
<div className="pb-16">
|
||||
<Header slug={eventKey} title="Kamera" />
|
||||
<Header slug={eventKey} title={t('upload.cameraTitle')} />
|
||||
<main className="px-4 py-6">
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
Dieses Gerät unterstützt keine Kamera-Zugriffe. Du kannst stattdessen Fotos aus deiner Galerie hochladen.
|
||||
</AlertDescription>
|
||||
<AlertDescription>{t('upload.cameraUnsupported.message')}</AlertDescription>
|
||||
</Alert>
|
||||
</main>
|
||||
<BottomNav />
|
||||
@@ -549,10 +559,10 @@ export default function UploadPage() {
|
||||
if (loadingTask) {
|
||||
return (
|
||||
<div className="pb-16">
|
||||
<Header slug={eventKey} title="Kamera" />
|
||||
<Header slug={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">Aufgabe und Kamera werden vorbereitet ...</p>
|
||||
<p className="text-sm text-muted-foreground">{t('upload.preparing')}</p>
|
||||
</main>
|
||||
<BottomNav />
|
||||
</div>
|
||||
@@ -562,13 +572,14 @@ export default function UploadPage() {
|
||||
if (!canUpload) {
|
||||
return (
|
||||
<div className="pb-16">
|
||||
<Header slug={eventKey} title="Kamera" />
|
||||
<Header slug={eventKey} title={t('upload.cameraTitle')} />
|
||||
<main className="px-4 py-6">
|
||||
<Alert variant="destructive">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Upload-Limit erreicht ({eventPackage?.used_photos || 0} / {eventPackage?.package.max_photos || 0} Fotos).
|
||||
Kontaktieren Sie den Organisator für ein Package-Upgrade.
|
||||
{t('upload.limitReached')
|
||||
.replace('{used}', `${eventPackage?.used_photos || 0}`)
|
||||
.replace('{max}', `${eventPackage?.package.max_photos || 0}`)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</main>
|
||||
@@ -583,13 +594,14 @@ export default function UploadPage() {
|
||||
<div className="flex items-start gap-3">
|
||||
<Info className="mt-0.5 h-5 w-5 flex-shrink-0" />
|
||||
<div className="text-left">
|
||||
<p className="font-semibold">Bereit für dein Shooting?</p>
|
||||
<p className="font-semibold">{t('upload.primer.title')}</p>
|
||||
<p className="mt-1">
|
||||
Suche dir gutes Licht, halte die Stimmung der Aufgabe fest und nutze die Kontrollleiste für Countdown, Grid und Kamerawechsel.
|
||||
{t('upload.primer.body.part1')}{' '}
|
||||
{t('upload.primer.body.part2')}
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onClick={dismissPrimer}>
|
||||
Alles klar
|
||||
{t('upload.primer.dismiss')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -601,9 +613,7 @@ export default function UploadPage() {
|
||||
if (permissionState === 'unsupported') {
|
||||
return (
|
||||
<Alert className="mx-4">
|
||||
<AlertDescription>
|
||||
Dieses Gerät unterstützt keine Kamera. Nutze den Button `Foto aus Galerie wählen`, um dennoch teilzunehmen.
|
||||
</AlertDescription>
|
||||
<AlertDescription>{t('upload.cameraUnsupported.message')}</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
@@ -613,24 +623,22 @@ export default function UploadPage() {
|
||||
<AlertDescription className="space-y-3">
|
||||
<div>{permissionMessage}</div>
|
||||
<Button size="sm" variant="outline" onClick={startCamera}>
|
||||
Erneut versuchen
|
||||
{t('upload.buttons.tryAgain')}
|
||||
</Button>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Alert className="mx-4">
|
||||
<AlertDescription>
|
||||
Wir benötigen Zugriff auf deine Kamera. Bestätige die Browser-Abfrage oder nutze alternativ ein Foto aus deiner Galerie.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<Alert className="mx-4">
|
||||
<AlertDescription>{t('upload.cameraDenied.prompt')}</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="pb-16">
|
||||
<Header slug={eventKey} title="Kamera" />
|
||||
<Header slug={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()}
|
||||
@@ -666,14 +674,17 @@ export default function UploadPage() {
|
||||
<div className="absolute inset-0 z-20 flex flex-col items-center justify-center bg-black/70 text-center text-sm">
|
||||
<Camera className="mb-3 h-8 w-8 text-pink-400" />
|
||||
<p className="max-w-xs text-white/90">
|
||||
Kamera ist nicht aktiv. {permissionMessage || 'Tippe auf `Kamera starten`, um loszulegen.'}
|
||||
{t('upload.cameraInactive').replace(
|
||||
'{hint}',
|
||||
(permissionMessage ?? t('upload.cameraInactiveHint').replace('{label}', t('upload.buttons.startCamera')))
|
||||
)}
|
||||
</p>
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
<Button size="sm" onClick={startCamera}>
|
||||
Kamera starten
|
||||
{t('upload.buttons.startCamera')}
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" onClick={() => fileInputRef.current?.click()}>
|
||||
Foto aus Galerie wählen
|
||||
{t('upload.galleryButton')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -684,14 +695,10 @@ export default function UploadPage() {
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<Badge variant="secondary" className="flex items-center gap-2 text-xs">
|
||||
<Sparkles className="h-3.5 w-3.5" />
|
||||
Aufgabe #{task.id}
|
||||
{t('upload.taskInfo.badge').replace('{id}', `${task.id}`)}
|
||||
</Badge>
|
||||
<span className={cn('text-xs font-medium uppercase tracking-wide', difficultyBadgeClass)}>
|
||||
{task.difficulty === 'easy'
|
||||
? 'Leicht'
|
||||
: task.difficulty === 'hard'
|
||||
? 'Herausfordernd'
|
||||
: 'Medium'}
|
||||
{t(`upload.taskInfo.difficulty.${task.difficulty ?? 'medium'}`)}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
@@ -699,13 +706,19 @@ export default function UploadPage() {
|
||||
<p className="mt-1 text-xs leading-relaxed text-white/80">{task.description}</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2 text-[11px] text-white/70">
|
||||
{task.instructions && <span>Hinweis: {task.instructions}</span>}
|
||||
{task.instructions && (
|
||||
<span>
|
||||
{t('upload.taskInfo.instructionsPrefix')}: {task.instructions}
|
||||
</span>
|
||||
)}
|
||||
{emotionSlug && (
|
||||
<span className="rounded-full border border-white/20 px-2 py-0.5">Stimmung: {task.emotion?.name || emotionSlug}</span>
|
||||
<span className="rounded-full border border-white/20 px-2 py-0.5">
|
||||
{t('upload.taskInfo.emotion').replace('{value}', `${task.emotion?.name || emotionSlug}`)}
|
||||
</span>
|
||||
)}
|
||||
{preferences.countdownEnabled && (
|
||||
<span className="rounded-full border border-white/20 px-2 py-0.5">
|
||||
Countdown: {preferences.countdownSeconds}s
|
||||
{t('upload.countdownLabel').replace('{seconds}', `${preferences.countdownSeconds}`)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -715,7 +728,7 @@ export default function UploadPage() {
|
||||
{mode === 'countdown' && (
|
||||
<div className="absolute inset-0 z-40 flex flex-col items-center justify-center bg-black/60 text-white">
|
||||
<div className="text-6xl font-bold">{countdownValue}</div>
|
||||
<p className="mt-2 text-sm text-white/70">Bereit machen ...</p>
|
||||
<p className="mt-2 text-sm text-white/70">{t('upload.countdownReady')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -760,7 +773,7 @@ export default function UploadPage() {
|
||||
onClick={handleToggleGrid}
|
||||
>
|
||||
<Grid3X3 className="h-5 w-5" />
|
||||
<span className="sr-only">Raster umschalten</span>
|
||||
<span className="sr-only">{t('upload.controls.toggleGrid')}</span>
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
@@ -769,7 +782,7 @@ export default function UploadPage() {
|
||||
onClick={handleToggleCountdown}
|
||||
>
|
||||
<span className="text-sm font-semibold">{preferences.countdownSeconds}s</span>
|
||||
<span className="sr-only">Countdown umschalten</span>
|
||||
<span className="sr-only">{t('upload.controls.toggleCountdown')}</span>
|
||||
</Button>
|
||||
{preferences.facingMode === 'user' && (
|
||||
<Button
|
||||
@@ -779,7 +792,7 @@ export default function UploadPage() {
|
||||
onClick={handleToggleMirror}
|
||||
>
|
||||
<span className="text-sm font-semibold">?</span>
|
||||
<span className="sr-only">Spiegelung für Frontkamera umschalten</span>
|
||||
<span className="sr-only">{t('upload.controls.toggleMirror')}</span>
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
@@ -790,7 +803,7 @@ export default function UploadPage() {
|
||||
disabled={preferences.facingMode !== 'environment'}
|
||||
>
|
||||
{preferences.flashPreferred ? <Zap className="h-5 w-5 text-yellow-300" /> : <ZapOff className="h-5 w-5" />}
|
||||
<span className="sr-only">Blitzpräferenz umschalten</span>
|
||||
<span className="sr-only">{t('upload.controls.toggleFlash')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -802,7 +815,7 @@ export default function UploadPage() {
|
||||
onClick={handleSwitchCamera}
|
||||
>
|
||||
<RotateCcw className="mr-1 h-4 w-4" />
|
||||
Kamera wechseln
|
||||
{t('upload.switchCamera')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
@@ -811,7 +824,7 @@ export default function UploadPage() {
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
<ImagePlus className="mr-1 h-4 w-4" />
|
||||
Foto aus Galerie
|
||||
{t('upload.galleryButton')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -820,10 +833,10 @@ export default function UploadPage() {
|
||||
{mode === 'review' && reviewPhoto ? (
|
||||
<div className="flex w-full max-w-md flex-col gap-3 sm:flex-row">
|
||||
<Button variant="secondary" className="flex-1" onClick={handleRetake}>
|
||||
Noch einmal
|
||||
{t('upload.review.retake')}
|
||||
</Button>
|
||||
<Button className="flex-1" onClick={handleUsePhoto}>
|
||||
Foto verwenden
|
||||
{t('upload.review.keep')}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
@@ -834,7 +847,7 @@ export default function UploadPage() {
|
||||
disabled={!isCameraActive || mode === 'countdown'}
|
||||
>
|
||||
<Camera className="h-7 w-7" />
|
||||
<span className="sr-only">Foto aufnehmen</span>
|
||||
<span className="sr-only">{t('upload.captureButton')}</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user