/** * Public event presentation config: the colour theme and the comments feature flag, * served unauthenticated from `GET /api/v1/event`. Loaded once on boot (and refreshed * on the `event-updated` SSE) so the theme applies before the user has even joined and * the comment UI reflects the instance setting. * * The resolved theme CSS is cached in localStorage so the boot script in app.html can * paint the right colours before the JS bundle loads (no flash of the default palette). */ import { writable } from 'svelte/store'; import { api } from './api'; import { buildPaletteCss, applyPaletteCss, type ThemeConfig } from './theme/palette'; export const PALETTE_CACHE_KEY = 'eventsnap_palette_css'; export type EventConfig = { name: string; slug: string; commentsEnabled: boolean; theme: ThemeConfig; }; export const eventConfig = writable(null); /** * Convenience flag for the comment UI. `null` until `/event` answers — deliberately NOT an * optimistic `true`. * * Optimistic-open was wrong in the direction that costs a guest their words. Production pins * `COMMENTS_ENABLED=false`, so on every cold open the comment button, the grid tile button and * the whole lightbox composer rendered for as long as `/event` took — up to the 20s api timeout * on congested venue wifi, and PERMANENTLY if that request failed, because the catch below * leaves the last value in place. A guest would type a comment, tap Senden, get a red error, * and watch the text be discarded. * * `null` is falsy, so every `{#if $commentsEnabled}` hides and every `$commentsEnabled ? a : b` * picks the comments-off copy until the server has actually said otherwise. The failure mode * flips from "offered something that doesn't work" to "revealed a moment late". */ export const commentsEnabled = writable(null); type PublicEventDto = { name: string; slug: string; comments_enabled?: boolean; theme_preset?: string; theme_primary?: string; theme_accent?: string; }; /** Fetch /event, apply the theme, and cache the resolved CSS for the next boot. */ export async function loadEventConfig(): Promise { try { // Through `api` rather than a bare `fetch`: this was the ONE request in the app with // no AbortController deadline. On a congested venue WiFi a socket that never answers // left the promise pending indefinitely, so the palette never reconciled with the // server — and since `event-updated` re-calls this, those zombie requests stack up. // `api` aborts at 20s and turns it into an ApiError the catch below already handles. const b = await api.get('/event'); const theme: ThemeConfig = { preset: b.theme_preset ?? 'champagne-gold', primary: b.theme_primary ?? '#8a6a2b', accent: b.theme_accent ?? '#8a6a2b' }; eventConfig.set({ name: b.name, slug: b.slug, commentsEnabled: b.comments_enabled ?? true, theme }); commentsEnabled.set(b.comments_enabled ?? true); const css = buildPaletteCss(theme); applyPaletteCss(css); try { localStorage.setItem(PALETTE_CACHE_KEY, css); } catch { /* private mode / quota — the theme still applied for this session */ } } catch { /* offline / server down — keep whatever the boot script already painted */ } } /** Apply a theme immediately without persisting — used by the admin live preview. */ export function previewTheme(theme: ThemeConfig): void { applyPaletteCss(buildPaletteCss(theme)); }