"use client"; import { useEffect, useEffectEvent, useState } from "react"; import { PRESENCE_HEARTBEAT_MS } from "@/lib/presence-constants"; type VisitorCounts = { global: number; guide: number | null; byGuide: Record; }; export type EditorsByGuide = Record< string, Array<{ userId: string; name: string; sections: string[] }> >; const visitorIdStorageKey = "buzz-sheet:visitor-id"; let currentTabId: string | undefined; let fallbackVisitorId: string | undefined; function tabId(): string { currentTabId ??= crypto.randomUUID(); return currentTabId; } function visitorId(): string { try { const existing = window.localStorage.getItem(visitorIdStorageKey); if (existing) return existing; const created = crypto.randomUUID(); window.localStorage.setItem(visitorIdStorageKey, created); return created; } catch { fallbackVisitorId ??= crypto.randomUUID(); return fallbackVisitorId; } } function useHeartbeat({ endpoint, guideId, section, identifyBy, }: { endpoint: string; guideId: string | null; section?: string | null; identifyBy: "browser" | "tab"; }) { const [value, setValue] = useState(null); const send = useEffectEvent(async (active = true) => { try { const response = await fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ tabId: identifyBy === "browser" ? visitorId() : tabId(), guideId, ...(section !== undefined ? { section } : {}), active, }), cache: "no-store", keepalive: !active, }); if (!response.ok) throw new Error("presence unavailable"); if (active) setValue(await response.json() as T); } catch { if (active) setValue(null); } }); useEffect(() => { const immediate = window.setTimeout(() => void send(), 0); const interval = window.setInterval(() => void send(), PRESENCE_HEARTBEAT_MS); const refresh = () => { if (document.visibilityState === "visible") void send(); }; const leave = () => { if (identifyBy === "tab") void send(false); }; window.addEventListener("focus", refresh); window.addEventListener("pagehide", leave); document.addEventListener("visibilitychange", refresh); return () => { window.clearInterval(interval); window.clearTimeout(immediate); window.removeEventListener("focus", refresh); window.removeEventListener("pagehide", leave); document.removeEventListener("visibilitychange", refresh); }; }, [endpoint, guideId, identifyBy, section]); return value; } export function useVisitorPresence(guideId: string | null) { return useHeartbeat({ endpoint: "/api/presence/visitors", guideId, identifyBy: "browser", }); } export function useAdminPresence(guideId: string | null, section: string | null = null) { return useHeartbeat<{ editorsByGuide: EditorsByGuide }>({ endpoint: "/api/presence/admin", guideId, section, identifyBy: "tab", }); }