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

@@ -3,14 +3,14 @@ import { useParams } from 'react-router-dom';
import { fetchEvent, EventData } from '../services/eventApi';
export function useEventData() {
const { slug } = useParams<{ slug: string }>();
const { token } = useParams<{ token: string }>();
const [event, setEvent] = useState<EventData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!slug) {
setError('No event slug provided');
if (!token) {
setError('No event token provided');
setLoading(false);
return;
}
@@ -19,7 +19,7 @@ export function useEventData() {
try {
setLoading(true);
setError(null);
const eventData = await fetchEvent(slug);
const eventData = await fetchEvent(token);
setEvent(eventData);
} catch (err) {
console.error('Failed to load event:', err);
@@ -30,11 +30,11 @@ export function useEventData() {
};
loadEvent();
}, [slug]);
}, [token]);
return {
event,
loading,
error,
};
}
}

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) => {