332 lines
9.8 KiB
TypeScript
332 lines
9.8 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
Box,
|
|
Flex,
|
|
HStack,
|
|
IconButton,
|
|
Link as ChakraLink,
|
|
Stack,
|
|
VStack,
|
|
Button,
|
|
MenuItem,
|
|
ClientOnly,
|
|
Text,
|
|
Separator,
|
|
} from "@chakra-ui/react";
|
|
import { Link, useRouter } from "@/i18n/navigation";
|
|
import { ColorModeButton } from "@/components/ui/color-mode";
|
|
import {
|
|
PopoverBody,
|
|
PopoverContent,
|
|
PopoverRoot,
|
|
PopoverTrigger,
|
|
} from "@/components/ui/overlays/popover";
|
|
import { RxHamburgerMenu } from "react-icons/rx";
|
|
import { NAV_ITEMS, getVisibleNavItems } from "@/config/navigation";
|
|
import HeaderLink from "./header-link";
|
|
import MobileHeaderLink from "./mobile-header-link";
|
|
import LocaleSwitcher from "@/components/ui/locale-switcher";
|
|
import { useEffect, useState } from "react";
|
|
import { useTranslations } from "next-intl";
|
|
import {
|
|
MenuContent,
|
|
MenuRoot,
|
|
MenuTrigger,
|
|
} from "@/components/ui/overlays/menu";
|
|
import { Avatar } from "@/components/ui/data-display/avatar";
|
|
import { Skeleton } from "@/components/ui/feedback/skeleton";
|
|
import { signOut, useSession } from "next-auth/react";
|
|
import { authConfig } from "@/config/auth";
|
|
import { LoginModal } from "@/components/auth/login-modal";
|
|
import { isAdminRole } from "@/lib/auth/roles";
|
|
import { LuLogIn, LuUser, LuShield, LuZap } from "react-icons/lu";
|
|
import GlobalSearch from "@/components/search/global-search";
|
|
|
|
export default function Header() {
|
|
const t = useTranslations();
|
|
const [isSticky, setIsSticky] = useState(false);
|
|
const [loginModalOpen, setLoginModalOpen] = useState(false);
|
|
const [loginModalMode, setLoginModalMode] = useState<"login" | "register">("login");
|
|
const router = useRouter();
|
|
const { data: session, status } = useSession();
|
|
|
|
const isAuthenticated = !!session;
|
|
const isLoading = status === "loading";
|
|
const visibleItems = getVisibleNavItems(NAV_ITEMS, isAuthenticated);
|
|
|
|
useEffect(() => {
|
|
const handleScroll = () => setIsSticky(window.scrollY >= 10);
|
|
window.addEventListener("scroll", handleScroll);
|
|
return () => window.removeEventListener("scroll", handleScroll);
|
|
}, []);
|
|
|
|
const handleLogout = async () => {
|
|
await signOut({ redirect: false });
|
|
if (authConfig.isAuthRequired) {
|
|
router.replace("/home");
|
|
}
|
|
};
|
|
|
|
const openAuthModal = (mode: "login" | "register") => {
|
|
setLoginModalMode(mode);
|
|
setLoginModalOpen(true);
|
|
};
|
|
|
|
// Desktop auth section
|
|
const renderAuthSection = () => {
|
|
if (isLoading) return <Skeleton boxSize="10" rounded="full" />;
|
|
|
|
if (isAuthenticated) {
|
|
return (
|
|
<MenuRoot positioning={{ placement: "bottom-start" }}>
|
|
<MenuTrigger rounded="full" focusRing="none">
|
|
<Avatar name={session?.user?.name || "User"} variant="solid" />
|
|
</MenuTrigger>
|
|
<MenuContent>
|
|
<MenuItem value="profile" onClick={() => router.push("/profile")}>
|
|
<LuUser />
|
|
{t("nav.profile")}
|
|
</MenuItem>
|
|
{session?.user && isAdminRole(session.user.roles) && (
|
|
<MenuItem value="admin" onClick={() => router.push("/admin")}>
|
|
<LuShield />
|
|
{t("nav.admin")}
|
|
</MenuItem>
|
|
)}
|
|
<MenuItem onClick={handleLogout} value="sign-out">
|
|
{t("auth.sign-out")}
|
|
</MenuItem>
|
|
</MenuContent>
|
|
</MenuRoot>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<HStack gap={2}>
|
|
<Button
|
|
variant="outline"
|
|
colorPalette="gray"
|
|
size="sm"
|
|
borderRadius="full"
|
|
onClick={() => openAuthModal("register")}
|
|
>
|
|
{t("auth.sign-up")}
|
|
</Button>
|
|
<Button
|
|
variant="solid"
|
|
colorPalette="primary"
|
|
size="sm"
|
|
borderRadius="full"
|
|
onClick={() => openAuthModal("login")}
|
|
>
|
|
<LuLogIn />
|
|
{t("auth.sign-in")}
|
|
</Button>
|
|
</HStack>
|
|
);
|
|
};
|
|
|
|
// Mobile auth section
|
|
const renderMobileAuthSection = () => {
|
|
if (isLoading) return <Skeleton height="10" width="full" />;
|
|
|
|
if (isAuthenticated) {
|
|
return (
|
|
<VStack gap={2} w="full">
|
|
<Flex align="center" gap={2} w="full">
|
|
<Avatar
|
|
name={session?.user?.name || "User"}
|
|
variant="solid"
|
|
size="sm"
|
|
/>
|
|
<Text fontSize="sm" fontWeight="semibold" truncate>
|
|
{session?.user?.name || session?.user?.email}
|
|
</Text>
|
|
</Flex>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
width="full"
|
|
justifyContent="flex-start"
|
|
onClick={() => router.push("/profile")}
|
|
>
|
|
<LuUser />
|
|
{t("nav.profile")}
|
|
</Button>
|
|
<Button
|
|
variant="surface"
|
|
size="sm"
|
|
width="full"
|
|
onClick={handleLogout}
|
|
>
|
|
{t("auth.sign-out")}
|
|
</Button>
|
|
</VStack>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<VStack gap={2} w="full">
|
|
<Button
|
|
variant="outline"
|
|
colorPalette="gray"
|
|
size="sm"
|
|
width="full"
|
|
borderRadius="full"
|
|
onClick={() => openAuthModal("register")}
|
|
>
|
|
{t("auth.sign-up")}
|
|
</Button>
|
|
<Button
|
|
variant="solid"
|
|
colorPalette="primary"
|
|
size="sm"
|
|
width="full"
|
|
borderRadius="full"
|
|
onClick={() => openAuthModal("login")}
|
|
>
|
|
<LuLogIn />
|
|
{t("auth.sign-in")}
|
|
</Button>
|
|
</VStack>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<Box
|
|
as="nav"
|
|
bg={isSticky ? "rgba(255, 255, 255, 0.75)" : "white"}
|
|
_dark={{
|
|
bg: isSticky ? "rgba(1, 1, 1, 0.75)" : "black",
|
|
}}
|
|
shadow={isSticky ? "md" : "xs"}
|
|
backdropFilter="blur(16px) saturate(180%)"
|
|
borderBottom="1px solid"
|
|
borderColor={{ base: "gray.100", _dark: "gray.800" }}
|
|
transition="all 0.3s ease-in-out"
|
|
px={{ base: 4, md: 6 }}
|
|
py="2.5"
|
|
position="sticky"
|
|
top={0}
|
|
zIndex={10}
|
|
w="full"
|
|
>
|
|
<Flex justify="space-between" align="center" maxW="8xl" mx="auto">
|
|
{/* Logo */}
|
|
<ChakraLink
|
|
as={Link}
|
|
href="/home"
|
|
focusRing="none"
|
|
textDecor="none"
|
|
_hover={{ textDecor: "none" }}
|
|
display="flex"
|
|
alignItems="center"
|
|
gap="2"
|
|
flexShrink={0}
|
|
mr={6}
|
|
>
|
|
<Flex
|
|
boxSize="32px"
|
|
bg="primary.500"
|
|
borderRadius="lg"
|
|
align="center"
|
|
justify="center"
|
|
shadow="sm"
|
|
>
|
|
<LuZap color="white" size={18} />
|
|
</Flex>
|
|
<Box>
|
|
<Text
|
|
fontSize="md"
|
|
fontWeight="800"
|
|
lineHeight="1"
|
|
color={{ base: "gray.900", _dark: "white" }}
|
|
letterSpacing="-0.02em"
|
|
>
|
|
Suggest
|
|
</Text>
|
|
<Text
|
|
fontSize="xs"
|
|
fontWeight="600"
|
|
lineHeight="1"
|
|
mt="1px"
|
|
color={{ base: "primary.600", _dark: "primary.300" }}
|
|
letterSpacing="0.08em"
|
|
textTransform="uppercase"
|
|
>
|
|
BET
|
|
</Text>
|
|
</Box>
|
|
</ChakraLink>
|
|
|
|
{/* DESKTOP NAVIGATION */}
|
|
<HStack gap={1} display={{ base: "none", lg: "flex" }} flex={1}>
|
|
{visibleItems.map((item) => (
|
|
<HeaderLink key={item.href} item={item} />
|
|
))}
|
|
</HStack>
|
|
|
|
{/* Right side actions */}
|
|
<HStack gap={2} flexShrink={0}>
|
|
{/* Global Search (Desktop) */}
|
|
<Box display={{ base: "none", lg: "block" }}>
|
|
<GlobalSearch />
|
|
</Box>
|
|
|
|
<Separator
|
|
orientation="vertical"
|
|
height="5"
|
|
display={{ base: "none", lg: "block" }}
|
|
borderColor={{ base: "gray.200", _dark: "gray.700" }}
|
|
/>
|
|
|
|
<ColorModeButton colorPalette="gray" />
|
|
<Box display={{ base: "none", lg: "inline-flex" }} gap={2}>
|
|
<LocaleSwitcher />
|
|
<ClientOnly fallback={<Skeleton boxSize="10" rounded="full" />}>
|
|
{renderAuthSection()}
|
|
</ClientOnly>
|
|
</Box>
|
|
|
|
{/* MOBILE NAVIGATION */}
|
|
<Stack display={{ base: "inline-flex", lg: "none" }}>
|
|
<ClientOnly fallback={<Skeleton boxSize="9" />}>
|
|
<PopoverRoot>
|
|
<PopoverTrigger as="span">
|
|
<IconButton aria-label="Open menu" variant="ghost">
|
|
<RxHamburgerMenu />
|
|
</IconButton>
|
|
</PopoverTrigger>
|
|
<PopoverContent width={{ base: "xs", sm: "sm", md: "md" }}>
|
|
<PopoverBody>
|
|
<VStack mt="2" align="start" spaceY="2" w="full">
|
|
{visibleItems.map((item) => (
|
|
<MobileHeaderLink key={item.href} item={item} />
|
|
))}
|
|
<Box
|
|
w="full"
|
|
pt={2}
|
|
borderTopWidth="1px"
|
|
borderColor="border.muted"
|
|
>
|
|
<LocaleSwitcher />
|
|
</Box>
|
|
{renderMobileAuthSection()}
|
|
</VStack>
|
|
</PopoverBody>
|
|
</PopoverContent>
|
|
</PopoverRoot>
|
|
</ClientOnly>
|
|
</Stack>
|
|
</HStack>
|
|
</Flex>
|
|
</Box>
|
|
|
|
{/* Login Modal */}
|
|
<LoginModal open={loginModalOpen} onOpenChange={setLoginModalOpen} initialMode={loginModalMode} />
|
|
</>
|
|
);
|
|
}
|