// Hotel Walks — app shell, top-tab navigation, hash routing. // All screens mount inside . const { useState, useEffect, useMemo } = React; const { SmartKey, Wordmark, Lockup, tokens: HWT } = window.HW; const { Caps, Btn, Pill } = window.UI; /* ───────────────────────────────────────────────────────── Hash router — supports /, /app, /app/send, /app/incoming/:id, /app/letter/:id, /app/invoices, /guest/:id ───────────────────────────────────────────────────────── */ function useHashRoute() { const parse = () => { const raw = window.location.hash.replace(/^#/, "") || "/"; const [path, queryStr] = raw.split("?"); const parts = path.split("/").filter(Boolean); const query = {}; if (queryStr) queryStr.split("&").forEach(p => { const [k, v] = p.split("="); query[k] = decodeURIComponent(v || ""); }); return { path, parts, query, raw }; }; const [route, setRoute] = useState(parse()); useEffect(() => { const on = () => setRoute(parse()); window.addEventListener("hashchange", on); return () => window.removeEventListener("hashchange", on); }, []); return route; } function go(path) { window.location.hash = path; // scroll any internal scroller back to top const main = document.querySelector("[data-main-scroll]"); if (main) main.scrollTop = 0; } window.HWgo = go; /* ───────────────────────────────────────────────────────── Tweaks — light/dark mode (host-persisted) ───────────────────────────────────────────────────────── */ const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "theme": "light" }/*EDITMODE-END*/; function useTheme() { const [theme, setTheme] = useState(TWEAK_DEFAULTS.theme); useEffect(() => { document.documentElement.setAttribute("data-theme", theme); }, [theme]); return [theme, setTheme]; } /* ───────────────────────────────────────────────────────── Small stroke icons for the top nav tabs. ───────────────────────────────────────────────────────── */ function NavIcon({ kind, size = 18 }) { const stroke = "currentColor"; const sw = 1.6; const common = { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke, strokeWidth: sw, strokeLinecap: "square", strokeLinejoin: "round" }; switch (kind) { case "today": // calendar with current-day pip return ( ); case "send": // outbound paper plane return ( ); case "incoming": // inbox tray with downward arrow return ( ); case "letters": // document with lines return ( ); case "invoices": // receipt / ledger return ( ); default: return null; } } /* ───────────────────────────────────────────────────────── Top tab nav ───────────────────────────────────────────────────────── */ function TopNav({ active, onMarketing }) { const { ME } = window.HW_DATA; const [open, setOpen] = useState(false); const tabs = [ { id: "app", label: "Today", path: "/app", icon: "today" }, { id: "send", label: "Send", path: "/app/send", icon: "send" }, { id: "incoming", label: "Incoming", path: "/app/incoming", icon: "incoming", badge: window.HW_DATA.INCOMING.length }, { id: "letters", label: "Letters", path: "/app/letters", icon: "letters" }, { id: "invoices", label: "Invoices", path: "/app/invoices", icon: "invoices" }, ]; const activeTab = tabs.find(t => t.id === active); // Close drawer on route change useEffect(() => { setOpen(false); }, [active]); return (
{/* Top strip: lockup + property + user */}
Hotel Walks {/* Desktop right side */}
Property
{ME.property.name}
{ME.user.initials}
{/* Mobile hamburger */}
{/* Desktop tab strip */} {/* Mobile drawer */} {open && (
{/* Property + user */}
Property
{ME.property.name}
{ME.user.shift} shift
{ME.user.initials}
{/* Routes */} {tabs.map(t => { const isActive = active === t.id; return ( {t.label}
{t.badge > 0 && ( {t.badge} )} {isActive ? Now · : }
); })} {/* Footer: site link */}
)}
); } /* Mobile responsiveness — hide desktop nav, show hamburger + drawer */ const responsiveCSS = ` @media (max-width: 720px) { .property-meta { display: none; } .topbar { padding: 12px 18px !important; } .topbar-right { display: none !important; } .tab-strip { display: none !important; } .hamburger { display: inline-flex !important; } .mobile-drawer { display: block !important; } } `; /* ───────────────────────────────────────────────────────── Theme tweak panel ───────────────────────────────────────────────────────── */ function ThemeTweaks({ theme, setTheme }) { const { TweaksPanel, TweakSection, TweakRadio } = window; return ( { setTheme(v); window.parent.postMessage({ type: "__edit_mode_set_keys", edits: { theme: v } }, "*"); }} options={[ { label: "Bone", value: "light" }, { label: "Ink", value: "dark" }, ]} /> ); } /* ───────────────────────────────────────────────────────── Route resolver ───────────────────────────────────────────────────────── */ function Routes({ route }) { const p = route.parts; // / → marketing // /guest/:id → guest cancellation // /app → dashboard // /app/send → send walk request // /app/incoming → incoming queue // /app/incoming/:id → opportunity detail // /app/letters → letters list (just shows most recent) // /app/letter/:id → walk letter doc // /app/invoices → invoices if (p.length === 0) return ; if (p[0] === "guest") return ; if (p[0] === "app") { if (!p[1]) return ; if (p[1] === "send") return ; if (p[1] === "incoming" && !p[2])return ; if (p[1] === "incoming" && p[2]) return ; if (p[1] === "letters" && !p[2]) return ; if (p[1] === "letter" && p[2]) return ; if (p[1] === "invoices") return ; } return (

Not found

No screen matches {route.raw}.

go("/app")}>Back to today
); } /* ───────────────────────────────────────────────────────── Root ───────────────────────────────────────────────────────── */ function App() { const route = useHashRoute(); const [theme, setTheme] = useTheme(); // Scroll to top whenever the route changes, regardless of how navigation happened // (programmatic via go(), or a static link). // Use the path part so a hash change to the same screen with new ?query stays put. useEffect(() => { window.scrollTo(0, 0); const main = document.querySelector("[data-main-scroll]"); if (main) main.scrollTop = 0; }, [route.path]); // Active tab id const activeTab = useMemo(() => { const p = route.parts; if (p[0] !== "app") return null; if (!p[1]) return "app"; if (p[1] === "send") return "send"; if (p[1] === "incoming") return "incoming"; if (p[1] === "letter" || p[1] === "letters") return "letters"; if (p[1] === "invoices") return "invoices"; return null; }, [route]); const isMarketing = route.parts.length === 0; const isGuest = route.parts[0] === "guest"; const isLetter = route.parts[1] === "letter"; // Show chrome only inside /app const showChrome = !isMarketing && !isGuest; return (
{showChrome && go("/")} />}
{showChrome && !isLetter && }
); } // Wait until all screen modules registered before mounting. function mount() { if (!window.Screens || !window.Screens.Marketing) { setTimeout(mount, 30); return; } ReactDOM.createRoot(document.getElementById("root")).render(); } mount();