generated from fahricansecer/boilerplate-fe
Initial commit
This commit is contained in:
71
src/components/layout/footer/footer.tsx
Normal file
71
src/components/layout/footer/footer.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { Box, Text, HStack, Link as ChakraLink } from "@chakra-ui/react";
|
||||
import { Link } from "@/i18n/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export default function Footer() {
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
<Box as="footer" bg="bg.muted" mt="auto">
|
||||
<HStack
|
||||
display="flex"
|
||||
justify={{ base: "center", md: "space-between" }}
|
||||
alignContent="center"
|
||||
maxW="8xl"
|
||||
mx="auto"
|
||||
wrap="wrap"
|
||||
px={{ base: 4, md: 8 }}
|
||||
position="relative"
|
||||
minH="16"
|
||||
>
|
||||
<Text fontSize="sm" color="fg.muted">
|
||||
© {new Date().getFullYear()}
|
||||
<ChakraLink
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://www.fcs.com.tr"
|
||||
color={{ base: "primary.500", _dark: "primary.300" }}
|
||||
focusRing="none"
|
||||
ml="1"
|
||||
>
|
||||
{"FCS"}
|
||||
</ChakraLink>
|
||||
. {t("all-right-reserved")}
|
||||
</Text>
|
||||
|
||||
<HStack spaceX={4}>
|
||||
<ChakraLink
|
||||
as={Link}
|
||||
href="/privacy"
|
||||
fontSize="sm"
|
||||
color="fg.muted"
|
||||
focusRing="none"
|
||||
position="relative"
|
||||
textDecor="none"
|
||||
transition="color 0.3s ease-in-out"
|
||||
_hover={{
|
||||
color: { base: "primary.500", _dark: "primary.300" },
|
||||
}}
|
||||
>
|
||||
{t("privacy-policy")}
|
||||
</ChakraLink>
|
||||
<ChakraLink
|
||||
as={Link}
|
||||
href="/terms"
|
||||
fontSize="sm"
|
||||
color="fg.muted"
|
||||
focusRing="none"
|
||||
position="relative"
|
||||
textDecor="none"
|
||||
transition="color 0.3s ease-in-out"
|
||||
_hover={{
|
||||
color: { base: "primary.500", _dark: "primary.300" },
|
||||
}}
|
||||
>
|
||||
{t("terms-of-service")}
|
||||
</ChakraLink>
|
||||
</HStack>
|
||||
</HStack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
104
src/components/layout/header/header-link.tsx
Normal file
104
src/components/layout/header/header-link.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { Box, Link as ChakraLink, Text } from '@chakra-ui/react';
|
||||
import { NavItem } from '@/config/navigation';
|
||||
import { MenuContent, MenuItem, MenuRoot, MenuTrigger } from '@/components/ui/overlays/menu';
|
||||
import { RxChevronDown } from 'react-icons/rx';
|
||||
import { useActiveNavItem } from '@/hooks/useActiveNavItem';
|
||||
import { Link } from '@/i18n/navigation';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useRef, useState } from 'react';
|
||||
|
||||
function HeaderLink({ item }: { item: NavItem }) {
|
||||
const t = useTranslations();
|
||||
const { isActive, isChildActive } = useActiveNavItem(item);
|
||||
const [open, setOpen] = useState(false);
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const handleMouseOpen = () => {
|
||||
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const handleMouseClose = () => {
|
||||
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = setTimeout(() => setOpen(false), 150);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box key={item.label}>
|
||||
{item.children ? (
|
||||
<Box onMouseEnter={handleMouseOpen} onMouseLeave={handleMouseClose}>
|
||||
<MenuRoot open={open} onOpenChange={(e) => setOpen(e.open)}>
|
||||
<MenuTrigger asChild>
|
||||
<Text
|
||||
display='inline-flex'
|
||||
alignItems='center'
|
||||
gap='1'
|
||||
cursor='pointer'
|
||||
color={isActive ? { base: 'primary.500', _dark: 'primary.300' } : 'fg.muted'}
|
||||
position='relative'
|
||||
fontWeight='semibold'
|
||||
>
|
||||
{t(item.label)}
|
||||
<RxChevronDown
|
||||
style={{ transform: open ? 'rotate(-180deg)' : 'rotate(0deg)', transition: 'transform 0.2s' }}
|
||||
/>
|
||||
</Text>
|
||||
</MenuTrigger>
|
||||
<MenuContent>
|
||||
{item.children.map((child, index) => {
|
||||
const isActiveChild = isChildActive(child.href);
|
||||
|
||||
return (
|
||||
<MenuItem key={index} value={child.href}>
|
||||
<ChakraLink
|
||||
key={index}
|
||||
as={Link}
|
||||
href={child.href}
|
||||
focusRing='none'
|
||||
w='full'
|
||||
color={isActiveChild ? { base: 'primary.500', _dark: 'primary.300' } : 'fg.muted'}
|
||||
position='relative'
|
||||
textDecor='none'
|
||||
fontWeight='semibold'
|
||||
>
|
||||
{t(child.label)}
|
||||
</ChakraLink>
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</MenuContent>
|
||||
</MenuRoot>
|
||||
</Box>
|
||||
) : (
|
||||
<ChakraLink
|
||||
as={Link}
|
||||
href={item.href}
|
||||
focusRing='none'
|
||||
color={isActive ? { base: 'primary.500', _dark: 'primary.300' } : 'fg.muted'}
|
||||
position='relative'
|
||||
textDecor='none'
|
||||
fontWeight='semibold'
|
||||
_after={{
|
||||
content: "''",
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
bottom: '-2px',
|
||||
width: '0%',
|
||||
height: '1.5px',
|
||||
bg: { base: 'primary.500', _dark: 'primary.300' },
|
||||
transition: 'width 0.3s ease-in-out',
|
||||
}}
|
||||
_hover={{
|
||||
_after: {
|
||||
width: '100%',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{t(item.label)}
|
||||
</ChakraLink>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default HeaderLink;
|
||||
229
src/components/layout/header/header.tsx
Normal file
229
src/components/layout/header/header.tsx
Normal file
@@ -0,0 +1,229 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
HStack,
|
||||
IconButton,
|
||||
Link as ChakraLink,
|
||||
Stack,
|
||||
VStack,
|
||||
Button,
|
||||
MenuItem,
|
||||
ClientOnly,
|
||||
} 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 } 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 { LuLogIn } from "react-icons/lu";
|
||||
|
||||
export default function Header() {
|
||||
const t = useTranslations();
|
||||
const [isSticky, setIsSticky] = useState(false);
|
||||
const [loginModalOpen, setLoginModalOpen] = useState(false);
|
||||
const router = useRouter();
|
||||
const { data: session, status } = useSession();
|
||||
|
||||
const isAuthenticated = !!session;
|
||||
const isLoading = status === "loading";
|
||||
|
||||
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("/signin");
|
||||
}
|
||||
};
|
||||
|
||||
// Render user menu or login button based on auth state
|
||||
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 onClick={handleLogout} value="sign-out">
|
||||
{t("auth.sign-out")}
|
||||
</MenuItem>
|
||||
</MenuContent>
|
||||
</MenuRoot>
|
||||
);
|
||||
}
|
||||
|
||||
// Not authenticated - show login button
|
||||
return (
|
||||
<Button
|
||||
variant="solid"
|
||||
colorPalette="primary"
|
||||
size="sm"
|
||||
onClick={() => setLoginModalOpen(true)}
|
||||
>
|
||||
<LuLogIn />
|
||||
{t("auth.sign-in")}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
// Render mobile auth section
|
||||
const renderMobileAuthSection = () => {
|
||||
if (isLoading) {
|
||||
return <Skeleton height="10" width="full" />;
|
||||
}
|
||||
|
||||
if (isAuthenticated) {
|
||||
return (
|
||||
<>
|
||||
<Avatar name={session?.user?.name || "User"} variant="solid" />
|
||||
<Button
|
||||
variant="surface"
|
||||
size="sm"
|
||||
width="full"
|
||||
onClick={handleLogout}
|
||||
>
|
||||
{t("auth.sign-out")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="solid"
|
||||
colorPalette="primary"
|
||||
size="sm"
|
||||
width="full"
|
||||
onClick={() => setLoginModalOpen(true)}
|
||||
>
|
||||
<LuLogIn />
|
||||
{t("auth.sign-in")}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
as="nav"
|
||||
bg={isSticky ? "rgba(255, 255, 255, 0.6)" : "white"}
|
||||
_dark={{
|
||||
bg: isSticky ? "rgba(1, 1, 1, 0.6)" : "black",
|
||||
}}
|
||||
shadow={isSticky ? "sm" : "none"}
|
||||
backdropFilter="blur(12px) saturate(180%)"
|
||||
border="1px solid"
|
||||
borderColor={isSticky ? "whiteAlpha.300" : "transparent"}
|
||||
borderBottomRadius={isSticky ? "xl" : "none"}
|
||||
transition="all 0.4s ease-in-out"
|
||||
px={{ base: 4, md: 8 }}
|
||||
py="3"
|
||||
position="sticky"
|
||||
top={0}
|
||||
zIndex={10}
|
||||
w="full"
|
||||
>
|
||||
<Flex justify="space-between" align="center" maxW="8xl" mx="auto">
|
||||
{/* Logo */}
|
||||
<HStack>
|
||||
<ChakraLink
|
||||
as={Link}
|
||||
href="/home"
|
||||
fontSize="lg"
|
||||
fontWeight="bold"
|
||||
color={{ base: "primary.500", _dark: "primary.300" }}
|
||||
focusRing="none"
|
||||
textDecor="none"
|
||||
transition="all 0.3s ease-in-out"
|
||||
_hover={{
|
||||
color: { base: "primary.900", _dark: "primary.50" },
|
||||
}}
|
||||
>
|
||||
{"FCS "}
|
||||
</ChakraLink>
|
||||
</HStack>
|
||||
|
||||
{/* DESKTOP NAVIGATION */}
|
||||
<HStack spaceX={4} display={{ base: "none", lg: "flex" }}>
|
||||
{NAV_ITEMS.map((item, index) => (
|
||||
<HeaderLink key={index} item={item} />
|
||||
))}
|
||||
</HStack>
|
||||
|
||||
<HStack>
|
||||
<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">
|
||||
{NAV_ITEMS.map((item) => (
|
||||
<MobileHeaderLink key={item.label} item={item} />
|
||||
))}
|
||||
<LocaleSwitcher />
|
||||
{renderMobileAuthSection()}
|
||||
</VStack>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</PopoverRoot>
|
||||
</ClientOnly>
|
||||
</Stack>
|
||||
</HStack>
|
||||
</Flex>
|
||||
</Box>
|
||||
|
||||
{/* Login Modal */}
|
||||
<LoginModal open={loginModalOpen} onOpenChange={setLoginModalOpen} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
84
src/components/layout/header/mobile-header-link.tsx
Normal file
84
src/components/layout/header/mobile-header-link.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import { Text, Box, Link as ChakraLink, useDisclosure, VStack } from '@chakra-ui/react';
|
||||
import { RxChevronDown } from 'react-icons/rx';
|
||||
import { NavItem } from '@/config/navigation';
|
||||
import { useActiveNavItem } from '@/hooks/useActiveNavItem';
|
||||
import { Link } from '@/i18n/navigation';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
function MobileHeaderLink({ item }: { item: NavItem }) {
|
||||
const t = useTranslations();
|
||||
const { isActive, isChildActive } = useActiveNavItem(item);
|
||||
const { open, onToggle } = useDisclosure();
|
||||
|
||||
return (
|
||||
<Box key={item.label} w='full'>
|
||||
{item.children ? (
|
||||
<VStack align='start' w='full' spaceY={0}>
|
||||
<Text
|
||||
onClick={onToggle}
|
||||
display='inline-flex'
|
||||
alignItems='center'
|
||||
gap='1'
|
||||
cursor='pointer'
|
||||
color={isActive ? { base: 'primary.500', _dark: 'primary.300' } : 'fg.muted'}
|
||||
textUnderlineOffset='4px'
|
||||
textUnderlinePosition='from-font'
|
||||
textDecoration={isActive ? 'underline' : 'none'}
|
||||
fontWeight='semibold'
|
||||
fontSize={{ base: 'md', md: 'lg' }}
|
||||
_hover={{
|
||||
color: { base: 'primary.500', _dark: 'primary.300' },
|
||||
transition: 'all 0.2s ease-in-out',
|
||||
}}
|
||||
>
|
||||
{t(item.label)}
|
||||
<RxChevronDown
|
||||
style={{ transform: open ? 'rotate(-180deg)' : 'rotate(0deg)', transition: 'transform 0.2s' }}
|
||||
/>
|
||||
</Text>
|
||||
{open && item.children && (
|
||||
<VStack align='start' pl='4' pt='1' pb='2' w='full' spaceY={1}>
|
||||
{item.children.map((child, index) => {
|
||||
const isActiveChild = isChildActive(child.href);
|
||||
|
||||
return (
|
||||
<ChakraLink
|
||||
key={index}
|
||||
as={Link}
|
||||
href={child.href}
|
||||
focusRing='none'
|
||||
color={isActiveChild ? { base: 'primary.500', _dark: 'primary.300' } : 'fg.muted'}
|
||||
textUnderlineOffset='4px'
|
||||
textUnderlinePosition='from-font'
|
||||
textDecoration={isActiveChild ? 'underline' : 'none'}
|
||||
fontWeight='semibold'
|
||||
fontSize={{ base: 'md', md: 'lg' }}
|
||||
>
|
||||
{t(child.label)}
|
||||
</ChakraLink>
|
||||
);
|
||||
})}
|
||||
</VStack>
|
||||
)}
|
||||
</VStack>
|
||||
) : (
|
||||
<ChakraLink
|
||||
as={Link}
|
||||
href={item.href}
|
||||
w='full'
|
||||
focusRing='none'
|
||||
color={isActive ? { base: 'primary.500', _dark: 'primary.300' } : 'fg.muted'}
|
||||
textUnderlineOffset='4px'
|
||||
textUnderlinePosition='from-font'
|
||||
textDecoration={isActive ? 'underline' : 'none'}
|
||||
fontWeight='semibold'
|
||||
fontSize={{ base: 'md', md: 'lg' }}
|
||||
>
|
||||
{t(item.label)}
|
||||
</ChakraLink>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default MobileHeaderLink;
|
||||
Reference in New Issue
Block a user