import { demoFixtures, type DemoFixtures } from './fixtures'; type DemoConfig = { fixtures: DemoFixtures; }; let enabled = false; let originalFetch: typeof window.fetch | null = null; const likeState = new Map(); declare global { interface Window { __FOTOSPIEL_DEMO__?: boolean; __FOTOSPIEL_DEMO_ACTIVE__?: boolean; } } export function shouldEnableGuestDemoMode(): boolean { if (typeof window === 'undefined') { return false; } const params = new URLSearchParams(window.location.search); if (params.get('demo') === '1') { return true; } if (window.__FOTOSPIEL_DEMO__ === true) { return true; } const attr = document.documentElement?.dataset?.guestDemo; return attr === 'true'; } export function enableGuestDemoMode(config: DemoConfig = { fixtures: demoFixtures }): void { if (typeof window === 'undefined' || enabled) { return; } originalFetch = window.fetch.bind(window); window.fetch = async (input: RequestInfo | URL, init?: RequestInit) => { const request = new Request(input, init); const url = new URL(request.url, window.location.origin); const response = handleDemoRequest(url, request, config.fixtures); if (response) { return response; } return originalFetch!(request); }; enabled = true; window.__FOTOSPIEL_DEMO_ACTIVE__ = true; notifyDemoToast(); } function handleDemoRequest(url: URL, request: Request, fixtures: DemoFixtures): Promise | null { if (!url.pathname.startsWith('/api/')) { return null; } const eventMatch = url.pathname.match(/^\/api\/v1\/events\/([^/]+)(?:\/(.*))?/); if (eventMatch) { const token = decodeURIComponent(eventMatch[1]); const remainder = eventMatch[2] ?? ''; if (token !== fixtures.token && token !== fixtures.event.slug && token !== 'demo') { return null; } return Promise.resolve(handleDemoEventEndpoint(remainder, request, fixtures)); } const galleryMatch = url.pathname.match(/^\/api\/v1\/gallery\/([^/]+)(?:\/(.*))?/); if (galleryMatch) { const token = decodeURIComponent(galleryMatch[1]); if (token !== fixtures.token && token !== fixtures.event.slug && token !== 'demo') { return null; } const resource = galleryMatch[2] ?? ''; if (!resource) { return Promise.resolve(jsonResponse(fixtures.gallery.meta)); } if (resource.startsWith('photos')) { return Promise.resolve( jsonResponse({ data: fixtures.gallery.photos, next_cursor: null }, { etag: '"demo-gallery"' }) ); } } if (url.pathname.startsWith('/api/v1/photo-shares/')) { return Promise.resolve(jsonResponse(fixtures.share)); } if (url.pathname.startsWith('/api/v1/photos/')) { return Promise.resolve(handlePhotoAction(url, request, fixtures)); } return null; } function handleDemoEventEndpoint(path: string, request: Request, fixtures: DemoFixtures): Response { const [resource, ...rest] = path.split('/').filter(Boolean); const method = request.method.toUpperCase(); switch (resource) { case undefined: return jsonResponse(fixtures.event); case 'stats': return jsonResponse(fixtures.stats); case 'package': return jsonResponse(fixtures.eventPackage); case 'tasks': if (method === 'GET') { return jsonResponse(fixtures.tasks, { etag: '"demo-tasks"' }); } return blockedResponse('Aufgaben können in der Demo nicht geändert werden.'); case 'photos': if (method === 'GET') { return jsonResponse({ data: fixtures.photos, latest_photo_at: fixtures.photos[0]?.created_at ?? null }, { etag: '"demo-photos"', }); } if (method === 'POST') { return blockedResponse('Uploads sind in der Demo deaktiviert.'); } break; case 'upload': return blockedResponse('Uploads sind in der Demo deaktiviert.'); case 'achievements': return jsonResponse(fixtures.achievements, { etag: '"demo-achievements"' }); case 'emotions': return jsonResponse(fixtures.emotions, { etag: '"demo-emotions"' }); case 'notifications': if (rest.length >= 2) { return new Response(null, { status: 204 }); } return jsonResponse({ data: fixtures.notifications, meta: { unread_count: 1 } }, { etag: '"demo-notifications"' }); case 'push-subscriptions': return new Response(null, { status: 204 }); default: break; } return jsonResponse({ demo: true }); } function handlePhotoAction(url: URL, request: Request, fixtures: DemoFixtures): Response { const pathname = url.pathname.replace('/api/v1/photos/', ''); const [photoIdPart, action] = pathname.split('/'); const photoId = Number(photoIdPart); const targetPhoto = fixtures.photos.find((photo) => photo.id === photoId); if (action === 'like') { if (!targetPhoto) { return new Response(JSON.stringify({ error: { message: 'Foto nicht gefunden' } }), { status: 404, headers: demoHeaders() }); } const current = likeState.get(photoId) ?? targetPhoto.likes_count; const next = current + 1; likeState.set(photoId, next); return jsonResponse({ likes_count: next }); } if (action === 'share' && request.method.toUpperCase() === 'POST') { return jsonResponse({ slug: fixtures.share.slug, url: `${window.location.origin}/share/${fixtures.share.slug}` }); } return new Response(JSON.stringify({ error: { message: 'Demo-Endpunkt nicht verfügbar.' } }), { status: 404, headers: demoHeaders(), }); } function jsonResponse(data: unknown, options: { etag?: string } = {}): Response { const headers = demoHeaders(); if (options.etag) { headers.ETag = options.etag; } return new Response(JSON.stringify(data), { status: 200, headers, }); } function demoHeaders(): Record { return { 'Content-Type': 'application/json', 'Cache-Control': 'no-store', }; } function blockedResponse(message: string): Response { return new Response( JSON.stringify({ error: { code: 'demo_read_only', message, }, }), { status: 403, headers: demoHeaders(), } ); } export function isGuestDemoModeEnabled(): boolean { return enabled; } function notifyDemoToast(): void { if (typeof document === 'undefined') { return; } try { const detail = { type: 'info', text: 'Demo-Modus aktiv. Änderungen werden nicht gespeichert.' }; window.setTimeout(() => { window.dispatchEvent(new CustomEvent('guest-toast', { detail })); }, 0); } catch { // ignore } }