88 行
4.0 KiB
TypeScript
88 行
4.0 KiB
TypeScript
import { useEffect } from "react";
|
|
import { Navigate, Outlet, useLocation } from "react-router-dom";
|
|
import { useAuth } from "../auth/AuthContext";
|
|
import { AppPwaUpdateNotice } from "../components/common/AppPwaUpdateNotice";
|
|
import { ErrorBoundary } from "../components/common/ErrorBoundary";
|
|
import { MobileBottomNav } from "../components/mobile/MobileBottomNav";
|
|
import { Sidebar } from "../components/navigation/Sidebar";
|
|
import { Topbar } from "../components/navigation/Topbar";
|
|
import { resolvePageMeta } from "../features/navigation/schema";
|
|
import { writeRecentVisit } from "../lib/recentVisits";
|
|
import { resolveMobileEquivalentPath, shouldPreferMobileWorkplace } from "../lib/runtimeContext";
|
|
|
|
export function AppShellLayout() {
|
|
const { permissions } = useAuth();
|
|
const location = useLocation();
|
|
const isMobileRoute = location.pathname.startsWith("/mobile");
|
|
const mobileSearchParams = new URLSearchParams(location.search);
|
|
const isMobileDeepRoute =
|
|
/^\/mobile\/(?:reimbursements|approvals|leave|purchase-requests)\/[^/]+$/.test(location.pathname) ||
|
|
(location.pathname === "/mobile/approvals" && mobileSearchParams.has("taskId"));
|
|
const showMobileBottomNav = isMobileRoute && !isMobileDeepRoute;
|
|
const mobileEquivalentPath = !isMobileRoute && shouldPreferMobileWorkplace()
|
|
? resolveMobileEquivalentPath(location.pathname, location.search)
|
|
: null;
|
|
|
|
useEffect(() => {
|
|
if (!isMobileRoute || typeof window === "undefined") {
|
|
return;
|
|
}
|
|
|
|
const updateViewportHeight = () => {
|
|
const height = Math.max(320, Math.floor(window.visualViewport?.height ?? window.innerHeight));
|
|
document.documentElement.style.setProperty("--mobile-viewport-height", `${height}px`);
|
|
};
|
|
|
|
updateViewportHeight();
|
|
const frameId = window.requestAnimationFrame(updateViewportHeight);
|
|
window.addEventListener("resize", updateViewportHeight);
|
|
window.addEventListener("orientationchange", updateViewportHeight);
|
|
window.visualViewport?.addEventListener("resize", updateViewportHeight);
|
|
window.visualViewport?.addEventListener("scroll", updateViewportHeight);
|
|
|
|
return () => {
|
|
window.cancelAnimationFrame(frameId);
|
|
window.removeEventListener("resize", updateViewportHeight);
|
|
window.removeEventListener("orientationchange", updateViewportHeight);
|
|
window.visualViewport?.removeEventListener("resize", updateViewportHeight);
|
|
window.visualViewport?.removeEventListener("scroll", updateViewportHeight);
|
|
};
|
|
}, [isMobileRoute]);
|
|
|
|
useEffect(() => {
|
|
const pathname = location.pathname;
|
|
if (!pathname || pathname === "/login" || pathname.startsWith("/mobile/")) {
|
|
return;
|
|
}
|
|
writeRecentVisit(pathname, resolvePageMeta(pathname, permissions).title, permissions);
|
|
}, [location.pathname, permissions]);
|
|
|
|
if (mobileEquivalentPath && mobileEquivalentPath !== `${location.pathname}${location.search}`) {
|
|
return <Navigate to={mobileEquivalentPath} replace />;
|
|
}
|
|
|
|
return (
|
|
<div className="app-shell relative flex min-h-screen overflow-hidden">
|
|
{isMobileRoute ? null : <Sidebar />}
|
|
|
|
<main
|
|
className={`app-shell-main flex min-w-0 flex-1 flex-col ${isMobileRoute ? "hyb-mobile-shell overflow-hidden" : "xl:ml-72"}`}
|
|
style={isMobileRoute ? { height: "var(--mobile-viewport-height, 100vh)", maxHeight: "var(--mobile-viewport-height, 100vh)", minHeight: "var(--mobile-viewport-height, 100vh)" } : undefined}
|
|
>
|
|
{isMobileRoute ? null : <Topbar />}
|
|
|
|
<div className={`flex-1 px-4 py-5 ${isMobileRoute ? `min-h-0 overscroll-contain overflow-y-auto px-[var(--mobile-page-x)] py-[var(--mobile-page-y)] ${showMobileBottomNav ? "pb-[calc(var(--mobile-bottom-nav-height,68px)+env(safe-area-inset-bottom,0px)+var(--mobile-page-y)+0.5rem)]" : "pb-5 sm:pb-6"}` : "sm:px-7 sm:py-7 lg:px-8"}`}>
|
|
<div className="layout-container max-w-none">
|
|
<AppPwaUpdateNotice />
|
|
<ErrorBoundary>
|
|
<Outlet />
|
|
</ErrorBoundary>
|
|
</div>
|
|
</div>
|
|
|
|
{showMobileBottomNav ? <MobileBottomNav /> : null}
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|