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:
Codex Agent
2025-10-12 10:32:37 +02:00
parent d04e234ca0
commit 9394c3171e
73 changed files with 3277 additions and 911 deletions

View File

@@ -1,7 +1,7 @@
import React from 'react';
function storageKey(slug: string) {
return `guestTasks_${slug}`;
function storageKey(eventKey: string) {
return `guestTasks_${eventKey}`;
}
function parseStored(value: string | null) {
@@ -20,18 +20,18 @@ function parseStored(value: string | null) {
}
}
export function useGuestTaskProgress(slug: string | undefined) {
export function useGuestTaskProgress(eventKey: string | undefined) {
const [completed, setCompleted] = React.useState<number[]>([]);
const [hydrated, setHydrated] = React.useState(false);
React.useEffect(() => {
if (!slug) {
if (!eventKey) {
setCompleted([]);
setHydrated(true);
return;
}
try {
const stored = window.localStorage.getItem(storageKey(slug));
const stored = window.localStorage.getItem(storageKey(eventKey));
setCompleted(parseStored(stored));
} catch (error) {
console.warn('Failed to read task progress', error);
@@ -39,24 +39,24 @@ export function useGuestTaskProgress(slug: string | undefined) {
} finally {
setHydrated(true);
}
}, [slug]);
}, [eventKey]);
const persist = React.useCallback(
(next: number[]) => {
if (!slug) return;
if (!eventKey) return;
setCompleted(next);
try {
window.localStorage.setItem(storageKey(slug), JSON.stringify(next));
window.localStorage.setItem(storageKey(eventKey), JSON.stringify(next));
} catch (error) {
console.warn('Failed to persist task progress', error);
}
},
[slug]
[eventKey]
);
const markCompleted = React.useCallback(
(taskId: number) => {
if (!slug || !Number.isInteger(taskId)) {
if (!eventKey || !Number.isInteger(taskId)) {
return;
}
setCompleted((prev) => {
@@ -65,25 +65,25 @@ export function useGuestTaskProgress(slug: string | undefined) {
}
const next = [...prev, taskId];
try {
window.localStorage.setItem(storageKey(slug), JSON.stringify(next));
window.localStorage.setItem(storageKey(eventKey), JSON.stringify(next));
} catch (error) {
console.warn('Failed to persist task progress', error);
}
return next;
});
},
[slug]
[eventKey]
);
const clearProgress = React.useCallback(() => {
if (!slug) return;
if (!eventKey) return;
setCompleted([]);
try {
window.localStorage.removeItem(storageKey(slug));
window.localStorage.removeItem(storageKey(eventKey));
} catch (error) {
console.warn('Failed to clear task progress', error);
}
}, [slug]);
}, [eventKey]);
const isCompleted = React.useCallback(
(taskId: number | null | undefined) => {