last update
CI / quality (push) Waiting to run

This commit is contained in:
MOH
2026-07-14 20:10:38 +02:00
parent cc6db94977
commit 1a7b3397f6
14 changed files with 847 additions and 146 deletions
+33 -4
View File
@@ -1,7 +1,7 @@
"use client";
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
import { Menu, X } from "lucide-react";
import { Menu, ShieldCheck, X } from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useLocale, useTranslations } from "next-intl";
@@ -13,6 +13,7 @@ import { SiteLogo } from "@/components/layout/site-logo";
import { SoundToggle } from "@/components/sound-toggle";
import { ThemeToggle } from "@/components/theme-toggle";
import { Button } from "@/components/ui/button";
import { buildAdminUrl } from "@/lib/admin-routing";
import { getLocalizedPath, stripLocalePrefix } from "@/lib/locale";
import { toast } from "@/lib/toast";
import { cn } from "@/lib/utils";
@@ -35,6 +36,13 @@ type SiteHeaderProps = {
lightLogoUrl?: string | null;
darkLogoUrl?: string | null;
defaultLocale: "de" | "en" | "ar";
/**
* Server-computed via `isSuperAdmin()` in the parent layout — this client
* component only decides whether to *render* the Admin shortcut link from
* this flag. It performs no auth check itself and the flag grants no
* access; the real guard lives on each admin page/action server-side.
*/
isSuperAdmin?: boolean;
};
function getDirectionalReveal(direction: "rtl" | "ltr") {
@@ -146,6 +154,7 @@ export function SiteHeader({
lightLogoUrl,
darkLogoUrl,
defaultLocale,
isSuperAdmin = false,
}: SiteHeaderProps) {
const [isOpen, setIsOpen] = useState(false);
const [isScrolled, setIsScrolled] = useState(false);
@@ -168,6 +177,11 @@ export function SiteHeader({
"h-9 w-9 rounded-pill border border-transparent p-0 text-foreground/80 hover:bg-accent hover:text-foreground";
const mobileControlButtonClassName =
"h-10 w-10 rounded-pill border border-border/70 bg-background text-foreground/80 shadow-xs hover:bg-accent hover:text-foreground";
const adminButtonClassName =
"inline-flex h-9 items-center gap-1.5 rounded-pill border border-border/70 bg-background/80 px-3 text-sm font-medium text-foreground/80 backdrop-blur-chrome transition-colors hover:bg-accent hover:text-foreground";
const mobileAdminButtonClassName =
"flex h-10 w-10 items-center justify-center rounded-pill border border-border/70 bg-background text-foreground/80 shadow-xs transition-colors hover:bg-accent hover:text-foreground";
const adminHref = buildAdminUrl("/");
useEffect(() => {
const handleScroll = () => {
@@ -201,7 +215,7 @@ export function SiteHeader({
};
return (
<header className="fixed inset-x-0 top-3 z-40 bg-transparent">
<motion.header layoutRoot className="fixed inset-x-0 top-3 z-40 bg-transparent">
<Container className="max-w-[88rem] px-5 sm:px-6 lg:px-8">
<div className="relative grid min-h-[4.5rem] grid-cols-[auto_1fr_auto] items-center gap-3 lg:min-h-[5rem]">
<motion.div
@@ -290,7 +304,7 @@ export function SiteHeader({
animate={isScrolled ? { opacity: 0, x: 28 } : { opacity: 1, x: 0 }}
transition={reducedMotion ? { duration: 0 } : { duration: 0.3, ease: [0.22, 1, 0.36, 1] }}
className={cn(
"col-start-3 hidden items-center justify-end lg:flex",
"col-start-3 hidden items-center justify-end gap-2 lg:flex",
isScrolled ? "pointer-events-none invisible" : "",
)}
>
@@ -316,6 +330,12 @@ export function SiteHeader({
className={desktopControlButtonClassName}
/>
</div>
{isSuperAdmin ? (
<a href={adminHref} aria-label="Open admin dashboard" className={adminButtonClassName}>
<ShieldCheck className="h-4 w-4" />
<span className="hidden xl:inline">Admin</span>
</a>
) : null}
</motion.div>
<motion.div
@@ -459,6 +479,15 @@ export function SiteHeader({
variant="ghost"
className={mobileControlButtonClassName}
/>
{isSuperAdmin ? (
<a
href={adminHref}
aria-label="Open admin dashboard"
className={mobileAdminButtonClassName}
>
<ShieldCheck className="h-4 w-4" />
</a>
) : null}
</div>
</div>
</motion.div>
@@ -467,6 +496,6 @@ export function SiteHeader({
</motion.div>
) : null}
</AnimatePresence>
</header>
</motion.header>
);
}