diff options
Diffstat (limited to 'frontend/src/components')
-rw-r--r-- | frontend/src/components/footer.tsx | 2 | ||||
-rw-r--r-- | frontend/src/components/header.tsx | 667 | ||||
-rw-r--r-- | frontend/src/components/loading.tsx | 111 | ||||
-rw-r--r-- | frontend/src/components/product-card.tsx | 2 | ||||
-rw-r--r-- | frontend/src/components/product-page.tsx | 567 | ||||
-rw-r--r-- | frontend/src/components/theme-provider.tsx | 2 | ||||
-rw-r--r-- | frontend/src/components/theme-toggle.tsx | 23 | ||||
-rw-r--r-- | frontend/src/components/ui/navigation-menu.tsx | 19 | ||||
-rw-r--r-- | frontend/src/components/ui/tabs.tsx | 55 |
9 files changed, 1029 insertions, 419 deletions
diff --git a/frontend/src/components/footer.tsx b/frontend/src/components/footer.tsx index 62ebf94..b126661 100644 --- a/frontend/src/components/footer.tsx +++ b/frontend/src/components/footer.tsx @@ -97,7 +97,7 @@ export function Footer() { <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-8"> {/* Brand Info */} <div className="lg:col-span-2"> - <Link href="/" className="flex items-center space-x-2 mb-4"> + <Link href="/" className="flex items-center space-x-2 mb-4" suppressHydrationWarning> {/* Light theme logo - visible by default, hidden in dark mode */} <Image src="/black-logo.png" diff --git a/frontend/src/components/header.tsx b/frontend/src/components/header.tsx index b792767..566ea71 100644 --- a/frontend/src/components/header.tsx +++ b/frontend/src/components/header.tsx @@ -42,8 +42,6 @@ import { export function Header() { const [cartItems] = useState(3); const [wishlistItems] = useState(5); - const [isMounted, setIsMounted] = useState(false); - const [sheetError, setSheetError] = useState(false); const sheetRef = useRef<HTMLDivElement>(null); const { setTheme, theme } = useTheme(); @@ -69,49 +67,19 @@ export function Header() { ], }, { - title: "Kids", + title: "Unisex", items: [ - { name: "Boys", href: "/kids/boys" }, - { name: "Girls", href: "/kids/girls" }, - { name: "Baby", href: "/kids/baby" }, - { name: "Shoes", href: "/kids/shoes" }, + { name: "Hoodies & Sweatshirts", href: "/unisex/hoodies" }, + { name: "T-Shirts", href: "/unisex/tshirts" }, + { name: "Jeans & Pants", href: "/unisex/pants" }, + { name: "Outerwear", href: "/unisex/outerwear" }, + { name: "Accessories", href: "/unisex/accessories" }, ], }, ]; - // Preload both logo variants to ensure smooth loading + // Preload logos for smooth loading useEffect(() => { - setIsMounted(true); - - // Add global error handler for scroll-related errors - const handleError = (event: ErrorEvent) => { - if (event.error?.message?.includes('parameter 1 is not of type \'Node\'') || - event.error?.message?.includes('handleScroll') || - event.error?.message?.includes('RemoveScrollSideCar') || - event.error?.message?.includes('shouldCancelEvent') || - event.error?.message?.includes('shouldPrevent')) { - console.warn('Scroll handling error caught and suppressed:', event.error); - event.preventDefault(); - setSheetError(true); - return false; - } - }; - - // Also add an unhandled rejection handler for async scroll errors - const handleUnhandledRejection = (event: PromiseRejectionEvent) => { - if (event.reason?.message?.includes('handleScroll') || - event.reason?.message?.includes('RemoveScrollSideCar') || - event.reason?.message?.includes('parameter 1 is not of type \'Node\'')) { - console.warn('Scroll promise rejection caught and suppressed:', event.reason); - event.preventDefault(); - setSheetError(true); - return false; - } - }; - - window.addEventListener('error', handleError); - window.addEventListener('unhandledrejection', handleUnhandledRejection); - const preloadLogos = () => { if (typeof window !== 'undefined') { const lightLogo = new window.Image(); @@ -122,117 +90,10 @@ export function Header() { }; preloadLogos(); - - return () => { - window.removeEventListener('error', handleError); - window.removeEventListener('unhandledrejection', handleUnhandledRejection); - }; }, []); - // Add scroll error prevention - useEffect(() => { - const sheetElement = sheetRef.current; - if (sheetElement) { - const handleScroll = (event: Event) => { - try { - // Allow default scroll behavior but catch any errors - } catch (error) { - console.warn('Sheet scroll error caught:', error); - event.preventDefault(); - } - }; - - sheetElement.addEventListener('scroll', handleScroll, { passive: false }); - - return () => { - sheetElement.removeEventListener('scroll', handleScroll); - }; - } - }, [isMounted]); - - // Prevent hydration issues with Sheet component - if (!isMounted) { - return ( - <header className="sticky top-0 z-50 w-full bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60"> - {/* Top banner */} - <div className="bg-black text-white text-center py-2 px-4"> - <p className="text-sm font-medium"> - FREE SHIPPING ON ORDERS OVER $100 • NEW ARRIVALS EVERY WEEK - </p> - </div> - - {/* Announcement bar */} - <div className="bg-neutral-100 dark:bg-neutral-800 text-center py-2 px-4"> - <p className="text-sm"> - 🔥 <span className="font-semibold">WINTER SALE</span> - Up to 50% off on selected items - </p> - </div> - - {/* Main header */} - <div className="border-b dark:border-neutral-800"> - <div className="container mx-auto px-3 sm:px-4 lg:px-6"> - <div className="flex h-14 sm:h-16 items-center justify-between gap-2 sm:gap-4 md:justify-between"> - {/* Mobile menu placeholder */} - <Button - variant="ghost" - size="icon" - className="md:hidden nav-button-transparent backdrop-blur-sm" - disabled - > - <Menu className="h-5 w-5" /> - </Button> - - {/* Logo */} - <div className="flex items-center flex-1 md:flex-initial justify-center md:justify-start"> - <Link href="/" className="flex items-center space-x-2 ml-8 md:ml-0"> - <Image - src="/black-logo.png" - alt="blcklst" - width={120} - height={40} - className="h-8 w-auto block dark:hidden" - priority - /> - <Image - src="/white-logo.png" - alt="blcklst" - width={120} - height={40} - className="h-8 w-auto hidden dark:block" - priority - /> - </Link> - </div> - - {/* Simplified action buttons for SSR */} - <div className="flex items-center space-x-1 sm:space-x-2"> - <Button - variant="ghost" - size="icon" - className="lg:hidden nav-button-transparent backdrop-blur-sm min-w-[44px] min-h-[44px]" - disabled - > - <Search className="h-5 w-5" /> - </Button> - - <Button - variant="ghost" - size="icon" - className="relative nav-button-transparent backdrop-blur-sm min-w-[44px] min-h-[44px]" - disabled - > - <ShoppingBag className="h-4 w-4 sm:h-5 sm:w-5 text-muted-foreground flex-shrink-0" /> - </Button> - </div> - </div> - </div> - </div> - </header> - ); - } - return ( - <header className="sticky top-0 z-50 w-full bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60"> + <header className="sticky top-0 z-50 w-full bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60" suppressHydrationWarning> {/* Top banner */} <div className="bg-black text-white text-center py-2 px-4"> <p className="text-sm font-medium"> @@ -252,229 +113,235 @@ export function Header() { <div className="container mx-auto px-3 sm:px-4 lg:px-6"> <div className="flex h-14 sm:h-16 items-center justify-between gap-2 sm:gap-4 md:justify-between"> {/* Mobile menu */} - {!sheetError ? ( - <Sheet> - <SheetTrigger asChild> - <Button - variant="ghost" - size="icon" - className="md:hidden nav-button-transparent backdrop-blur-sm" - > - <Menu className="h-5 w-5" /> - </Button> - </SheetTrigger> - <SheetContent - ref={sheetRef} - side="left" - className="w-[280px] xs:w-[320px] sm:w-[380px] p-0 border-r dark:border-neutral-800" - onOpenAutoFocus={(event) => { - // Prevent auto focus to avoid scroll handling issues - event.preventDefault(); - }} - onCloseAutoFocus={(event) => { - // Prevent auto focus to avoid scroll handling issues - event.preventDefault(); - }} - onEscapeKeyDown={(event) => { - // Handle escape key gracefully - try { - // Default behavior - } catch (error) { - console.warn('Escape key handling error:', error); - event.preventDefault(); - setSheetError(true); - } - }} - onPointerDownOutside={(event) => { - // Handle pointer events gracefully - try { - // Default behavior - } catch (error) { - console.warn('Pointer down outside error:', error); - event.preventDefault(); - setSheetError(true); - } - }} - onInteractOutside={(event) => { - // Additional interaction handler - try { - // Default behavior - } catch (error) { - console.warn('Interact outside error:', error); - event.preventDefault(); - setSheetError(true); - } - }} - onFocusOutside={(event) => { - // Additional focus handler - try { - // Default behavior - } catch (error) { - console.warn('Focus outside error:', error); - event.preventDefault(); - setSheetError(true); - } - }} + <Sheet> + <SheetTrigger asChild> + <Button + variant="ghost" + size="icon" + className="md:hidden nav-button-transparent cursor-pointer" > - <div className="flex flex-col h-full"> - {/* Header Section */} - <SheetHeader className="px-6 py-4 border-b dark:border-neutral-800 bg-neutral-50 dark:bg-neutral-900"> - <SheetTitle className="text-left text-lg font-semibold"> - Menu - </SheetTitle> - </SheetHeader> + <Menu className="h-5 w-5" /> + </Button> + </SheetTrigger> + <SheetContent + ref={sheetRef} + side="left" + className="w-[280px] xs:w-[320px] sm:w-[380px] p-0 border-r dark:border-neutral-800" + > + <div className="flex flex-col h-full"> + {/* Header Section */} + <SheetHeader className="px-6 py-4 border-b dark:border-neutral-800 bg-neutral-50 dark:bg-neutral-900"> + <SheetTitle className="text-left text-lg font-semibold"> + Menu + </SheetTitle> + </SheetHeader> - {/* Navigation Section */} - <div className="flex-1 overflow-y-auto mobile-nav-scroll"> - <nav className="px-4 py-6 space-y-6"> - {categories.map((category) => ( - <div key={category.title} className="space-y-3"> - {/* Category Header */} - <h3 className="px-2 text-sm font-semibold text-foreground uppercase tracking-wider border-b border-neutral-200 dark:border-neutral-700 pb-2"> - {category.title} - </h3> - - {/* Category Items */} - <div className="space-y-1"> - {category.items.map((item) => ( - <Link - key={item.name} - href={item.href} - className="flex items-center px-3 py-3 text-sm text-muted-foreground hover:text-foreground hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-lg transition-colors touch-manipulation min-h-[44px]" + {/* Navigation Section */} + <div className="flex-1 overflow-y-auto mobile-nav-scroll"> + <nav className="px-4 py-6 space-y-6"> + {categories.map((category) => ( + <div key={category.title} className="space-y-3"> + {/* Category Header */} + <h3 className="px-2 text-sm font-semibold text-foreground uppercase tracking-wider border-b border-neutral-200 dark:border-neutral-700 pb-2"> + {category.title} + </h3> + + {/* Category Items */} + <div className="space-y-1"> + {category.items.map((item) => ( + <Link + key={item.name} + href={item.href} + className="flex items-center px-3 py-3 text-sm text-muted-foreground hover:text-foreground hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-lg transition-colors touch-manipulation min-h-[44px]" + > + <span className="flex-1">{item.name}</span> + <svg + className="w-4 h-4 opacity-40" + fill="none" + stroke="currentColor" + viewBox="0 0 24 24" > - <span className="flex-1">{item.name}</span> - <svg - className="w-4 h-4 opacity-40" - fill="none" - stroke="currentColor" - viewBox="0 0 24 24" - > - <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /> - </svg> - </Link> - ))} - </div> + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /> + </svg> + </Link> + ))} </div> - ))} + </div> + ))} - {/* Sale Section */} - <div className="space-y-3 pt-4 border-t border-neutral-200 dark:border-neutral-700"> - <h3 className="px-2 text-sm font-semibold text-red-600 uppercase tracking-wider"> - Special - </h3> - <Link - href="/sale" - className="flex items-center px-3 py-3 text-sm font-medium text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-950/30 rounded-lg transition-colors touch-manipulation min-h-[44px]" + {/* Sale Section */} + <div className="space-y-3 pt-4 border-t border-neutral-200 dark:border-neutral-700"> + <h3 className="px-2 text-sm font-semibold text-red-600 uppercase tracking-wider"> + Special + </h3> + <Link + href="/sale" + className="flex items-center px-3 py-3 text-sm font-medium text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-950/30 rounded-lg transition-colors touch-manipulation min-h-[44px]" + > + <span className="flex-1">Sale Items</span> + <svg + className="w-4 h-4" + fill="none" + stroke="currentColor" + viewBox="0 0 24 24" > - <span className="flex-1">Sale Items</span> - <svg - className="w-4 h-4" - fill="none" - stroke="currentColor" - viewBox="0 0 24 24" - > - <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /> - </svg> - </Link> - </div> - </nav> - </div> + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /> + </svg> + </Link> + </div> - {/* Footer Section */} - <div className="border-t dark:border-neutral-800 bg-neutral-50 dark:bg-neutral-900 px-4 py-4"> - <div className="grid grid-cols-2 gap-3"> - {/* Search Button */} - <Button - variant="outline" - size="sm" - className="flex items-center justify-center gap-2 h-10 text-xs font-medium" + {/* More Section */} + <div className="space-y-3 pt-4 border-t border-neutral-200 dark:border-neutral-700"> + <h3 className="px-2 text-sm font-semibold text-foreground uppercase tracking-wider"> + More + </h3> + <Link + href="/about" + className="flex items-center px-3 py-3 text-sm text-muted-foreground hover:text-foreground hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-lg transition-colors touch-manipulation min-h-[44px]" > - <Search className="h-4 w-4" /> - Search - </Button> - - {/* Account Button */} - <Button - variant="outline" - size="sm" - className="flex items-center justify-center gap-2 h-10 text-xs font-medium" + <span className="flex-1">About Us</span> + <svg + className="w-4 h-4 opacity-40" + fill="none" + stroke="currentColor" + viewBox="0 0 24 24" + > + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /> + </svg> + </Link> + <Link + href="/contact" + className="flex items-center px-3 py-3 text-sm text-muted-foreground hover:text-foreground hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-lg transition-colors touch-manipulation min-h-[44px]" > - <User className="h-4 w-4" /> - Account - </Button> - </div> - - {/* Quick Actions */} - <div className="grid grid-cols-4 gap-1 mt-4 pt-3 border-t border-neutral-200 dark:border-neutral-700"> - <button className="flex flex-col items-center justify-center space-y-2 p-3 rounded-xl hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-all duration-200 touch-manipulation group min-h-[72px] border border-transparent hover:border-neutral-200 dark:hover:border-neutral-700"> - <div className="relative"> - <Heart className="h-5 w-5 text-muted-foreground group-hover:text-foreground transition-colors flex-shrink-0" /> - {wishlistItems > 0 && ( - <Badge className="absolute -top-2 -right-2 h-4 w-4 rounded-full p-0 text-[10px] leading-none"> - {wishlistItems} - </Badge> - )} - </div> - <span className="text-[10px] text-muted-foreground group-hover:text-foreground transition-colors text-center leading-tight font-medium">Wishlist</span> - </button> - - <button className="flex flex-col items-center justify-center space-y-2 p-3 rounded-xl hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-all duration-200 touch-manipulation group min-h-[72px] border border-transparent hover:border-neutral-200 dark:hover:border-neutral-700"> - <div className="relative"> - <ShoppingBag className="h-5 w-5 text-muted-foreground group-hover:text-foreground transition-colors flex-shrink-0" /> - {cartItems > 0 && ( - <Badge className="absolute -top-2 -right-2 h-4 w-4 rounded-full p-0 text-[10px] leading-none"> - {cartItems} - </Badge> - )} - </div> - <span className="text-[10px] text-muted-foreground group-hover:text-foreground transition-colors text-center leading-tight font-medium">Cart</span> - </button> - - {/* Theme Toggle in Mobile Menu */} - <button - onClick={() => { - if (theme === 'dark') { - setTheme('light'); - } else { - setTheme('dark'); - } - }} - className="flex flex-col items-center justify-center space-y-2 p-3 rounded-xl hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-all duration-200 touch-manipulation group min-h-[72px] border border-transparent hover:border-neutral-200 dark:hover:border-neutral-700" + <span className="flex-1">Contact</span> + <svg + className="w-4 h-4 opacity-40" + fill="none" + stroke="currentColor" + viewBox="0 0 24 24" + > + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /> + </svg> + </Link> + <Link + href="/size-guide" + className="flex items-center px-3 py-3 text-sm text-muted-foreground hover:text-foreground hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-lg transition-colors touch-manipulation min-h-[44px]" > - <div className="relative"> - {theme === 'dark' ? ( - <Moon className="h-5 w-5 text-muted-foreground group-hover:text-foreground transition-colors flex-shrink-0" /> - ) : ( - <Sun className="h-5 w-5 text-muted-foreground group-hover:text-foreground transition-colors flex-shrink-0" /> - )} - </div> - <span className="text-[10px] text-muted-foreground group-hover:text-foreground transition-colors text-center leading-tight font-medium">Theme</span> - </button> - - <button className="flex flex-col items-center justify-center space-y-2 p-3 rounded-xl hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-all duration-200 touch-manipulation group min-h-[72px] border border-transparent hover:border-neutral-200 dark:hover:border-neutral-700"> - <div className="relative"> - <Globe className="h-5 w-5 text-muted-foreground group-hover:text-foreground transition-colors flex-shrink-0" /> - </div> - <span className="text-[10px] text-muted-foreground group-hover:text-foreground transition-colors text-center leading-tight font-medium">USD</span> - </button> + <span className="flex-1">Size Guide</span> + <svg + className="w-4 h-4 opacity-40" + fill="none" + stroke="currentColor" + viewBox="0 0 24 24" + > + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /> + </svg> + </Link> + <Link + href="/help" + className="flex items-center px-3 py-3 text-sm text-muted-foreground hover:text-foreground hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-lg transition-colors touch-manipulation min-h-[44px]" + > + <span className="flex-1">Help & FAQ</span> + <svg + className="w-4 h-4 opacity-40" + fill="none" + stroke="currentColor" + viewBox="0 0 24 24" + > + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /> + </svg> + </Link> </div> + </nav> + </div> + + {/* Footer Section */} + <div className="border-t dark:border-neutral-800 bg-neutral-50 dark:bg-neutral-900 px-4 py-4"> + <div className="grid grid-cols-2 gap-3"> + {/* Search Button */} + <Button + variant="outline" + size="sm" + className="flex items-center justify-center gap-2 h-10 text-xs font-medium" + > + <Search className="h-4 w-4" /> + Search + </Button> + + {/* Account Button */} + <Button + variant="outline" + size="sm" + className="flex items-center justify-center gap-2 h-10 text-xs font-medium" + > + <User className="h-4 w-4" /> + Account + </Button> + </div> + + {/* Quick Actions */} + <div className="grid grid-cols-4 gap-1 mt-4 pt-3 border-t border-neutral-200 dark:border-neutral-700"> + <button className="flex flex-col items-center justify-center space-y-2 p-3 rounded-xl hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-all duration-200 touch-manipulation group min-h-[72px] border border-transparent hover:border-neutral-200 dark:hover:border-neutral-700"> + <div className="relative"> + <Heart className="h-5 w-5 text-muted-foreground group-hover:text-foreground transition-colors flex-shrink-0" /> + {wishlistItems > 0 && ( + <Badge className="absolute -top-2 -right-2 h-4 w-4 rounded-full p-0 text-[10px] leading-none"> + {wishlistItems} + </Badge> + )} + </div> + <span className="text-[10px] text-muted-foreground group-hover:text-foreground transition-colors text-center leading-tight font-medium">Wishlist</span> + </button> + + <button className="flex flex-col items-center justify-center space-y-2 p-3 rounded-xl hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-all duration-200 touch-manipulation group min-h-[72px] border border-transparent hover:border-neutral-200 dark:hover:border-neutral-700"> + <div className="relative"> + <ShoppingBag className="h-5 w-5 text-muted-foreground group-hover:text-foreground transition-colors flex-shrink-0" /> + {cartItems > 0 && ( + <Badge className="absolute -top-2 -right-2 h-4 w-4 rounded-full p-0 text-[10px] leading-none"> + {cartItems} + </Badge> + )} + </div> + <span className="text-[10px] text-muted-foreground group-hover:text-foreground transition-colors text-center leading-tight font-medium">Cart</span> + </button> + + {/* Theme Toggle in Mobile Menu */} + <button + onClick={() => { + if (theme === 'dark') { + setTheme('light'); + } else { + setTheme('dark'); + } + }} + className="flex flex-col items-center justify-center space-y-2 p-3 rounded-xl hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-all duration-200 touch-manipulation group min-h-[72px] border border-transparent hover:border-neutral-200 dark:hover:border-neutral-700" + suppressHydrationWarning + > + <div className="relative"> + {theme === 'dark' ? ( + <Moon className="h-5 w-5 text-muted-foreground group-hover:text-foreground transition-colors flex-shrink-0" /> + ) : ( + <Sun className="h-5 w-5 text-muted-foreground group-hover:text-foreground transition-colors flex-shrink-0" /> + )} + </div> + <span className="text-[10px] text-muted-foreground group-hover:text-foreground transition-colors text-center leading-tight font-medium">Theme</span> + </button> + + <button className="flex flex-col items-center justify-center space-y-2 p-3 rounded-xl hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-all duration-200 touch-manipulation group min-h-[72px] border border-transparent hover:border-neutral-200 dark:hover:border-neutral-700"> + <div className="relative"> + <Globe className="h-5 w-5 text-muted-foreground group-hover:text-foreground transition-colors flex-shrink-0" /> + </div> + <span className="text-[10px] text-muted-foreground group-hover:text-foreground transition-colors text-center leading-tight font-medium">USD</span> + </button> </div> </div> - </SheetContent> - </Sheet> - ) : ( - <Button - variant="ghost" - size="icon" - className="md:hidden nav-button-transparent backdrop-blur-sm" - > - <Menu className="h-5 w-5" /> - </Button> - )} + </div> + </SheetContent> + </Sheet> {/* Logo */} <div className="flex items-center flex-1 md:flex-initial justify-center md:justify-start"> - <Link href="/" className="flex items-center space-x-2 ml-8 md:ml-0"> + <Link href="/" className="flex items-center space-x-2 ml-8 md:ml-0" suppressHydrationWarning> <Image src="/black-logo.png" alt="blcklst" @@ -495,40 +362,54 @@ export function Header() { </div> {/* Desktop Navigation */} - <NavigationMenu className="hidden md:flex"> - <NavigationMenuList> - {categories.map((category) => ( - <NavigationMenuItem key={category.title}> - <NavigationMenuTrigger - className="font-medium nav-button-transparent backdrop-blur-sm" - > - {category.title} - </NavigationMenuTrigger> - <NavigationMenuContent> - <div className="grid w-[400px] gap-3 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px] nav-dropdown-transparent"> - {category.items.map((item) => ( - <NavigationMenuLink key={item.name} asChild> - <Link - href={item.href} - className="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors nav-dropdown-item backdrop-blur-sm" - > - <div className="text-sm font-medium leading-none">{item.name}</div> - </Link> - </NavigationMenuLink> - ))} - </div> - </NavigationMenuContent> + <div className="hidden md:flex"> + <NavigationMenu + className="w-full" + delayDuration={150} + skipDelayDuration={250} + > + <NavigationMenuList> + {categories.map((category) => ( + <NavigationMenuItem key={category.title} value={category.title}> + <NavigationMenuTrigger + className="font-medium nav-button-transparent text-foreground hover:text-foreground/90 transition-all duration-200 group cursor-pointer" + aria-haspopup="true" + > + {category.title} + </NavigationMenuTrigger> + <NavigationMenuContent + className="data-[motion=from-start]:animate-in data-[motion=from-end]:animate-in data-[motion=to-start]:animate-out data-[motion=to-end]:animate-out data-[motion=from-start]:slide-in-from-left-52 data-[motion=from-end]:slide-in-from-right-52 data-[motion=to-start]:slide-out-to-left-52 data-[motion=to-end]:slide-out-to-right-52" + > + <div className="grid w-[400px] gap-3 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px] nav-dropdown-transparent"> + {category.items.map((item) => ( + <NavigationMenuLink key={item.name} asChild> + <Link + href={item.href} + className="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors nav-dropdown-item group" + > + <div className="text-sm font-medium leading-none group-hover:text-foreground transition-colors"> + {item.name} + </div> + <p className="line-clamp-2 text-xs leading-snug text-muted-foreground group-hover:text-foreground/80 transition-colors mt-1"> + Discover our latest {item.name.toLowerCase()} collection + </p> + </Link> + </NavigationMenuLink> + ))} + </div> + </NavigationMenuContent> + </NavigationMenuItem> + ))} + <NavigationMenuItem> + <NavigationMenuLink asChild> + <Link href="/sale" className="font-medium text-red-600 hover:text-red-700 px-4 py-2 rounded-md nav-button-transparent transition-colors cursor-pointer"> + Sale + </Link> + </NavigationMenuLink> </NavigationMenuItem> - ))} - <NavigationMenuItem> - <NavigationMenuLink asChild> - <Link href="/sale" className="font-medium text-red-600 hover:text-red-700 px-4 py-2 rounded-md nav-button-transparent backdrop-blur-sm transition-colors"> - Sale - </Link> - </NavigationMenuLink> - </NavigationMenuItem> - </NavigationMenuList> - </NavigationMenu> + </NavigationMenuList> + </NavigationMenu> + </div> {/* Search Bar */} <div className="hidden lg:flex flex-1 max-w-sm mx-8"> @@ -547,13 +428,13 @@ export function Header() { <Button variant="ghost" size="icon" - className="lg:hidden nav-button-transparent backdrop-blur-sm min-w-[44px] min-h-[44px]" + className="lg:hidden nav-button-transparent min-w-[44px] min-h-[44px] cursor-pointer" > <Search className="h-5 w-5" /> </Button> {/* Theme toggle */} - <div className="hidden sm:block"> + <div className="hidden sm:block" suppressHydrationWarning> <ThemeToggle /> </div> @@ -563,7 +444,7 @@ export function Header() { <Button variant="ghost" size="icon" - className="hidden sm:flex nav-button-transparent backdrop-blur-sm min-w-[44px] min-h-[44px]" + className="hidden sm:flex nav-button-transparent min-w-[44px] min-h-[44px] cursor-pointer" > <User className="h-5 w-5" /> </Button> @@ -595,7 +476,7 @@ export function Header() { <Button variant="ghost" size="icon" - className="relative hidden xs:flex nav-button-transparent backdrop-blur-sm min-w-[44px] min-h-[44px]" + className="relative hidden xs:flex nav-button-transparent min-w-[44px] min-h-[44px] cursor-pointer" > <Heart className="h-5 w-5" /> {wishlistItems > 0 && ( @@ -609,7 +490,7 @@ export function Header() { <Button variant="ghost" size="icon" - className="relative nav-button-transparent backdrop-blur-sm min-w-[44px] min-h-[44px]" + className="relative nav-button-transparent min-w-[44px] min-h-[44px] cursor-pointer" > <ShoppingBag className="h-4 w-4 sm:h-5 sm:w-5 text-muted-foreground flex-shrink-0" /> {cartItems > 0 && ( @@ -625,7 +506,7 @@ export function Header() { <Button variant="ghost" size="icon" - className="hidden md:flex nav-button-transparent backdrop-blur-sm min-w-[44px] min-h-[44px]" + className="hidden md:flex nav-button-transparent min-w-[44px] min-h-[44px] cursor-pointer" > <Globe className="h-5 w-5" /> </Button> diff --git a/frontend/src/components/loading.tsx b/frontend/src/components/loading.tsx new file mode 100644 index 0000000..cbd5c15 --- /dev/null +++ b/frontend/src/components/loading.tsx @@ -0,0 +1,111 @@ +"use client"; + +import { useEffect, useState } from "react"; +import Image from "next/image"; + +export function LoadingScreen() { + const [isVisible, setIsVisible] = useState(true); + + useEffect(() => { + // Hide loading screen after a short delay to ensure everything is ready + const timer = setTimeout(() => { + setIsVisible(false); + }, 1000); + + return () => clearTimeout(timer); + }, []); + + if (!isVisible) return null; + + return ( + <div className="fixed inset-0 z-[9999] bg-background flex items-center justify-center"> + {/* Background pattern */} + <div className="absolute inset-0 bg-gradient-to-br from-background via-background to-muted/20" /> + + {/* Loading content */} + <div className="relative z-10 flex flex-col items-center space-y-4"> + {/* Logo with animation */} + <div className="relative"> + <div className="animate-pulse"> + <Image + src="/black-logo.png" + alt="blcklst" + width={160} + height={50} + className="h-12 w-auto block dark:hidden" + priority + /> + <Image + src="/white-logo.png" + alt="blcklst" + width={160} + height={50} + className="h-12 w-auto hidden dark:block" + priority + /> + </div> + </div> + + {/* Loading text - closer to logo */} + <div className="text-center space-y-3 -mt-2"> + <p className="text-sm text-muted-foreground animate-pulse"> + not everyone gets blcklsted + </p> + + {/* Loading dots */} + <div className="flex items-center justify-center space-x-1"> + <div className="w-2 h-2 bg-foreground/60 rounded-full animate-bounce [animation-delay:-0.3s]"></div> + <div className="w-2 h-2 bg-foreground/60 rounded-full animate-bounce [animation-delay:-0.15s]"></div> + <div className="w-2 h-2 bg-foreground/60 rounded-full animate-bounce"></div> + </div> + </div> + + {/* Progress bar */} + <div className="w-64 h-1 bg-muted rounded-full overflow-hidden"> + <div className="h-full bg-foreground rounded-full animate-[loading_1s_ease-in-out_infinite]"></div> + </div> + </div> + </div> + ); +} + +export function PageWrapper({ children }: { children: React.ReactNode }) { + const [isLoading, setIsLoading] = useState(true); + const [showContent, setShowContent] = useState(false); + + useEffect(() => { + // Check if page is ready + const checkReady = () => { + if (document.readyState === 'complete') { + setTimeout(() => { + setIsLoading(false); + setTimeout(() => setShowContent(true), 100); + }, 800); + } + }; + + if (document.readyState === 'complete') { + checkReady(); + } else { + window.addEventListener('load', checkReady); + } + + return () => { + window.removeEventListener('load', checkReady); + }; + }, []); + + return ( + <> + {isLoading && <LoadingScreen />} + <div + className={`transition-opacity duration-500 ${ + showContent ? 'opacity-100' : 'opacity-0' + }`} + suppressHydrationWarning + > + {children} + </div> + </> + ); +}
\ No newline at end of file diff --git a/frontend/src/components/product-card.tsx b/frontend/src/components/product-card.tsx index 25717f4..e953e83 100644 --- a/frontend/src/components/product-card.tsx +++ b/frontend/src/components/product-card.tsx @@ -75,7 +75,7 @@ export function ProductCard({ onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} > - <Link href={`/product/${id}`}> + <Link href={`/products/${id}`}> <div className="relative overflow-hidden"> {/* Product Image */} <div className="aspect-[3/4] bg-neutral-100 dark:bg-neutral-800 relative overflow-hidden"> diff --git a/frontend/src/components/product-page.tsx b/frontend/src/components/product-page.tsx new file mode 100644 index 0000000..aff9d53 --- /dev/null +++ b/frontend/src/components/product-page.tsx @@ -0,0 +1,567 @@ +"use client"; + +import { useState } from "react"; +import Image from "next/image"; +import Link from "next/link"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Separator } from "@/components/ui/separator"; +import { + Heart, + Share2, + ShoppingBag, + Star, + Truck, + RefreshCw, + Shield, + Ruler, + ChevronLeft, + ChevronRight, + Minus, + Plus, + Check, +} from "lucide-react"; + +interface ProductPageProps { + productId?: string; +} + +export function ProductPage({ productId = "1" }: ProductPageProps) { + const [selectedImage, setSelectedImage] = useState(0); + const [selectedSize, setSelectedSize] = useState(""); + const [selectedColor, setSelectedColor] = useState(""); + const [quantity, setQuantity] = useState(1); + const [isWishlisted, setIsWishlisted] = useState(false); + + // Mock product data - in real app, this would come from an API + const product = { + id: productId, + name: "Oversized Cotton Hoodie", + brand: "blcklst", + price: 89, + originalPrice: 129, + rating: 4.5, + reviewCount: 234, + description: "A premium oversized cotton hoodie crafted for ultimate comfort and style. Made from 100% organic cotton with a soft fleece lining, this hoodie features a relaxed fit perfect for layering or wearing solo.", + features: [ + "100% organic cotton construction", + "Soft fleece interior lining", + "Kangaroo pocket with hidden phone compartment", + "Reinforced double-stitched seams", + "Pre-shrunk for lasting fit", + "Unisex design" + ], + images: [ + "/api/placeholder/600/800", + "/api/placeholder/600/800", + "/api/placeholder/600/800", + "/api/placeholder/600/800", + ], + colors: [ + { name: "Black", value: "#000000", available: true }, + { name: "White", value: "#FFFFFF", available: true }, + { name: "Gray", value: "#6B7280", available: true }, + { name: "Navy", value: "#1E3A8A", available: false }, + ], + sizes: [ + { name: "XS", available: true }, + { name: "S", available: true }, + { name: "M", available: true }, + { name: "L", available: true }, + { name: "XL", available: true }, + { name: "XXL", available: false }, + ], + inStock: true, + stockCount: 12, + }; + + const relatedProducts = [ + { + id: "2", + name: "Minimal T-Shirt", + price: 45, + originalPrice: 65, + image: "/api/placeholder/300/400", + rating: 4.3, + }, + { + id: "3", + name: "Denim Jacket", + price: 129, + originalPrice: 179, + image: "/api/placeholder/300/400", + rating: 4.7, + }, + { + id: "4", + name: "Cargo Pants", + price: 89, + originalPrice: 119, + image: "/api/placeholder/300/400", + rating: 4.2, + }, + { + id: "5", + name: "Sneakers", + price: 149, + originalPrice: 199, + image: "/api/placeholder/300/400", + rating: 4.6, + }, + ]; + + const reviews = [ + { + id: 1, + name: "Sarah M.", + rating: 5, + date: "2024-01-15", + comment: "Amazing quality! The fit is perfect and the material feels premium. Definitely worth the price.", + verified: true, + }, + { + id: 2, + name: "Alex K.", + rating: 4, + date: "2024-01-10", + comment: "Great hoodie, very comfortable. Only wish it came in more colors.", + verified: true, + }, + { + id: 3, + name: "Jordan L.", + rating: 5, + date: "2024-01-05", + comment: "Best hoodie I've ever owned. The oversized fit is exactly what I wanted.", + verified: true, + }, + ]; + + const nextImage = () => { + setSelectedImage((prev) => (prev + 1) % product.images.length); + }; + + const prevImage = () => { + setSelectedImage((prev) => (prev - 1 + product.images.length) % product.images.length); + }; + + const addToCart = () => { + if (!selectedSize || !selectedColor) { + alert("Please select size and color"); + return; + } + // Add to cart logic here + console.log("Added to cart:", { product, selectedSize, selectedColor, quantity }); + }; + + const renderStars = (rating: number) => { + return Array.from({ length: 5 }, (_, i) => ( + <Star + key={i} + className={`h-4 w-4 ${ + i < Math.floor(rating) + ? "fill-yellow-400 text-yellow-400" + : i < rating + ? "fill-yellow-400/50 text-yellow-400" + : "text-gray-300" + }`} + /> + )); + }; + + return ( + <div className="min-h-screen bg-background"> + {/* Breadcrumb */} + <div className="border-b"> + <div className="container mx-auto px-4 py-4"> + <nav className="flex items-center space-x-2 text-sm text-muted-foreground"> + <Link href="/" className="hover:text-foreground">Home</Link> + <span>/</span> + <Link href="/men" className="hover:text-foreground">Men</Link> + <span>/</span> + <Link href="/men/hoodies" className="hover:text-foreground">Hoodies</Link> + <span>/</span> + <span className="text-foreground">{product.name}</span> + </nav> + </div> + </div> + + <div className="container mx-auto px-4 py-8"> + <div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-12"> + {/* Product Images */} + <div className="space-y-4"> + {/* Main Image */} + <div className="relative aspect-[3/4] overflow-hidden rounded-lg bg-neutral-100 dark:bg-neutral-800"> + <Image + src={product.images[selectedImage]} + alt={product.name} + fill + className="object-cover" + priority + /> + {product.originalPrice && ( + <Badge className="absolute top-4 left-4 bg-red-500 hover:bg-red-600"> + {Math.round(((product.originalPrice - product.price) / product.originalPrice) * 100)}% OFF + </Badge> + )} + + {/* Navigation Arrows */} + <button + onClick={prevImage} + className="absolute left-4 top-1/2 -translate-y-1/2 bg-white/80 dark:bg-black/80 p-2 rounded-full hover:bg-white dark:hover:bg-black transition-colors" + > + <ChevronLeft className="h-5 w-5" /> + </button> + <button + onClick={nextImage} + className="absolute right-4 top-1/2 -translate-y-1/2 bg-white/80 dark:bg-black/80 p-2 rounded-full hover:bg-white dark:hover:bg-black transition-colors" + > + <ChevronRight className="h-5 w-5" /> + </button> + </div> + + {/* Thumbnail Images */} + <div className="grid grid-cols-4 gap-3"> + {product.images.map((image, index) => ( + <button + key={index} + onClick={() => setSelectedImage(index)} + className={`relative aspect-square overflow-hidden rounded-lg border-2 transition-colors ${ + selectedImage === index + ? "border-primary" + : "border-transparent hover:border-neutral-300" + }`} + > + <Image + src={image} + alt={`${product.name} ${index + 1}`} + fill + className="object-cover" + /> + </button> + ))} + </div> + </div> + + {/* Product Details */} + <div className="space-y-6"> + {/* Header */} + <div> + <p className="text-sm text-muted-foreground font-medium mb-2">{product.brand}</p> + <h1 className="text-3xl font-bold mb-4">{product.name}</h1> + + {/* Rating */} + <div className="flex items-center space-x-2 mb-4"> + <div className="flex items-center space-x-1"> + {renderStars(product.rating)} + </div> + <span className="text-sm text-muted-foreground"> + {product.rating} ({product.reviewCount} reviews) + </span> + </div> + + {/* Price */} + <div className="flex items-center space-x-3 mb-6"> + <span className="text-3xl font-bold">${product.price}</span> + {product.originalPrice && ( + <span className="text-xl text-muted-foreground line-through"> + ${product.originalPrice} + </span> + )} + </div> + </div> + + {/* Color Selection */} + <div className="space-y-3"> + <div className="flex items-center justify-between"> + <h3 className="font-medium">Color</h3> + {selectedColor && ( + <span className="text-sm text-muted-foreground capitalize">{selectedColor}</span> + )} + </div> + <div className="flex space-x-3"> + {product.colors.map((color) => ( + <button + key={color.name} + onClick={() => color.available && setSelectedColor(color.name)} + disabled={!color.available} + className={`relative w-10 h-10 rounded-full border-2 transition-all ${ + selectedColor === color.name + ? "border-primary scale-110" + : "border-neutral-300 hover:border-neutral-400" + } ${!color.available && "opacity-50 cursor-not-allowed"}`} + style={{ backgroundColor: color.value }} + > + {selectedColor === color.name && ( + <Check className="absolute inset-0 m-auto h-4 w-4 text-white" /> + )} + {!color.available && ( + <div className="absolute inset-0 bg-neutral-500/50 rounded-full" /> + )} + </button> + ))} + </div> + </div> + + {/* Size Selection */} + <div className="space-y-3"> + <div className="flex items-center justify-between"> + <h3 className="font-medium">Size</h3> + <Button variant="link" className="h-auto p-0 text-sm"> + <Ruler className="h-4 w-4 mr-1" /> + Size Guide + </Button> + </div> + <div className="grid grid-cols-3 gap-3"> + {product.sizes.map((size) => ( + <button + key={size.name} + onClick={() => size.available && setSelectedSize(size.name)} + disabled={!size.available} + className={`py-3 px-4 border rounded-lg text-sm font-medium transition-colors ${ + selectedSize === size.name + ? "border-primary bg-primary text-primary-foreground" + : size.available + ? "border-neutral-300 hover:border-neutral-400 hover:bg-white dark:hover:bg-white hover:text-black" + : "border-neutral-200 text-muted-foreground opacity-50 cursor-not-allowed" + }`} + > + {size.name} + </button> + ))} + </div> + </div> + + {/* Quantity */} + <div className="space-y-3"> + <h3 className="font-medium">Quantity</h3> + <div className="flex items-center space-x-3"> + <div className="flex items-center border rounded-lg"> + <button + onClick={() => setQuantity(Math.max(1, quantity - 1))} + className="p-3 rounded-md hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-colors focus:outline-none focus:ring-0 m-1" + > + <Minus className="h-4 w-4" /> + </button> + <span className="px-4 py-3 min-w-[60px] text-center">{quantity}</span> + <button + onClick={() => setQuantity(Math.min(product.stockCount, quantity + 1))} + className="p-3 rounded-md hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-colors focus:outline-none focus:ring-0 m-1" + > + <Plus className="h-4 w-4" /> + </button> + </div> + <span className="text-sm text-muted-foreground"> + {product.stockCount} items left + </span> + </div> + </div> + + {/* Add to Cart */} + <div className="space-y-3"> + <div className="flex space-x-3"> + <Button + onClick={addToCart} + className="flex-1 h-12" + disabled={!product.inStock} + > + <ShoppingBag className="h-5 w-5 mr-2" /> + {product.inStock ? "Add to Cart" : "Out of Stock"} + </Button> + <Button + variant="outline" + size="icon" + className="h-12 w-12" + onClick={() => setIsWishlisted(!isWishlisted)} + > + <Heart className={`h-5 w-5 ${isWishlisted ? "fill-red-500 text-red-500" : ""}`} /> + </Button> + <Button variant="outline" size="icon" className="h-12 w-12"> + <Share2 className="h-5 w-5" /> + </Button> + </div> + + {/* Features */} + <div className="grid grid-cols-3 gap-4 pt-4"> + <div className="flex items-center space-x-2 text-sm"> + <Truck className="h-4 w-4 text-muted-foreground" /> + <span>Free Shipping</span> + </div> + <div className="flex items-center space-x-2 text-sm"> + <RefreshCw className="h-4 w-4 text-muted-foreground" /> + <span>Free Returns</span> + </div> + <div className="flex items-center space-x-2 text-sm"> + <Shield className="h-4 w-4 text-muted-foreground" /> + <span>2 Year Warranty</span> + </div> + </div> + </div> + </div> + </div> + + {/* Product Details Tabs */} + <div className="mt-16"> + <Tabs defaultValue="description" className="w-full"> + <TabsList className="grid w-full grid-cols-3"> + <TabsTrigger value="description">Description</TabsTrigger> + <TabsTrigger value="specifications">Specifications</TabsTrigger> + <TabsTrigger value="reviews">Reviews ({product.reviewCount})</TabsTrigger> + </TabsList> + + <TabsContent value="description" className="mt-8"> + <div className="prose dark:prose-invert max-w-none"> + <p className="text-lg text-muted-foreground mb-6">{product.description}</p> + <h3 className="text-xl font-semibold mb-4">Features</h3> + <ul className="space-y-2"> + {product.features.map((feature, index) => ( + <li key={index} className="flex items-start space-x-2"> + <Check className="h-5 w-5 text-green-500 mt-0.5 flex-shrink-0" /> + <span>{feature}</span> + </li> + ))} + </ul> + </div> + </TabsContent> + + <TabsContent value="specifications" className="mt-8"> + <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> + <div className="space-y-4"> + <h3 className="text-lg font-semibold">Material & Care</h3> + <div className="space-y-2"> + <div className="flex justify-between"> + <span className="text-muted-foreground">Material:</span> + <span>100% Organic Cotton</span> + </div> + <div className="flex justify-between"> + <span className="text-muted-foreground">Weight:</span> + <span>400 GSM</span> + </div> + <div className="flex justify-between"> + <span className="text-muted-foreground">Care:</span> + <span>Machine Wash Cold</span> + </div> + <div className="flex justify-between"> + <span className="text-muted-foreground">Origin:</span> + <span>Made in Portugal</span> + </div> + </div> + </div> + <div className="space-y-4"> + <h3 className="text-lg font-semibold">Sizing</h3> + <div className="space-y-2"> + <div className="flex justify-between"> + <span className="text-muted-foreground">Fit:</span> + <span>Oversized</span> + </div> + <div className="flex justify-between"> + <span className="text-muted-foreground">Model Height:</span> + <span>6'0" / 183cm</span> + </div> + <div className="flex justify-between"> + <span className="text-muted-foreground">Model Size:</span> + <span>Size M</span> + </div> + </div> + </div> + </div> + </TabsContent> + + <TabsContent value="reviews" className="mt-8"> + <div className="space-y-6"> + <div className="flex items-center justify-between"> + <div> + <div className="flex items-center space-x-2 mb-2"> + <div className="flex items-center space-x-1"> + {renderStars(product.rating)} + </div> + <span className="text-2xl font-bold">{product.rating}</span> + <span className="text-muted-foreground">({product.reviewCount} reviews)</span> + </div> + <p className="text-sm text-muted-foreground">Based on verified purchases</p> + </div> + <Button variant="outline">Write a Review</Button> + </div> + + <Separator /> + + <div className="space-y-6"> + {reviews.map((review) => ( + <div key={review.id} className="space-y-3"> + <div className="flex items-center justify-between"> + <div className="flex items-center space-x-3"> + <div> + <div className="flex items-center space-x-2"> + <span className="font-medium">{review.name}</span> + {review.verified && ( + <Badge variant="secondary" className="text-xs"> + Verified Purchase + </Badge> + )} + </div> + <div className="flex items-center space-x-2 mt-1"> + <div className="flex items-center space-x-1"> + {renderStars(review.rating)} + </div> + <span className="text-sm text-muted-foreground">{review.date}</span> + </div> + </div> + </div> + </div> + <p className="text-muted-foreground">{review.comment}</p> + <Separator /> + </div> + ))} + </div> + </div> + </TabsContent> + </Tabs> + </div> + + {/* Related Products */} + <div className="mt-16"> + <h2 className="text-2xl font-bold mb-8">You might also like</h2> + <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6"> + {relatedProducts.map((item) => ( + <Link key={item.id} href={`/products/${item.id}`} className="group"> + <div className="space-y-3"> + <div className="relative aspect-[3/4] overflow-hidden rounded-lg bg-neutral-100 dark:bg-neutral-800"> + <Image + src={item.image} + alt={item.name} + fill + className="object-cover group-hover:scale-105 transition-transform duration-300" + /> + {item.originalPrice && ( + <Badge className="absolute top-3 left-3 bg-red-500 hover:bg-red-600"> + {Math.round(((item.originalPrice - item.price) / item.originalPrice) * 100)}% OFF + </Badge> + )} + </div> + <div> + <h3 className="font-medium group-hover:text-primary transition-colors"> + {item.name} + </h3> + <div className="flex items-center space-x-1 mt-1"> + {renderStars(item.rating)} + <span className="text-sm text-muted-foreground">({item.rating})</span> + </div> + <div className="flex items-center space-x-2 mt-2"> + <span className="font-semibold">${item.price}</span> + {item.originalPrice && ( + <span className="text-sm text-muted-foreground line-through"> + ${item.originalPrice} + </span> + )} + </div> + </div> + </div> + </Link> + ))} + </div> + </div> + </div> + </div> + ); +}
\ No newline at end of file diff --git a/frontend/src/components/theme-provider.tsx b/frontend/src/components/theme-provider.tsx index ab9d9da..d6e536b 100644 --- a/frontend/src/components/theme-provider.tsx +++ b/frontend/src/components/theme-provider.tsx @@ -3,7 +3,7 @@ import * as React from "react" import { ThemeProvider as NextThemesProvider } from "next-themes" import type { ThemeProviderProps } from "next-themes" - + export function ThemeProvider({ children, ...props }: ThemeProviderProps) { return <NextThemesProvider {...props}>{children}</NextThemesProvider> }
\ No newline at end of file diff --git a/frontend/src/components/theme-toggle.tsx b/frontend/src/components/theme-toggle.tsx index 7532775..09a5fb8 100644 --- a/frontend/src/components/theme-toggle.tsx +++ b/frontend/src/components/theme-toggle.tsx @@ -14,26 +14,6 @@ import { export function ThemeToggle() { const { setTheme, theme } = useTheme() - const [mounted, setMounted] = React.useState(false) - - // Ensure we only render the correct icon after mounting to avoid hydration mismatch - React.useEffect(() => { - setMounted(true) - }, []) - - if (!mounted) { - return ( - <Button - variant="ghost" - size="icon" - className="nav-button-transparent backdrop-blur-sm min-w-[44px] min-h-[44px]" - disabled - > - <Monitor className="h-5 w-5" /> - <span className="sr-only">Toggle theme</span> - </Button> - ) - } return ( <DropdownMenu> @@ -41,7 +21,8 @@ export function ThemeToggle() { <Button variant="ghost" size="icon" - className="nav-button-transparent backdrop-blur-sm min-w-[44px] min-h-[44px]" + className="nav-button-transparent min-w-[44px] min-h-[44px] cursor-pointer" + suppressHydrationWarning > {/* Simple icon transitions with consistent styling */} <Sun className="h-5 w-5 rotate-0 scale-100 transition-all duration-500 ease-in-out dark:-rotate-180 dark:scale-0" /> diff --git a/frontend/src/components/ui/navigation-menu.tsx b/frontend/src/components/ui/navigation-menu.tsx index 1199945..65b5803 100644 --- a/frontend/src/components/ui/navigation-menu.tsx +++ b/frontend/src/components/ui/navigation-menu.tsx @@ -21,6 +21,8 @@ function NavigationMenu({ "group/navigation-menu relative flex max-w-max flex-1 items-center justify-center", className )} + delayDuration={150} + skipDelayDuration={300} {...props} > {children} @@ -59,7 +61,7 @@ function NavigationMenuItem({ } const navigationMenuTriggerStyle = cva( - "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1" + "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 [&]:touch-action-manipulation" ) function NavigationMenuTrigger({ @@ -67,15 +69,26 @@ function NavigationMenuTrigger({ children, ...props }: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) { + const [isHovered, setIsHovered] = React.useState(false) + return ( <NavigationMenuPrimitive.Trigger data-slot="navigation-menu-trigger" className={cn(navigationMenuTriggerStyle(), "group", className)} + onMouseEnter={() => setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + style={{ + WebkitTapHighlightColor: 'transparent', + touchAction: 'manipulation', + }} {...props} > {children}{" "} <ChevronDownIcon - className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180" + className={cn( + "relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180", + "transform-gpu will-change-transform" + )} aria-hidden="true" /> </NavigationMenuPrimitive.Trigger> @@ -91,6 +104,7 @@ function NavigationMenuContent({ data-slot="navigation-menu-content" className={cn( "data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto", + "transform-gpu will-change-transform backface-visibility-hidden", "group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none", className )} @@ -113,6 +127,7 @@ function NavigationMenuViewport({ data-slot="navigation-menu-viewport" className={cn( "origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]", + "transform-gpu will-change-transform backface-visibility-hidden", className )} {...props} diff --git a/frontend/src/components/ui/tabs.tsx b/frontend/src/components/ui/tabs.tsx new file mode 100644 index 0000000..d6bb57f --- /dev/null +++ b/frontend/src/components/ui/tabs.tsx @@ -0,0 +1,55 @@ +"use client" + +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" + +import { cn } from "@/lib/utils" + +const Tabs = TabsPrimitive.Root + +const TabsList = React.forwardRef< + React.ElementRef<typeof TabsPrimitive.List>, + React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> +>(({ className, ...props }, ref) => ( + <TabsPrimitive.List + ref={ref} + className={cn( + "inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground", + className + )} + {...props} + /> +)) +TabsList.displayName = TabsPrimitive.List.displayName + +const TabsTrigger = React.forwardRef< + React.ElementRef<typeof TabsPrimitive.Trigger>, + React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger> +>(({ className, ...props }, ref) => ( + <TabsPrimitive.Trigger + ref={ref} + className={cn( + "inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm", + className + )} + {...props} + /> +)) +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName + +const TabsContent = React.forwardRef< + React.ElementRef<typeof TabsPrimitive.Content>, + React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content> +>(({ className, ...props }, ref) => ( + <TabsPrimitive.Content + ref={ref} + className={cn( + "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", + className + )} + {...props} + /> +)) +TabsContent.displayName = TabsPrimitive.Content.displayName + +export { Tabs, TabsList, TabsTrigger, TabsContent }
\ No newline at end of file |