Files
server-26/drb-frontend/components/ChromeSwitcher.tsx
Logan CusanoandClaude Opus 5.5 afd2bc463d Live wall: saved views in the URL hash, per-viewer memory, kiosk mode (#234)
- liveWall.ts: WallLink carries layers (short stable keys), z/lat/lon,
  base, tourMs, kiosk and an f.<name> filter pass-through for #235;
  parse/serialise stay pure and round-trip; old inc/cam/tour links parse.
- MapView: overlays/basemap/viewport come from initialLayers/initialBase/
  initialView (defaults unchanged); onViewStateChange reports layer, base
  and region-view changes (debounced). Initially-checked overlays now start
  "shown" (react-leaflet adds a checked overlay before the control can fire
  overlayadd). Closing an incident flies back to the pre-drill view (#137).
- wallPrefs.ts: per-browser last layers/base/view in localStorage, every
  access wrapped; precedence hash > remembered > defaults.
- LiveView: one effect mirrors wall + map state into the hash; hash edits
  remount the map with the new view; Copy view link; configurable tour.
- Kiosk (?kiosk=1 or #kiosk): no nav, fullscreen button (auto on first
  touch), Screen Wake Lock re-acquired on visibility, reconnect banner
  (offline / useCalls error) with backoff reload, 18-20 px type via a
  data-kiosk attribute and Tailwind arbitrary variants.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:58:22 -04:00

88 lines
3.4 KiB
TypeScript

"use client";
import { useEffect } from "react";
import { usePathname, useRouter } from "next/navigation";
import { useAuth } from "@/components/AuthProvider";
import { Nav } from "@/components/Nav";
import { MarketingHeader } from "@/components/marketing/MarketingHeader";
import { MarketingFooter } from "@/components/marketing/MarketingFooter";
import { useKioskFlag } from "@/lib/useKiosk";
// Public marketing surface — exact paths, not prefixes, so e.g. /features/x
// (if it ever exists) doesn't accidentally get pulled into marketing chrome.
// Keep in sync with PUBLIC_PATHS in middleware.ts (that one decides whether
// to redirect at all; this one just picks page chrome).
const MARKETING_PATHS = new Set(["/", "/features", "/pricing", "/faq", "/terms", "/privacy"]);
// Pages a signed-in user with no org_id claim must still be able to reach —
// otherwise the redirect below would loop against itself, or lock someone
// out of the one screen (/onboarding) that fixes their account.
const NO_ORG_ALLOWED_PATHS = new Set(["/onboarding", "/login", "/signup", "/profile"]);
/**
* Picks page chrome by route: the public marketing pages get a full-bleed
* layout with their own header/footer, everything else (the authenticated
* app, including /login and /settings) keeps the existing app Nav + padded
* main container.
*
* Also carries AuthProvider's no-claim guard (SAAS_PLAN.md B3): a signed-in
* Firebase user with no org_id claim is a real session that is nonetheless
* provisioned into nothing — AuthProvider already refuses to set the
* drb_session cookie for them, so middleware.ts's redirect only covers
* "not signed in at all". This effect covers the other case: signed in, no
* org, anywhere in the app — send them to /onboarding rather than letting
* every page's data hooks fail open or silently return nothing.
*/
export function ChromeSwitcher({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
const { user, loading, orgId } = useAuth();
const router = useRouter();
const kiosk = useKioskFlag();
useEffect(() => {
if (loading) return;
if (!user) return; // not signed in — middleware.ts already routes this to /login
if (orgId) return;
if (MARKETING_PATHS.has(pathname) || NO_ORG_ALLOWED_PATHS.has(pathname)) return;
router.replace("/onboarding");
}, [loading, user, orgId, pathname, router]);
// "/" is marketing for a signed-out visitor, but the moment someone is
// signed in it's Live — the map is the home screen (UI_REDESIGN.md §3),
// not a marketing page. Every other marketing path stays marketing
// regardless of auth state.
const showMarketingChrome = MARKETING_PATHS.has(pathname) && !(pathname === "/" && user);
if (showMarketingChrome) {
return (
<>
<MarketingHeader />
{children}
<MarketingFooter />
</>
);
}
// Live ("/") is full-bleed under its own top bar, not the padded
// max-width container the rest of the app uses. In kiosk mode (?kiosk=1 or
// #kiosk, server-26#234) the wall gets the whole screen: no nav at all.
if (pathname === "/" && user && kiosk) {
return <main className="h-screen">{children}</main>;
}
if (pathname === "/" && user) {
return (
<>
<Nav />
<main className="h-[calc(100vh-3.75rem)]">{children}</main>
</>
);
}
return (
<>
<Nav />
<main className="max-w-screen-2xl mx-auto px-4 md:px-6 py-6">{children}</main>
</>
);
}