diff --git a/index.html b/index.html index 01da5bf..21294c6 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,7 @@ - + { - return ( - //@ts-ignore - - - - ); + return ; }; export default App; diff --git a/src/app/Layout.tsx b/src/app/Layout.tsx index 6202d09..8b391ad 100644 --- a/src/app/Layout.tsx +++ b/src/app/Layout.tsx @@ -1,102 +1,92 @@ import {NavLink, Outlet, useLocation} from "react-router-dom"; -import {useEffect, useMemo, useState} from "react"; +import {type FormEvent, useCallback, useEffect, useMemo, useState} from "react"; import {useAuth} from "../auth"; import {type NavLinkDef, resolveNavLinksByRole, toPath} from "./roleNavConfig.ts"; -import {Menu, X, LogIn, Search} from "lucide-react"; -import logoUrl from "../public/wine_graph_logo_1024x1024.png"; +import {LogIn, Menu, Search, X} from "lucide-react"; +import {likelyPathsForRole, prefetchPath, prefetchPaths} from "./routePrefetch.ts"; + +const linkClass = (isActive: boolean) => + [ + "mx-2 my-[2px] flex flex-col items-center justify-center rounded-[var(--radius-md)] h-14 text-[10px] uppercase tracking-[0.12em] transition-colors", + isActive + ? "bg-[color:var(--color-accent-soft)] text-[color:var(--color-accent)] ring-accent" + : "text-fg-muted hover:bg-[color:var(--color-muted)] hover:text-token", + ].join(" "); const Layout = () => { const {isAuthenticated, user, pos} = useAuth(); const [mobileOpen, setMobileOpen] = useState(false); const location = useLocation(); - // Retailer ID is NOT the Google user id. Prefer the POS merchant id when available, - // otherwise fall back to the role.id (which represents the retailer/merchant id in our user model). const retailerId = useMemo( - () => pos.token?.merchantId ?? user?.user?.role.id, - [pos.token?.merchantId, user?.user?.role.id] + () => user?.user?.role.value === "retailer" ? user.user.role.id : pos.token?.merchantId, + [user?.user?.role.id, user?.user?.role.value, pos.token?.merchantId] ); const producerId = user?.user.role.value === "producer" ? user?.user.role.id : ""; const role = user?.user.role.value; const links: NavLinkDef[] | undefined = useMemo(() => { - if (role === "retailer" && retailerId) { - return resolveNavLinksByRole(role, retailerId); - } - if (role === "producer" && producerId) { - return resolveNavLinksByRole(role, producerId); - } + if (role === "retailer" && retailerId) return resolveNavLinksByRole(role, retailerId); + if (role === "producer" && producerId) return resolveNavLinksByRole(role, producerId); return resolveNavLinksByRole("visitor", user?.user.role.id ?? ""); }, [retailerId, producerId, role, user?.user.role.id]); - // Dynamic profile path: route avatar to the correct profile page based on role const profilePath: string = useMemo(() => { - if (role === "retailer" && retailerId) { - return `/retailer/${retailerId}/profile`; - } - if (role === "producer") { - return `/producer/${producerId}/profile`; - } + if (role === "retailer" && retailerId) return `/retailer/${retailerId}/profile`; + if (role === "producer") return `/producer/${producerId}/profile`; return "/profile"; }, [role, retailerId, producerId]); - // Close mobile menu on route change useEffect(() => { setMobileOpen(false); }, [location.pathname]); - // Prevent body scroll when mobile menu is open useEffect(() => { - if (mobileOpen) { - document.body.style.overflow = "hidden"; - } else { - document.body.style.overflow = ""; - } + document.body.style.overflow = mobileOpen ? "hidden" : ""; return () => { document.body.style.overflow = ""; }; }, [mobileOpen]); - // Centralized auto-redirect: placeholder (user.id) -> definitive (role.id) - // useEffect(() => { - // if (!isAuthenticated) return; - // if (attachInFlight) return; // avoid redirecting mid-attach - // - // const googleId = user?.user?.id; - // const roleId = user?.user?.role?.id; - // if (!googleId || !roleId) return; - // if (googleId === roleId) return; - // - // const path = location.pathname; - // - // if (role === "producer") { - // const placeholder = `/producer/${googleId}/profile`; - // if (path === placeholder) { - // const target = `/producer/${roleId}/profile`; - // if (target !== path) navigate(target, { replace: true }); - // } - // } - // - // if (role === "retailer") { - // const placeholder = `/retailer/${googleId}/profile`; - // if (path === placeholder) { - // const target = `/retailer/${roleId}/profile`; - // if (target !== path) navigate(target, { replace: true }); - // } - // } - // }, [isAuthenticated, attachInFlight, role, user?.user?.id, user?.user?.role?.id, location.pathname, navigate]); + useEffect(() => { + const navCandidates = (links ?? []) + .map((l) => toPath(l)) + .filter((path) => path && path !== "#" && path !== location.pathname) + .slice(0, 4); + + const roleCandidates = likelyPathsForRole({ + role, + retailerId, + producerId, + }).filter((path) => path && path !== location.pathname); + + const candidates = Array.from(new Set([...roleCandidates, ...navCandidates])).slice(0, 6); + if (candidates.length === 0) return; + + const warm = () => prefetchPaths(candidates); + const ric = window.requestIdleCallback; + if (ric) { + const id = ric(warm, {timeout: 1200}); + return () => window.cancelIdleCallback?.(id); + } + + const timeout = window.setTimeout(warm, 300); + return () => window.clearTimeout(timeout); + }, [links, location.pathname, producerId, retailerId, role]); return (
- {/* Skip link */} - Skip - to main content + + Skip to main content + - {/* Slim header: left-side expanding search, logo on the right */} -
-
+
+
+
- {/* Desktop (sm+): icon-only until hover/focus, then expands */} +
e.preventDefault()} > -
- {/* Right: profile/sign-in (mobile) + logo */} +
- {/* Mobile/sm-only: show profile/avatar since left rail is hidden */} { > {isAuthenticated ? ( user?.user?.picture ? ( - - - + Wine Graph + Wine Graph
-
- {/* Body: sidebar + main */}
- {/* Sidebar (md+) — icon-only rail (stickies under the 48px header) */} - {/* Main content */}
-
- - {/* Main data region for pages (tables/charts/lists) */} -
- -
+
+
- {/* Mobile nav drawer */} {mobileOpen && (
setMobileOpen(false)}/> -