- 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>
88 lines
3.4 KiB
TypeScript
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>
|
|
</>
|
|
);
|
|
}
|