Added opaque join-token support across backend and frontend: new migration/model/service/endpoints, guest controllers now resolve tokens, and the demo seeder seeds a token. Tenant event details list/manage tokens with copy/revoke actions, and the guest PWA uses tokens end-to-end (routing, storage, uploads, achievements, etc.). Docs TODO updated to reflect completed steps.
This commit is contained in:
@@ -55,7 +55,8 @@ const DEFAULT_PREFS: CameraPreferences = {
|
||||
};
|
||||
|
||||
export default function UploadPage() {
|
||||
const { slug } = useParams<{ slug: string }>();
|
||||
const { token: slug } = useParams<{ token: string }>();
|
||||
const eventKey = slug ?? '';
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { appearance } = useAppearance();
|
||||
@@ -65,8 +66,8 @@ export default function UploadPage() {
|
||||
const taskIdParam = searchParams.get('task');
|
||||
const emotionSlug = searchParams.get('emotion') || '';
|
||||
|
||||
const primerStorageKey = slug ? `guestCameraPrimerDismissed_${slug}` : 'guestCameraPrimerDismissed';
|
||||
const prefsStorageKey = slug ? `guestCameraPrefs_${slug}` : 'guestCameraPrefs';
|
||||
const primerStorageKey = eventKey ? `guestCameraPrimerDismissed_${eventKey}` : 'guestCameraPrimerDismissed';
|
||||
const prefsStorageKey = eventKey ? `guestCameraPrefs_${eventKey}` : 'guestCameraPrefs';
|
||||
|
||||
const supportsCamera = typeof navigator !== 'undefined' && !!navigator.mediaDevices?.getUserMedia;
|
||||
|
||||
@@ -148,7 +149,7 @@ export default function UploadPage() {
|
||||
setLoadingTask(true);
|
||||
setTaskError(null);
|
||||
|
||||
const res = await fetch(`/api/v1/events/${encodeURIComponent(slug!)}/tasks`);
|
||||
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;
|
||||
@@ -203,15 +204,15 @@ export default function UploadPage() {
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [slug, taskId, emotionSlug]);
|
||||
}, [eventKey, taskId, emotionSlug]);
|
||||
|
||||
// Check upload limits
|
||||
useEffect(() => {
|
||||
if (!slug || !task) return;
|
||||
if (!eventKey || !task) return;
|
||||
|
||||
const checkLimits = async () => {
|
||||
try {
|
||||
const pkg = await getEventPackage(slug);
|
||||
const pkg = await getEventPackage(eventKey);
|
||||
setEventPackage(pkg);
|
||||
if (pkg && pkg.used_photos >= pkg.package.max_photos) {
|
||||
setCanUpload(false);
|
||||
@@ -227,7 +228,7 @@ export default function UploadPage() {
|
||||
};
|
||||
|
||||
checkLimits();
|
||||
}, [slug, task]);
|
||||
}, [eventKey, task]);
|
||||
|
||||
const stopStream = useCallback(() => {
|
||||
if (streamRef.current) {
|
||||
@@ -444,19 +445,19 @@ export default function UploadPage() {
|
||||
|
||||
const navigateAfterUpload = useCallback(
|
||||
(photoId: number | undefined) => {
|
||||
if (!slug || !task) return;
|
||||
if (!eventKey || !task) return;
|
||||
const params = new URLSearchParams();
|
||||
params.set('uploaded', 'true');
|
||||
if (task.id) params.set('task', String(task.id));
|
||||
if (photoId) params.set('photo', String(photoId));
|
||||
if (emotionSlug) params.set('emotion', emotionSlug);
|
||||
navigate(`/e/${encodeURIComponent(slug!)}/gallery?${params.toString()}`);
|
||||
navigate(`/e/${encodeURIComponent(eventKey)}/gallery?${params.toString()}`);
|
||||
},
|
||||
[emotionSlug, navigate, slug, task]
|
||||
[emotionSlug, navigate, eventKey, task]
|
||||
);
|
||||
|
||||
const handleUsePhoto = useCallback(async () => {
|
||||
if (!slug || !reviewPhoto || !task || !canUpload) return;
|
||||
if (!eventKey || !reviewPhoto || !task || !canUpload) return;
|
||||
setMode('uploading');
|
||||
setUploadProgress(5);
|
||||
setUploadError(null);
|
||||
@@ -470,7 +471,7 @@ export default function UploadPage() {
|
||||
}, 400);
|
||||
|
||||
try {
|
||||
const photoId = await uploadPhoto(slug, reviewPhoto.file, task.id, emotionSlug || undefined);
|
||||
const photoId = await uploadPhoto(eventKey, reviewPhoto.file, task.id, emotionSlug || undefined);
|
||||
setUploadProgress(100);
|
||||
setStatusMessage('Upload abgeschlossen.');
|
||||
markCompleted(task.id);
|
||||
@@ -487,7 +488,7 @@ export default function UploadPage() {
|
||||
}
|
||||
setStatusMessage('');
|
||||
}
|
||||
}, [emotionSlug, markCompleted, navigateAfterUpload, reviewPhoto, slug, stopStream, task, canUpload]);
|
||||
}, [emotionSlug, markCompleted, navigateAfterUpload, reviewPhoto, eventKey, stopStream, task, canUpload]);
|
||||
|
||||
const handleGalleryPick = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!canUpload) return;
|
||||
@@ -532,7 +533,7 @@ export default function UploadPage() {
|
||||
if (!supportsCamera && !task) {
|
||||
return (
|
||||
<div className="pb-16">
|
||||
<Header slug={slug} title="Kamera" />
|
||||
<Header slug={eventKey} title="Kamera" />
|
||||
<main className="px-4 py-6">
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
@@ -548,7 +549,7 @@ export default function UploadPage() {
|
||||
if (loadingTask) {
|
||||
return (
|
||||
<div className="pb-16">
|
||||
<Header slug={slug} title="Kamera" />
|
||||
<Header slug={eventKey} title="Kamera" />
|
||||
<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>
|
||||
@@ -561,7 +562,7 @@ export default function UploadPage() {
|
||||
if (!canUpload) {
|
||||
return (
|
||||
<div className="pb-16">
|
||||
<Header slug={slug} title="Kamera" />
|
||||
<Header slug={eventKey} title="Kamera" />
|
||||
<main className="px-4 py-6">
|
||||
<Alert variant="destructive">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
@@ -629,7 +630,7 @@ export default function UploadPage() {
|
||||
|
||||
return (
|
||||
<div className="pb-16">
|
||||
<Header slug={slug} title="Kamera" />
|
||||
<Header slug={eventKey} title="Kamera" />
|
||||
<main className="relative flex flex-col gap-4 pb-4">
|
||||
<div className="absolute left-0 right-0 top-0" aria-hidden="true">
|
||||
{renderPrimer()}
|
||||
|
||||
Reference in New Issue
Block a user