From 63eb8ed9319a22cd4bdb92ef97002f18a383cb88 Mon Sep 17 00:00:00 2001 From: DEls Date: Fri, 11 Sep 2026 23:02:30 +0200 Subject: [PATCH] feat(site-scope): load the allowed site list in the dashboard layout --- src/app/dashboard/layout.tsx | 22 ++++++++--- src/components/layout/site-scope-provider.tsx | 37 +++++++++++++++++++ 2 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 src/components/layout/site-scope-provider.tsx diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index 01dd9d6..75307d4 100644 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -1,13 +1,25 @@ import { Sidebar } from "@/components/layout/sidebar"; import { AuthGate } from "@/components/auth-gate"; +import { SiteScopeProvider } from "@/components/layout/site-scope-provider"; +import { getSiteScope, scopeFallbackValue } from "@/lib/site-scope"; + +export const dynamic = "force-dynamic"; + +export default async function DashboardLayout({ children }: { children: React.ReactNode }) { + // No searchParams here — layouts cannot read them and do not re-render + // on navigation. This call supplies `allowed` (stable for the session) + // and the cookie-resolved default; the switcher reads the live value + // from the URL itself. + const scope = await getSiteScope(); -export default function DashboardLayout({ children }: { children: React.ReactNode }) { return ( -
- -
{children}
-
+ +
+ +
{children}
+
+
); } diff --git a/src/components/layout/site-scope-provider.tsx b/src/components/layout/site-scope-provider.tsx new file mode 100644 index 0000000..c0523c1 --- /dev/null +++ b/src/components/layout/site-scope-provider.tsx @@ -0,0 +1,37 @@ +"use client"; + +import * as React from "react"; +import type { ScopeSite } from "@/lib/site-scope"; + +interface SiteScopeContextValue { + /** Every site the user may see. Loaded once by the dashboard layout. */ + allowed: ScopeSite[]; + /** + * What to show when the URL carries no ?site= — the cookie-resolved + * default. Deliberately NOT the active scope: active always comes + * from useSearchParams() so the control's state is the URL. + */ + fallbackValue: string; +} + +const SiteScopeContext = React.createContext( + undefined +); + +export function SiteScopeProvider({ + allowed, + fallbackValue, + children, +}: SiteScopeContextValue & { children: React.ReactNode }) { + const value = React.useMemo( + () => ({ allowed, fallbackValue }), + [allowed, fallbackValue] + ); + return {children}; +} + +export function useSiteScope() { + const ctx = React.useContext(SiteScopeContext); + if (!ctx) throw new Error("useSiteScope must be used inside SiteScopeProvider"); + return ctx; +}