Commit 99552c8c by krds-arun

perf(panel): server layout shell for faster navigation

Move interactive panel chrome into a small client shell and keep the panel layout as a server component. Also remove unnecessary client directives from module dashboards and module menu to reduce hydration and improve route transitions.

Made-with: Cursor
parent 275a55d5
"use client";
import { useRouter } from "next/navigation";
import { useEffect, useMemo, useState } from "react";
import { Sidebar } from "@/components/layout/Sidebar/Sidebar";
import { Header } from "@/components/layout/Header/Header";
import { Footer } from "@/components/layout/Footer/Footer";
import { BottomTab } from "@/components/layout/BottomTab/BottomTab";
import { NotificationsPanel } from "@/components/widgets/NotificationsPanel/NotificationsPanel";
import { ProfilePanelHeader } from "@/components/widgets/ProfilePanelHeader/ProfilePanelHeader";
import { QuickAction } from "@/components/widgets/QuickAction/QuickAction";
import { LocationSelector } from "@/components/widgets/LocationSelector/LocationSelector";
import { PanelModuleSearch } from "@/components/widgets/PanelModuleSearch/PanelModuleSearch";
import { panelNotificationSections } from "./panel-notifications";
import React from "react";
import { PanelShellClient } from "@/components/panel/shell/PanelShellClient";
import "./panel.css";
export default function PanelLayout({
children,
}: {
children: React.ReactNode;
}) {
const router = useRouter();
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
const isInteractiveTarget = useMemo(() => {
return (target: EventTarget | null) => {
if (!(target instanceof Element)) return false;
return Boolean(target.closest("a, button, input, textarea, select, [role='button']"));
};
}, []);
useEffect(() => {
let startX = 0;
let startY = 0;
let tracking = false;
const onTouchStart = (event: TouchEvent) => {
if (mobileSidebarOpen) return;
const touch = event.touches[0];
if (!touch) return;
// Left-edge swipe zone (tablet + mobile friendly)
if (touch.clientX > 32) return;
startX = touch.clientX;
startY = touch.clientY;
tracking = true;
};
const onTouchMove = (event: TouchEvent) => {
if (!tracking) return;
const touch = event.touches[0];
if (!touch) return;
const dx = touch.clientX - startX;
const dy = touch.clientY - startY;
if (dx > 64 && Math.abs(dy) < 42) {
tracking = false;
const useDrawer = window.matchMedia("(max-width: 1024px)").matches;
if (useDrawer) {
setMobileSidebarOpen(true);
} else if (sidebarCollapsed) {
setSidebarCollapsed(false);
}
}
};
const onTouchEnd = () => {
tracking = false;
};
window.addEventListener("touchstart", onTouchStart, { passive: true });
window.addEventListener("touchmove", onTouchMove, { passive: true });
window.addEventListener("touchend", onTouchEnd, { passive: true });
window.addEventListener("touchcancel", onTouchEnd, { passive: true });
return () => {
window.removeEventListener("touchstart", onTouchStart);
window.removeEventListener("touchmove", onTouchMove);
window.removeEventListener("touchend", onTouchEnd);
window.removeEventListener("touchcancel", onTouchEnd);
};
}, [mobileSidebarOpen, sidebarCollapsed]);
const panelHeaderRight = (
<>
<LocationSelector />
<NotificationsPanel
count={5}
sections={panelNotificationSections}
onMarkAllRead={() => {}}
onViewAll={() => {}}
onGoToPage={() => {}}
onForward={() => {}}
/>
<ProfilePanelHeader
name="Nino Lester Cruz"
role="ADMIN"
initials="NL"
onSignOut={() => router.push("/")}
/>
</>
);
return (
<div className={`maf-panel ${sidebarCollapsed ? "maf-panel--sidebar-collapsed" : ""}`.trim()}>
<aside
className="maf-panel__sidebarRail"
onClick={(event) => {
if (!sidebarCollapsed) return;
if (isInteractiveTarget(event.target)) return;
setSidebarCollapsed(false);
}}
aria-label="Sidebar rail"
>
<Sidebar onLogout={() => router.push("/")} />
</aside>
<div className="maf-panel__main">
<Header
hideNavMenu
hideActions
logoSrc="/maf-logo.png"
centerSlot={<PanelModuleSearch />}
panelRight={panelHeaderRight}
/>
<button
type="button"
className="maf-panel__mobileSidebarButton"
onClick={() => setMobileSidebarOpen(true)}
aria-label="Open sidebar"
>
</button>
<button
type="button"
className="maf-panel__sidebarToggle"
onClick={() => setSidebarCollapsed((value) => !value)}
aria-label={sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}
>
{sidebarCollapsed ? "›" : "‹"}
</button>
<main className="maf-panel__content">
<div className="maf-panel__contentInner">{children}</div>
</main>
<Footer
logoSrc="/panel-monogram-logo.png"
hideLinkGroups
panelBar
supportCtaLabel="Support"
supportCtaHref="#"
legalLinks={[
{ label: "Privacy Policy", href: "#" },
{ label: "Terms of Use", href: "#" },
{ label: "Child Privacy Policy", href: "#" },
]}
copyright="Gateway© 2020 - 2026 · Majid Al Futtaim"
/>
</div>
{mobileSidebarOpen ? (
<>
<button
type="button"
className="maf-panel__mobileSidebarBackdrop"
aria-label="Close sidebar backdrop"
onClick={() => setMobileSidebarOpen(false)}
/>
<aside className="maf-panel__mobileSidebarDrawer">
<button
type="button"
className="maf-panel__mobileSidebarClose"
onClick={() => setMobileSidebarOpen(false)}
aria-label="Close sidebar"
>
×
</button>
<Sidebar onLogout={() => router.push("/")} />
</aside>
</>
) : null}
<BottomTab />
<QuickAction />
</div>
);
return <PanelShellClient>{children}</PanelShellClient>;
}
\ No newline at end of file
"use client";
import React from "react";
import { ModuleCard } from "@/components/panel/modules/ModuleCard/ModuleCard";
import { ModuleDashboardMenu } from "@/components/panel/modules/ModuleDashboardMenu/ModuleDashboardMenu";
......
"use client";
import React from "react";
import { TopStatCard } from "@/components/panel/dashboard/TopStatCard/TopStatCard";
import { DashboardCard } from "@/components/panel/dashboard/DashboardCard/DashboardCard";
......
"use client";
import React from "react";
import { TopStatCard } from "@/components/panel/dashboard/TopStatCard/TopStatCard";
import { DashboardCard } from "@/components/panel/dashboard/DashboardCard/DashboardCard";
......
"use client";
import React from "react";
import { TopStatCard } from "@/components/panel/dashboard/TopStatCard/TopStatCard";
import { DualLineTrendChart } from "@/components/shared/charts/DualLineTrendChart/DualLineTrendChart";
......
"use client";
import React from "react";
import { TopStatCard } from "@/components/panel/dashboard/TopStatCard/TopStatCard";
import { DashboardCard } from "@/components/panel/dashboard/DashboardCard/DashboardCard";
......
"use client";
import Link from "next/link";
import React from "react";
import "./module-dashboard-menu.css";
......
"use client";
import React from "react";
import { TopStatCard } from "@/components/panel/dashboard/TopStatCard/TopStatCard";
import { DashboardCard } from "@/components/panel/dashboard/DashboardCard/DashboardCard";
......
"use client";
import { useRouter } from "next/navigation";
import React, { useEffect, useMemo, useState } from "react";
import { Sidebar } from "@/components/layout/Sidebar/Sidebar";
import { Header } from "@/components/layout/Header/Header";
import { Footer } from "@/components/layout/Footer/Footer";
import { BottomTab } from "@/components/layout/BottomTab/BottomTab";
import { NotificationsPanel } from "@/components/widgets/NotificationsPanel/NotificationsPanel";
import { ProfilePanelHeader } from "@/components/widgets/ProfilePanelHeader/ProfilePanelHeader";
import { QuickAction } from "@/components/widgets/QuickAction/QuickAction";
import { LocationSelector } from "@/components/widgets/LocationSelector/LocationSelector";
import { PanelModuleSearch } from "@/components/widgets/PanelModuleSearch/PanelModuleSearch";
import { panelNotificationSections } from "@/app/panel/panel-notifications";
export function PanelShellClient({ children }: { children: React.ReactNode }) {
const router = useRouter();
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
const isInteractiveTarget = useMemo(() => {
return (target: EventTarget | null) => {
if (!(target instanceof Element)) return false;
return Boolean(target.closest("a, button, input, textarea, select, [role='button']"));
};
}, []);
useEffect(() => {
let startX = 0;
let startY = 0;
let tracking = false;
const onTouchStart = (event: TouchEvent) => {
if (mobileSidebarOpen) return;
const touch = event.touches[0];
if (!touch) return;
if (touch.clientX > 32) return;
startX = touch.clientX;
startY = touch.clientY;
tracking = true;
};
const onTouchMove = (event: TouchEvent) => {
if (!tracking) return;
const touch = event.touches[0];
if (!touch) return;
const dx = touch.clientX - startX;
const dy = touch.clientY - startY;
if (dx > 64 && Math.abs(dy) < 42) {
tracking = false;
const useDrawer = window.matchMedia("(max-width: 1024px)").matches;
if (useDrawer) {
setMobileSidebarOpen(true);
} else if (sidebarCollapsed) {
setSidebarCollapsed(false);
}
}
};
const onTouchEnd = () => {
tracking = false;
};
window.addEventListener("touchstart", onTouchStart, { passive: true });
window.addEventListener("touchmove", onTouchMove, { passive: true });
window.addEventListener("touchend", onTouchEnd, { passive: true });
window.addEventListener("touchcancel", onTouchEnd, { passive: true });
return () => {
window.removeEventListener("touchstart", onTouchStart);
window.removeEventListener("touchmove", onTouchMove);
window.removeEventListener("touchend", onTouchEnd);
window.removeEventListener("touchcancel", onTouchEnd);
};
}, [mobileSidebarOpen, sidebarCollapsed]);
const panelHeaderRight = (
<>
<LocationSelector />
<NotificationsPanel
count={5}
sections={panelNotificationSections}
onMarkAllRead={() => {}}
onViewAll={() => {}}
onGoToPage={() => {}}
onForward={() => {}}
/>
<ProfilePanelHeader
name="Nino Lester Cruz"
role="ADMIN"
initials="NL"
onSignOut={() => router.push("/")}
/>
</>
);
return (
<div className={`maf-panel ${sidebarCollapsed ? "maf-panel--sidebar-collapsed" : ""}`.trim()}>
<aside
className="maf-panel__sidebarRail"
onClick={(event) => {
if (!sidebarCollapsed) return;
if (isInteractiveTarget(event.target)) return;
setSidebarCollapsed(false);
}}
aria-label="Sidebar rail"
>
<Sidebar onLogout={() => router.push("/")} />
</aside>
<div className="maf-panel__main">
<Header
hideNavMenu
hideActions
logoSrc="/maf-logo.png"
centerSlot={<PanelModuleSearch />}
panelRight={panelHeaderRight}
/>
<button
type="button"
className="maf-panel__mobileSidebarButton"
onClick={() => setMobileSidebarOpen(true)}
aria-label="Open sidebar"
>
</button>
<button
type="button"
className="maf-panel__sidebarToggle"
onClick={() => setSidebarCollapsed((value) => !value)}
aria-label={sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}
>
{sidebarCollapsed ? "›" : "‹"}
</button>
<main className="maf-panel__content">
<div className="maf-panel__contentInner">{children}</div>
</main>
<Footer
logoSrc="/panel-monogram-logo.png"
hideLinkGroups
panelBar
supportCtaLabel="Support"
supportCtaHref="#"
legalLinks={[
{ label: "Privacy Policy", href: "#" },
{ label: "Terms of Use", href: "#" },
{ label: "Child Privacy Policy", href: "#" },
]}
copyright="Gateway© 2020 - 2026 · Majid Al Futtaim"
/>
</div>
{mobileSidebarOpen ? (
<>
<button
type="button"
className="maf-panel__mobileSidebarBackdrop"
aria-label="Close sidebar backdrop"
onClick={() => setMobileSidebarOpen(false)}
/>
<aside className="maf-panel__mobileSidebarDrawer">
<button
type="button"
className="maf-panel__mobileSidebarClose"
onClick={() => setMobileSidebarOpen(false)}
aria-label="Close sidebar"
>
×
</button>
<Sidebar onLogout={() => router.push("/")} />
</aside>
</>
) : null}
<BottomTab />
<QuickAction />
</div>
);
}
"use client";
import React from "react";
import { TopStatCard } from "@/components/panel/dashboard/TopStatCard/TopStatCard";
import { DashboardCard } from "@/components/panel/dashboard/DashboardCard/DashboardCard";
......
"use client";
import React from "react";
import { TopStatCard } from "@/components/panel/dashboard/TopStatCard/TopStatCard";
import { DashboardCard } from "@/components/panel/dashboard/DashboardCard/DashboardCard";
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment