feat: global SiteHeader avec user menu (login/inscription, Mes réservations, Espace hôte, Admin)
All checks were successful
CI / test (pull_request) Successful in 2m9s
All checks were successful
CI / test (pull_request) Successful in 2m9s
This commit is contained in:
parent
4e8b88ab34
commit
3bc52b2b60
5 changed files with 149 additions and 0 deletions
|
|
@ -4,6 +4,7 @@ import "./globals.css";
|
|||
import { PluginProvider } from "@/lib/plugins/client";
|
||||
import { getEnabledPluginKeys, syncPluginsFromRegistry } from "@/lib/plugins/server";
|
||||
import { SeasonBanner } from "@/components/SeasonBanner";
|
||||
import { SiteHeaderGuard } from "@/components/SiteHeaderGuard";
|
||||
import { LocaleProvider } from "@/lib/i18n/client";
|
||||
import { dict, getLocale } from "@/lib/i18n/server";
|
||||
|
||||
|
|
@ -102,6 +103,7 @@ export default async function RootLayout({
|
|||
<PluginProvider enabledKeys={enabledKeys}>
|
||||
<LocaleProvider locale={locale} messages={messages}>
|
||||
<SeasonBanner />
|
||||
<SiteHeaderGuard />
|
||||
{children}
|
||||
</LocaleProvider>
|
||||
</PluginProvider>
|
||||
|
|
|
|||
19
src/components/SignOutButton.tsx
Normal file
19
src/components/SignOutButton.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { signOut } from "@/auth";
|
||||
|
||||
export function SignOutButton() {
|
||||
return (
|
||||
<form
|
||||
action={async () => {
|
||||
"use server";
|
||||
await signOut({ redirectTo: "/" });
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="submit"
|
||||
className="text-xs text-zinc-500 hover:text-zinc-900"
|
||||
>
|
||||
Se déconnecter
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
79
src/components/SiteHeader.tsx
Normal file
79
src/components/SiteHeader.tsx
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
/**
|
||||
* Header global affiché sur toutes les pages PUBLIQUES (hors /admin qui a son
|
||||
* propre shell). Charge la session côté serveur pour adapter les liens.
|
||||
*/
|
||||
|
||||
import Link from "next/link";
|
||||
|
||||
import { auth } from "@/auth";
|
||||
import { UserRole } from "@/generated/prisma/enums";
|
||||
|
||||
import { SignOutButton } from "./SignOutButton";
|
||||
|
||||
export async function SiteHeader() {
|
||||
const session = await auth();
|
||||
const u = session?.user;
|
||||
const isAdmin = u?.role === UserRole.ADMIN;
|
||||
const isOwner = u?.role === UserRole.OWNER || isAdmin;
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-30 border-b border-zinc-200 bg-white/85 backdrop-blur supports-[backdrop-filter]:bg-white/70">
|
||||
<div className="mx-auto flex h-12 max-w-7xl items-center justify-between gap-4 px-4">
|
||||
<Link href="/" className="flex items-center gap-2 text-base font-semibold text-zinc-900">
|
||||
<span className="inline-flex h-6 w-6 items-center justify-center rounded-md bg-emerald-600 text-xs font-bold text-white">
|
||||
K
|
||||
</span>
|
||||
Karbé
|
||||
</Link>
|
||||
|
||||
<nav className="hidden items-center gap-5 text-sm text-zinc-700 sm:flex">
|
||||
<Link href="/carbets" className="hover:text-zinc-900">
|
||||
Carbets
|
||||
</Link>
|
||||
<Link href="/comment-ca-marche" className="hover:text-zinc-900">
|
||||
Comment ça marche
|
||||
</Link>
|
||||
<Link href="/pour-comites-entreprise" className="hover:text-zinc-900">
|
||||
Comités d'entreprise
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
<div className="flex items-center gap-3 text-sm">
|
||||
{u ? (
|
||||
<>
|
||||
<Link href="/mes-reservations" className="hidden text-zinc-700 hover:text-zinc-900 sm:inline">
|
||||
Mes réservations
|
||||
</Link>
|
||||
{isOwner ? (
|
||||
<Link href="/espace-hote" className="hidden text-zinc-700 hover:text-zinc-900 sm:inline">
|
||||
Espace hôte
|
||||
</Link>
|
||||
) : null}
|
||||
{isAdmin ? (
|
||||
<Link href="/admin" className="hidden rounded-md bg-zinc-900 px-2.5 py-1 text-xs font-semibold text-white hover:bg-zinc-800 sm:inline-block">
|
||||
Admin
|
||||
</Link>
|
||||
) : null}
|
||||
<span className="hidden max-w-[14ch] truncate text-xs text-zinc-500 md:inline" title={u.email ?? ""}>
|
||||
{u.name || u.email}
|
||||
</span>
|
||||
<SignOutButton />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Link href="/connexion" className="text-zinc-700 hover:text-zinc-900">
|
||||
Connexion
|
||||
</Link>
|
||||
<Link
|
||||
href="/inscription"
|
||||
className="rounded-md bg-zinc-900 px-3 py-1 text-xs font-semibold text-white hover:bg-zinc-800"
|
||||
>
|
||||
Créer un compte
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
26
src/components/SiteHeaderGuard.tsx
Normal file
26
src/components/SiteHeaderGuard.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* N'affiche le SiteHeader QUE sur les pages publiques.
|
||||
* Sur /admin, le shell admin a déjà sa propre TopBar + Sidebar.
|
||||
* Sur /connexion et /inscription, on garde la page nue.
|
||||
*/
|
||||
|
||||
import { headers } from "next/headers";
|
||||
|
||||
import { SiteHeader } from "./SiteHeader";
|
||||
|
||||
export async function SiteHeaderGuard() {
|
||||
const h = await headers();
|
||||
// Next.js 16 expose le pathname via le header x-pathname si on l'a posé,
|
||||
// sinon on retombe sur next-url ou referer. On utilise une heuristique simple :
|
||||
// pathname depuis x-invoke-path (Next internal) ou x-next-url-path-prefix.
|
||||
const pathname =
|
||||
h.get("x-pathname") ??
|
||||
h.get("x-invoke-path") ??
|
||||
h.get("next-url") ??
|
||||
"";
|
||||
|
||||
if (pathname.startsWith("/admin")) return null;
|
||||
if (pathname === "/connexion" || pathname === "/inscription") return null;
|
||||
|
||||
return <SiteHeader />;
|
||||
}
|
||||
23
src/middleware.ts
Normal file
23
src/middleware.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
* Middleware Karbé.
|
||||
*
|
||||
* Pose `x-pathname` sur tous les requests pour que les server components puissent
|
||||
* lire le path courant via `headers()` (utile pour SiteHeaderGuard qui décide
|
||||
* de rendre ou non le header global selon /admin vs reste).
|
||||
*/
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import type { NextRequest } from "next/server";
|
||||
|
||||
export function middleware(request: NextRequest) {
|
||||
const response = NextResponse.next();
|
||||
response.headers.set("x-pathname", request.nextUrl.pathname);
|
||||
return response;
|
||||
}
|
||||
|
||||
export const config = {
|
||||
// Exclut les assets statiques + API auth (qu'on ne veut pas modifier).
|
||||
matcher: [
|
||||
"/((?!_next/static|_next/image|favicon.ico|api/auth|api/health|api/metrics).*)",
|
||||
],
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue