Skip to content
File

Blob: src/client/components/ui/banners.tsx

typescript73 lines
1import { Link, useLocation } from "@tanstack/react-router";
2import { SESSION_MODES } from "@/client/lib/constants";
3import { selectHasLocalSession, useAuthStore } from "@/client/stores/auth-store";
4import { useOnline } from "@/client/hooks/use-online";
5import { usePwaUpdate } from "@/client/lib/pwa";
6 
7export function Banners() {
8 const online = useOnline();
9 const sessionMode = useAuthStore((s) => s.sessionMode);
10 const refreshState = useAuthStore((s) => s.refreshState);
11 const hasLocalSession = useAuthStore(selectHasLocalSession);
12 const pathname = useLocation({ select: (l) => l.pathname });
13 const pwaUpdate = usePwaUpdate();
14 
15 const hasDegradedSession = sessionMode === SESSION_MODES.LOCAL_ONLY || sessionMode === SESSION_MODES.EXPIRED;
16 const showRestoring = refreshState === "refreshing" && hasDegradedSession;
17 const showExpired = sessionMode === SESSION_MODES.EXPIRED && !showRestoring;
18 // `Banners` is also mounted by `StandaloneLayout`, which covers anonymous
19 // surfaces (login, invite). Gate the PWA update prompt to local-session
20 // surfaces so unauthenticated users never see it.
21 const showPwaUpdate = hasLocalSession && pwaUpdate.waiting && pwaUpdate.apply !== null;
22 
23 return (
24 <>
25 {!online && (
26 <div
27 role="status"
28 aria-live="polite"
29 className="animate-slide-up border-b border-amber-500/20 bg-amber-500/10 py-1.5 text-center text-xs text-amber-400"
30 >
31 Offline. Your edits are saved locally and will sync when you're back.
32 </div>
33 )}
34 {showPwaUpdate && (
35 <div
36 role="status"
37 aria-live="polite"
38 className="animate-slide-up border-b border-accent-500/20 bg-accent-500/10 py-1.5 text-center text-xs text-accent-300"
39 >
40 A new version of bland is available.{" "}
41 <button type="button" onClick={() => pwaUpdate.apply?.()} className="underline hover:text-accent-200">
42 Reload to update
43 </button>
44 </div>
45 )}
46 {showRestoring && (
47 <div
48 role="status"
49 aria-live="polite"
50 className="animate-slide-up border-b border-zinc-700/60 bg-zinc-800/90 px-4 py-1.5 text-center text-xs text-zinc-300"
51 >
52 <span className="inline-flex items-center gap-2">
53 <span className="h-1.5 w-1.5 rounded-full bg-zinc-400 animate-pulse" aria-hidden="true" />
54 Restoring your session in the background.
55 </span>
56 </div>
57 )}
58 {showExpired && (
59 <div
60 role="alert"
61 className="animate-slide-up border-b border-red-500/20 bg-red-500/10 py-1.5 text-center text-xs text-red-400"
62 >
63 Session expired.{" "}
64 <Link to="/login" search={{ redirect: pathname }} className="underline hover:text-red-300">
65 Sign in
66 </Link>{" "}
67 to resume editing.
68 </div>
69 )}
70 </>
71 );
72}