1"use client";
2
3
14import { useEffect, useMemo, useRef, useState } from "react";
15import { realtime } from "@/lib/realtime";
16import {
17 computeOfflineStatus,
18 normalizePhase,
19 type ConnectionPhase,
20} from "@/lib/offline-status";
21import { useOutboxStore } from "@/stores/offlineQueue";
22
23export interface OfflineStatusView {
24 bannerVisible: boolean;
25 pendingCount: number;
26 failedCount: number;
27 showReviewLink: boolean;
28 chip: ConnectionPhase;
29 reconnectFlash: boolean;
30 ariaLabel: string;
31}
32
33export function useOfflineStatus(): OfflineStatusView {
34 const [online, setOnline] = useState<boolean>(() =>
35 typeof navigator !== "undefined" ? navigator.onLine : true
36 );
37 const [phase, setPhase] = useState<ConnectionPhase>(() => normalizePhase(realtime.connectionStatus));
38 const phaseSinceMs = useRef<number>(Date.now());
39 const [phaseSince, setPhaseSince] = useState<number>(phaseSinceMs.current);
40 const [nowMs, setNowMs] = useState<number>(() => Date.now());
41 const outboxPending = useOutboxStore((s) => s.pending);
42 const outboxFailed = useOutboxStore((s) => s.failed);
43
44
45
46 useEffect(() => {
47 const id = setInterval(() => setNowMs(Date.now()), 1_000);
48 return () => clearInterval(id);
49 }, []);
50
51 useEffect(() => {
52 if (typeof window === "undefined") return;
53 const onUp = () => setOnline(true);
54 const onDown = () => setOnline(false);
55 window.addEventListener("online", onUp);
56 window.addEventListener("offline", onDown);
57 return () => {
58 window.removeEventListener("online", onUp);
59 window.removeEventListener("offline", onDown);
60 };
61 }, []);
62
63 useEffect(() => {
64 const off = realtime.onStatus((status) => {
65 const next = normalizePhase(status);
66 setPhase((prev) => {
67 if (prev !== next) {
68 phaseSinceMs.current = Date.now();
69 setPhaseSince(phaseSinceMs.current);
70 }
71 return next;
72 });
73 });
74 return off;
75 }, []);
76
77 return useMemo(
78 () =>
79 computeOfflineStatus({
80 navigatorOnline: online,
81 phase,
82 phaseSinceMs: phaseSince,
83 nowMs,
84 crdtPending: 0,
85 outboxPending,
86 outboxFailed
87 }),
88 [online, phase, phaseSince, nowMs, outboxPending, outboxFailed]
89 );
90}
91