214 lines
6.4 KiB
TypeScript
214 lines
6.4 KiB
TypeScript
import { demoFixtures, type DemoFixtures } from './fixtures';
|
|
|
|
type DemoConfig = {
|
|
fixtures: DemoFixtures;
|
|
};
|
|
|
|
let enabled = false;
|
|
let originalFetch: typeof window.fetch | null = null;
|
|
const likeState = new Map<number, number>();
|
|
|
|
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 as any).__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 as any).__FOTOSPIEL_DEMO_ACTIVE__ = true;
|
|
notifyDemoToast();
|
|
}
|
|
|
|
function handleDemoRequest(url: URL, request: Request, fixtures: DemoFixtures): Promise<Response> | 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<string, string> {
|
|
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
|
|
}
|
|
}
|