upgrade to tamagui v2 and guest pwa overhaul

This commit is contained in:
Codex Agent
2026-02-02 13:01:20 +01:00
parent 2e78f3ab8d
commit 7c6e14ffe2
168 changed files with 47462 additions and 8914 deletions

View File

@@ -0,0 +1,82 @@
import React from 'react';
import { fetchGallery } from '../services/photosApi';
export type GalleryDelta = {
photos: Record<string, unknown>[];
latestPhotoAt: string | null;
nextCursor: string | null;
};
const emptyDelta: GalleryDelta = {
photos: [],
latestPhotoAt: null,
nextCursor: null,
};
export function usePollGalleryDelta(
eventToken: string | null,
options: { intervalMs?: number; locale?: string } = {}
) {
const intervalMs = options.intervalMs ?? 30000;
const [data, setData] = React.useState<GalleryDelta>(emptyDelta);
const [loading, setLoading] = React.useState(Boolean(eventToken));
const [error, setError] = React.useState<string | null>(null);
const latestRef = React.useRef<string | null>(null);
React.useEffect(() => {
if (!eventToken) {
setData(emptyDelta);
setLoading(false);
setError(null);
latestRef.current = null;
return;
}
let active = true;
let timer: number | null = null;
const poll = async () => {
if (document.visibilityState === 'hidden') {
timer = window.setTimeout(poll, intervalMs);
return;
}
try {
setLoading(true);
const response = await fetchGallery(eventToken, {
since: latestRef.current ?? undefined,
locale: options.locale,
});
if (!active) return;
const photos = Array.isArray(response.data) ? response.data : [];
const latestPhotoAt = response.latest_photo_at ?? latestRef.current ?? null;
latestRef.current = latestPhotoAt;
setData({
photos,
latestPhotoAt,
nextCursor: response.next_cursor ?? null,
});
setError(null);
} catch (err) {
if (!active) return;
setError(err instanceof Error ? err.message : 'Failed to load gallery updates');
} finally {
if (active) {
setLoading(false);
timer = window.setTimeout(poll, intervalMs);
}
}
};
poll();
return () => {
active = false;
if (timer) {
window.clearTimeout(timer);
}
};
}, [eventToken, intervalMs, options.locale]);
return { data, loading, error } as const;
}

View File

@@ -0,0 +1,57 @@
import React from 'react';
import { fetchEventStats } from '../services/statsApi';
import type { EventStats } from '../services/eventApi';
const defaultStats: EventStats = { onlineGuests: 0, tasksSolved: 0, latestPhotoAt: null };
export function usePollStats(eventToken: string | null, intervalMs = 10000) {
const [stats, setStats] = React.useState<EventStats>(defaultStats);
const [loading, setLoading] = React.useState<boolean>(Boolean(eventToken));
const [error, setError] = React.useState<string | null>(null);
React.useEffect(() => {
if (!eventToken) {
setStats(defaultStats);
setLoading(false);
setError(null);
return;
}
let active = true;
let timer: number | null = null;
const poll = async () => {
if (document.visibilityState === 'hidden') {
timer = window.setTimeout(poll, intervalMs);
return;
}
try {
setLoading(true);
const next = await fetchEventStats(eventToken);
if (!active) return;
setStats(next);
setError(null);
} catch (err) {
if (!active) return;
setError(err instanceof Error ? err.message : 'Failed to load stats');
} finally {
if (active) {
setLoading(false);
timer = window.setTimeout(poll, intervalMs);
}
}
};
poll();
return () => {
active = false;
if (timer) {
window.clearTimeout(timer);
}
};
}, [eventToken, intervalMs]);
return { stats, loading, error } as const;
}