feat: implement tenant OAuth flow and guest achievements

This commit is contained in:
2025-09-25 08:32:37 +02:00
parent ef6203c603
commit b22d91ed32
84 changed files with 5984 additions and 1399 deletions

View File

@@ -8,6 +8,9 @@ export function usePollGalleryDelta(slug: string) {
const [newCount, setNewCount] = useState(0);
const latestAt = useRef<string | null>(null);
const timer = useRef<number | null>(null);
const [visible, setVisible] = useState(
typeof document !== 'undefined' ? document.visibilityState === 'visible' : true
);
async function fetchDelta() {
try {
@@ -75,16 +78,25 @@ export function usePollGalleryDelta(slug: string) {
}
}
useEffect(() => {
const onVis = () => setVisible(document.visibilityState === 'visible');
document.addEventListener('visibilitychange', onVis);
return () => document.removeEventListener('visibilitychange', onVis);
}, []);
useEffect(() => {
setLoading(true);
latestAt.current = null;
setPhotos([]);
fetchDelta();
timer.current = window.setInterval(fetchDelta, 30_000);
if (timer.current) window.clearInterval(timer.current);
// Poll less aggressively when hidden
const interval = visible ? 30_000 : 90_000;
timer.current = window.setInterval(fetchDelta, interval);
return () => {
if (timer.current) window.clearInterval(timer.current);
};
}, [slug]);
}, [slug, visible]);
function acknowledgeNew() { setNewCount(0); }
return { loading, photos, newCount, acknowledgeNew };

View File

@@ -1,39 +1,67 @@
import { useEffect, useRef, useState } from 'react';
type Stats = { onlineGuests: number; tasksSolved: number; latestPhotoAt?: string };
export type EventStats = {
onlineGuests: number;
tasksSolved: number;
latestPhotoAt: string | null;
};
export function usePollStats(slug: string) {
const [data, setData] = useState<Stats | null>(null);
type StatsResponse = {
online_guests?: number;
tasks_solved?: number;
latest_photo_at?: string;
};
export function usePollStats(slug: string | null | undefined) {
const [data, setData] = useState<EventStats>({ onlineGuests: 0, tasksSolved: 0, latestPhotoAt: null });
const [loading, setLoading] = useState(true);
const timer = useRef<number | null>(null);
const visible = typeof document !== 'undefined' ? document.visibilityState === 'visible' : true;
const [visible, setVisible] = useState(
typeof document !== 'undefined' ? document.visibilityState === 'visible' : true
);
async function fetchOnce() {
const canPoll = Boolean(slug);
async function fetchOnce(activeSlug: string) {
try {
const res = await fetch(`/api/v1/events/${encodeURIComponent(slug)}/stats`, {
const res = await fetch(`/api/v1/events/${encodeURIComponent(activeSlug)}/stats`, {
headers: { 'Cache-Control': 'no-store' },
});
if (res.status === 304) return;
const json = await res.json();
setData({ onlineGuests: json.online_guests ?? 0, tasksSolved: json.tasks_solved ?? 0, latestPhotoAt: json.latest_photo_at });
const json: StatsResponse = await res.json();
setData({
onlineGuests: json.online_guests ?? 0,
tasksSolved: json.tasks_solved ?? 0,
latestPhotoAt: json.latest_photo_at ?? null,
});
} finally {
setLoading(false);
}
}
useEffect(() => {
setLoading(true);
fetchOnce();
function schedule() {
if (!visible) return;
timer.current = window.setInterval(fetchOnce, 10_000);
const onVis = () => setVisible(document.visibilityState === 'visible');
document.addEventListener('visibilitychange', onVis);
return () => document.removeEventListener('visibilitychange', onVis);
}, []);
useEffect(() => {
if (!canPoll) {
setLoading(false);
return;
}
setLoading(true);
const activeSlug = String(slug);
fetchOnce(activeSlug);
if (timer.current) window.clearInterval(timer.current);
if (visible) {
timer.current = window.setInterval(() => fetchOnce(activeSlug), 10_000);
}
schedule();
return () => {
if (timer.current) window.clearInterval(timer.current);
};
}, [slug, visible]);
}, [slug, visible, canPoll]);
return { loading, onlineGuests: data?.onlineGuests ?? 0, tasksSolved: data?.tasksSolved ?? 0 };
return { ...data, loading };
}