@@ -1,62 +1,3 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
export function SiteAmbientBackdrop() {
|
||||
return (
|
||||
<div className="pointer-events-none fixed inset-0 -z-10 overflow-hidden">
|
||||
<motion.div
|
||||
animate={{
|
||||
x: [0, 72, -36, 0],
|
||||
y: [0, -54, 28, 0],
|
||||
scale: [1, 1.1, 0.95, 1],
|
||||
}}
|
||||
transition={{
|
||||
duration: 18,
|
||||
repeat: Number.POSITIVE_INFINITY,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
className="absolute left-[-10%] top-[4%] h-[30rem] w-[30rem] rounded-full bg-brand-primary/12 blur-3xl sm:h-[38rem] sm:w-[38rem] lg:h-[46rem] lg:w-[46rem]"
|
||||
/>
|
||||
<motion.div
|
||||
animate={{
|
||||
x: [0, -84, 26, 0],
|
||||
y: [0, 44, -22, 0],
|
||||
scale: [1, 0.9, 1.08, 1],
|
||||
}}
|
||||
transition={{
|
||||
duration: 22,
|
||||
repeat: Number.POSITIVE_INFINITY,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
className="absolute right-[-12%] top-[8%] h-[26rem] w-[26rem] rounded-full bg-brand-secondary/12 blur-3xl sm:h-[34rem] sm:w-[34rem] lg:h-[42rem] lg:w-[42rem]"
|
||||
/>
|
||||
<motion.div
|
||||
animate={{
|
||||
x: [0, 34, -20, 0],
|
||||
y: [0, -24, 34, 0],
|
||||
opacity: [0.22, 0.38, 0.16, 0.22],
|
||||
}}
|
||||
transition={{
|
||||
duration: 16,
|
||||
repeat: Number.POSITIVE_INFINITY,
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
className="absolute bottom-[-12%] left-[22%] h-[20rem] w-[20rem] rounded-full bg-brand-primary/[0.08] blur-3xl sm:h-[28rem] sm:w-[28rem] lg:h-[34rem] lg:w-[34rem]"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-[linear-gradient(180deg,rgba(255,255,255,0.18),rgba(255,255,255,0.08)_24%,rgba(255,255,255,0.02)_52%,rgba(255,255,255,0)_100%),radial-gradient(circle_at_16%_18%,rgba(221,65,36,0.08),transparent_30%),radial-gradient(circle_at_78%_14%,rgba(235,143,117,0.08),transparent_34%),radial-gradient(circle_at_50%_100%,rgba(160,182,255,0.08),transparent_40%)] dark:bg-[linear-gradient(180deg,rgba(255,255,255,0.01),rgba(255,255,255,0.005)_26%,rgba(255,255,255,0)_100%),radial-gradient(circle_at_16%_18%,rgba(221,65,36,0.08),transparent_30%),radial-gradient(circle_at_78%_14%,rgba(235,143,117,0.06),transparent_34%),radial-gradient(circle_at_50%_100%,rgba(109,130,214,0.05),transparent_40%)]" />
|
||||
<motion.div
|
||||
animate={{
|
||||
x: [0, 20, -14, 0],
|
||||
y: [0, -16, 12, 0],
|
||||
}}
|
||||
transition={{
|
||||
duration: 9,
|
||||
repeat: Number.POSITIVE_INFINITY,
|
||||
ease: "linear",
|
||||
}}
|
||||
className="hero-noise absolute inset-[-6%] opacity-[0.22] mix-blend-soft-light dark:opacity-[0.08]"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -99,8 +99,8 @@ export function HeroShell({
|
||||
className={cn(
|
||||
"hero-surface relative isolate overflow-hidden",
|
||||
variant === "home"
|
||||
? "hero-home flex min-h-[92svh] items-center justify-center"
|
||||
: "hero-page flex min-h-[44svh] items-center sm:min-h-[48svh]",
|
||||
? "hero-home flex min-h-[76svh] items-center justify-center"
|
||||
: "hero-page flex min-h-[34svh] items-center sm:min-h-[38svh]",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
@@ -113,8 +113,8 @@ export function HeroShell({
|
||||
className={cn(
|
||||
"relative z-10 w-full",
|
||||
variant === "home"
|
||||
? "pb-12 pt-24 sm:pb-14 sm:pt-28 lg:pb-16 lg:pt-32"
|
||||
: "py-20 sm:py-24 lg:py-28",
|
||||
? "pb-10 pt-20 sm:pb-12 sm:pt-24 lg:pb-14 lg:pt-28"
|
||||
: "py-16 sm:py-20 lg:py-24",
|
||||
containerClassName,
|
||||
)}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user