+
+
Site Theme
+
+ Control seasonal decorations and the nav logo animation globally.
+ Changes take effect for all users within ~60 seconds (server cache TTL).
+
+
+
+
+
+ Particle Decoration
+
+ {#each DECORATIONS as d}
+
+ {/each}
+
+
+
+
+
+ Logo Animation
+
+ {#each LOGO_ANIMATIONS as a}
+
+ {/each}
+
+
+
+
+
+
+
+
+
+ {#if saved}
+
+
+ Saved
+
+ {/if}
+ {#if errMsg}
+
{errMsg}
+ {/if}
+
+
diff --git a/ui/src/routes/api/site-config/+server.ts b/ui/src/routes/api/site-config/+server.ts
new file mode 100644
index 0000000..9b9d279
--- /dev/null
+++ b/ui/src/routes/api/site-config/+server.ts
@@ -0,0 +1,57 @@
+import { json, error } from '@sveltejs/kit';
+import type { RequestHandler } from './$types';
+import { getSiteConfig, saveSiteConfig } from '$lib/server/pocketbase';
+import { log } from '$lib/server/logger';
+
+/**
+ * GET /api/site-config
+ * Public — returns current site-wide decoration/animation settings.
+ */
+export const GET: RequestHandler = async () => {
+ try {
+ const config = await getSiteConfig();
+ return json(config);
+ } catch (e) {
+ log.error('site-config', 'GET failed', { err: String(e) });
+ error(500, 'Failed to load site config');
+ }
+};
+
+/**
+ * PUT /api/site-config
+ * Admin only — updates decoration + logoAnimation + eventLabel.
+ */
+export const PUT: RequestHandler = async ({ request, locals }) => {
+ if (!locals.user || locals.user.role !== 'admin') {
+ error(403, 'Forbidden');
+ }
+
+ const body = await request.json().catch(() => null);
+ if (!body) error(400, 'Invalid JSON body');
+
+ const validDecorations = ['snow', 'sakura', 'fireflies', 'leaves', 'stars', null];
+ if (body.decoration !== undefined && !validDecorations.includes(body.decoration)) {
+ error(400, `Invalid decoration — must be one of: ${validDecorations.filter(Boolean).join(', ')}, or null`);
+ }
+
+ const validLogoAnimations = ['none', 'glow', 'rainbow', 'pulse', 'shimmer'];
+ if (body.logoAnimation !== undefined && !validLogoAnimations.includes(body.logoAnimation)) {
+ error(400, `Invalid logoAnimation — must be one of: ${validLogoAnimations.join(', ')}`);
+ }
+
+ if (body.eventLabel !== undefined && typeof body.eventLabel !== 'string') {
+ error(400, 'eventLabel must be a string');
+ }
+
+ try {
+ await saveSiteConfig({
+ decoration: body.decoration ?? null,
+ logoAnimation: body.logoAnimation ?? 'none',
+ eventLabel: typeof body.eventLabel === 'string' ? body.eventLabel.slice(0, 64) : '',
+ });
+ return json({ ok: true });
+ } catch (e) {
+ log.error('site-config', 'PUT failed', { err: String(e) });
+ error(500, 'Failed to save site config');
+ }
+};