hooks.ts
2.4 kB · typescript · 109 lines
1import { useCallback, useSyncExternalStore } from "react";2import { get as getMode, none, subscribe as onMode } from "../client/mode.ts";3import type { Primary } from "./shop.ts";45/* NOW */67const ticks = new Set<() => void>();89let now = 0;1011let timer: ReturnType<typeof setInterval> | null = null;1213function beat() {14 now = Date.now();15 for (const fn of ticks) fn();16}1718function run() {19 if (timer) clearInterval(timer);20 timer = null;21 if (document.hidden || !ticks.size) return;22 beat();23 timer = setInterval(beat, 1000);24}2526function onTick(fn: () => void) {27 ticks.add(fn);28 if (ticks.size === 1) {29 document.addEventListener("visibilitychange", run);30 run();31 }32 return () => {33 ticks.delete(fn);34 if (ticks.size) return;35 document.removeEventListener("visibilitychange", run);36 run();37 };38}3940const nowSnapshot = () => now;4142const nowServer = () => 0;4344export function useNow(initial: number) {45 return useSyncExternalStore(onTick, nowSnapshot, nowServer) || initial;46}4748/* MEDIA */4950export function useMediaQuery(query: string) {51 const subscribe = useCallback(52 (fn: () => void) => {53 const media = matchMedia(query);54 media.addEventListener("change", fn);55 return () => media.removeEventListener("change", fn);56 },57 [query],58 );59 return useSyncExternalStore(60 subscribe,61 useCallback(() => matchMedia(query).matches, [query]),62 () => false,63 );64}6566/* SEARCH */6768const NAV = "cm:nav";6970export const bump = () => dispatchEvent(new Event(NAV));7172function onNav(fn: () => void) {73 addEventListener("popstate", fn);74 addEventListener(NAV, fn);75 return () => {76 removeEventListener("popstate", fn);77 removeEventListener(NAV, fn);78 };79}8081const searchSnapshot = () => location.search;8283const searchServer = () => "";8485export const useSearch = () => useSyncExternalStore(onNav, searchSnapshot, searchServer);8687/* PRIMARY */8889const pickers = new Set<() => void>();9091let picked: Primary | null = null;9293export function pickPrimary(next: Primary) {94 picked = next;95 for (const fn of pickers) fn();96}9798function onPick(fn: () => void) {99 pickers.add(fn);100 return () => void pickers.delete(fn);101}102103const pickedSnapshot = () => picked;104105export function usePrimary(initial: Primary): Primary {106 const chosen = useSyncExternalStore(onPick, pickedSnapshot, none);107 const mode = useSyncExternalStore(onMode, getMode, none);108 return chosen ?? mode ?? initial;109}