From 1a2241c40e10193c5ff7008a7b7b36cc1d855d96 Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Tue, 25 Mar 2025 15:55:45 +0900 Subject: initial commit --- components/layout/Footer.tsx | 16 +++ components/layout/Header.tsx | 225 ++++++++++++++++++++++++++++++++ components/layout/MobileMenu.tsx | 88 +++++++++++++ components/layout/command-menu.tsx | 139 ++++++++++++++++++++ components/layout/createEmotionCashe.ts | 5 + components/layout/mode-switcher.tsx | 35 +++++ components/layout/providers.tsx | 38 ++++++ components/layout/sidebar-nav.tsx | 44 +++++++ 8 files changed, 590 insertions(+) create mode 100644 components/layout/Footer.tsx create mode 100644 components/layout/Header.tsx create mode 100644 components/layout/MobileMenu.tsx create mode 100644 components/layout/command-menu.tsx create mode 100644 components/layout/createEmotionCashe.ts create mode 100644 components/layout/mode-switcher.tsx create mode 100644 components/layout/providers.tsx create mode 100644 components/layout/sidebar-nav.tsx (limited to 'components/layout') diff --git a/components/layout/Footer.tsx b/components/layout/Footer.tsx new file mode 100644 index 00000000..f7d6906d --- /dev/null +++ b/components/layout/Footer.tsx @@ -0,0 +1,16 @@ +import { siteConfig } from "@/config/site" + +export function SiteFooter() { + return ( + + ) +} diff --git a/components/layout/Header.tsx b/components/layout/Header.tsx new file mode 100644 index 00000000..1b6c45bb --- /dev/null +++ b/components/layout/Header.tsx @@ -0,0 +1,225 @@ +"use client"; + +import * as React from "react"; +import Link from "next/link"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + NavigationMenu, + NavigationMenuContent, + NavigationMenuItem, + NavigationMenuLink, + NavigationMenuList, + NavigationMenuTrigger, + navigationMenuTriggerStyle, +} from "@/components/ui/navigation-menu"; +import { SearchIcon, BellIcon, Menu } from "lucide-react"; +import { useParams, usePathname } from "next/navigation"; +import { cn } from "@/lib/utils"; +import Image from "next/image"; +import { mainNav, additionalNav, MenuSection, mainNavVendor, additionalNavVendor } from "@/config/menuConfig"; // 메뉴 구성 임포트 +import { MobileMenu } from "./MobileMenu"; +import { CommandMenu } from "./command-menu"; +import { useSession, signOut } from "next-auth/react"; + + +export function Header() { + const params = useParams(); + const lng = params.lng as string; + const pathname = usePathname(); + const { data: session } = useSession(); + + const userName = session?.user?.name || ""; // 없을 수도 있으니 안전하게 처리 + const initials = userName + .split(" ") + .map((word) => word[0]?.toUpperCase()) + .join(""); + + + const [isMobileMenuOpen, setIsMobileMenuOpen] = React.useState(false); // 모바일 메뉴 상태 + + const toggleMobileMenu = () => { + setIsMobileMenuOpen(!isMobileMenuOpen); + }; + + const isPartnerRoute = pathname.includes("/partners"); + + const main = isPartnerRoute ? mainNavVendor : mainNav; + const additional = isPartnerRoute ? additionalNavVendor : additionalNav; + + const basePath = `/${lng}${isPartnerRoute ? "/partners" : "/evcp"}`; + + return ( + <> +
+
+
+ + + + +
+ + {/* 로고 영역 */} +
+ + EVCP Logo + eVCP + +
+ {/* 데스크탑 네비게이션 메뉴 */} + + + {main.map((section: MenuSection) => ( + + {section.title} + +
    + {section.items.map((item) => ( + + {item.description} + + ))} +
+
+
+ ))} + + + {/* 추가 네비게이션 항목 */} + {additional.map((item) => ( + + + + {item.title} + + + + ))} +
+
+ + +
+ + + {/* 우측 영역 */} +
+ + + + +
+ {/* 알림 버튼 */} + + + {/* 사용자 메뉴 (DropdownMenu) */} + + + + + + {initials || "?"} + + + + + My Account + + {/* + Profile + */} + + Settings + + + signOut({ callbackUrl: `/${lng}/login` })}> + Logout + + + + + {/* 모바일 햄버거 메뉴 버튼 */} + +
+
+
+
+ + {/* 모바일 메뉴 */} + {isMobileMenuOpen && } +
+ + ); +} + +const ListItem = React.forwardRef< + React.ElementRef<"a">, + React.ComponentPropsWithoutRef<"a"> +>(({ className, title, children, ...props }, ref) => { + return ( +
  • + + +
    {title}
    + {children && ( +

    + {children} +

    + )} +
    +
    +
  • + ); +}); +ListItem.displayName = "ListItem"; \ No newline at end of file diff --git a/components/layout/MobileMenu.tsx b/components/layout/MobileMenu.tsx new file mode 100644 index 00000000..d2e6b927 --- /dev/null +++ b/components/layout/MobileMenu.tsx @@ -0,0 +1,88 @@ +// components/MobileMenu.tsx + +"use client"; + +import * as React from "react"; +import Link from "next/link"; +import { useRouter,usePathname } from "next/navigation"; +import { MenuSection, mainNav, additionalNav, MenuItem, mainNavVendor, additionalNavVendor } from "@/config/menuConfig"; +import { cn } from "@/lib/utils"; +import { Drawer, DrawerContent,DrawerTitle,DrawerTrigger } from "@/components/ui/drawer"; +import { Button } from "@/components/ui/button"; + +interface MobileMenuProps { + lng: string; + onClose: () => void; +} + +export function MobileMenu({ lng, onClose }: MobileMenuProps) { + const router = useRouter(); + + + const handleLinkClick = (href: string) => { + router.push(href); + onClose(); + }; + const pathname = usePathname(); + const isPartnerRoute = pathname.includes("/partners"); + + const main = isPartnerRoute ? mainNavVendor : mainNav; + const additional = isPartnerRoute ? additionalNavVendor : additionalNav; + + return ( + + + + + +
    + + +
    +
    +
    + ); +} \ No newline at end of file diff --git a/components/layout/command-menu.tsx b/components/layout/command-menu.tsx new file mode 100644 index 00000000..5537a042 --- /dev/null +++ b/components/layout/command-menu.tsx @@ -0,0 +1,139 @@ +"use client" + +import * as React from "react" +import { useRouter,usePathname } from "next/navigation" +import { type DialogProps } from "@radix-ui/react-dialog" +import { Circle, File, Laptop, Moon, Sun } from "lucide-react" +import { useTheme } from "next-themes" + +import { MenuSection, mainNav, additionalNav, MenuItem, mainNavVendor, additionalNavVendor } from "@/config/menuConfig"; +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { + CommandDialog, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, +} from "@/components/ui/command" +import { DialogTitle } from "@/components/ui/dialog" + +export function CommandMenu({ ...props }: DialogProps) { + const router = useRouter() + const [open, setOpen] = React.useState(false) + const { setTheme } = useTheme() + + React.useEffect(() => { + const down = (e: KeyboardEvent) => { + if ((e.key === "k" && (e.metaKey || e.ctrlKey)) || e.key === "/") { + if ( + (e.target instanceof HTMLElement && e.target.isContentEditable) || + e.target instanceof HTMLInputElement || + e.target instanceof HTMLTextAreaElement || + e.target instanceof HTMLSelectElement + ) { + return + } + + e.preventDefault() + setOpen((open) => !open) + } + } + + document.addEventListener("keydown", down) + return () => document.removeEventListener("keydown", down) + }, []) + + const runCommand = React.useCallback((command: () => unknown) => { + setOpen(false) + command() + }, []) + + +const pathname = usePathname(); +const isPartnerRoute = pathname.includes("/partners"); + + const main = isPartnerRoute ? mainNavVendor : mainNav; + const additional = isPartnerRoute ? additionalNavVendor : additionalNav; + + + return ( + <> + + + Search Menu + + + No results found. + + {main.map((group) => ( + + {group.items.map((navItem) => ( + { + runCommand(() => router.push(navItem.href as string)) + }} + > +
    + +
    + {navItem.title} +
    + ))} +
    + ))} + + {additional + // .filter((navitem) => !navitem.external) + .map((navItem) => ( + { + runCommand(() => router.push(navItem.href as string)) + }} + > + + {navItem.title} + + ))} + + + + + + runCommand(() => setTheme("light"))}> + + Light + + runCommand(() => setTheme("dark"))}> + + Dark + + runCommand(() => setTheme("system"))}> + + System + + +
    +
    + + ) +} diff --git a/components/layout/createEmotionCashe.ts b/components/layout/createEmotionCashe.ts new file mode 100644 index 00000000..ae8bc3b5 --- /dev/null +++ b/components/layout/createEmotionCashe.ts @@ -0,0 +1,5 @@ +import createCache from '@emotion/cache'; + +export default function createEmotionCache() { + return createCache({ key: 'css' }); +} \ No newline at end of file diff --git a/components/layout/mode-switcher.tsx b/components/layout/mode-switcher.tsx new file mode 100644 index 00000000..d27b6a73 --- /dev/null +++ b/components/layout/mode-switcher.tsx @@ -0,0 +1,35 @@ +"use client" + +import * as React from "react" +import { MoonIcon, SunIcon } from "lucide-react" +import { useTheme } from "next-themes" + +import { META_THEME_COLORS } from "@/config/site" +import { useMetaColor } from "@/hooks/use-meta-color" +import { Button } from "@/components/ui/button" + +export function ModeSwitcher() { + const { setTheme, resolvedTheme } = useTheme() + const { setMetaColor } = useMetaColor() + + const toggleTheme = React.useCallback(() => { + setTheme(resolvedTheme === "dark" ? "light" : "dark") + setMetaColor( + resolvedTheme === "dark" + ? META_THEME_COLORS.light + : META_THEME_COLORS.dark + ) + }, [resolvedTheme, setTheme, setMetaColor]) + + return ( + + ) +} diff --git a/components/layout/providers.tsx b/components/layout/providers.tsx new file mode 100644 index 00000000..1c645531 --- /dev/null +++ b/components/layout/providers.tsx @@ -0,0 +1,38 @@ +"use client" + +import * as React from "react" +import { Provider as JotaiProvider } from "jotai" +import { ThemeProvider as NextThemesProvider } from "next-themes" +import { NuqsAdapter } from "nuqs/adapters/next/app" +import { SessionProvider } from "next-auth/react"; +import { CacheProvider } from '@emotion/react'; + +import { TooltipProvider } from "@/components/ui/tooltip" +import createEmotionCache from './createEmotionCashe'; + + +const cache = createEmotionCache(); + + +export function ThemeProvider({ + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + + {children} + + + + + + + + ) +} diff --git a/components/layout/sidebar-nav.tsx b/components/layout/sidebar-nav.tsx new file mode 100644 index 00000000..addcfefd --- /dev/null +++ b/components/layout/sidebar-nav.tsx @@ -0,0 +1,44 @@ +"use client" + +import Link from "next/link" +import { usePathname } from "next/navigation" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +interface SidebarNavProps extends React.HTMLAttributes { + items: { + href: string + title: string + }[] +} + +export function SidebarNav({ className, items, ...props }: SidebarNavProps) { + const pathname = usePathname() + + return ( + + ) +} -- cgit v1.2.3