diff options
author | 2025-05-29 20:57:31 +0530 | |
---|---|---|
committer | 2025-05-29 20:57:31 +0530 | |
commit | ca3ae0db6e8e3f2cf99423797c60f5c2cc66a780 (patch) | |
tree | f1d5fa2174283a811a5422cf148a96f435b461a6 | |
parent | a8fc4438ff1b890a78b7d2ba470011d8f87c6043 (diff) | |
download | blcklst-ca3ae0db6e8e3f2cf99423797c60f5c2cc66a780.tar.gz blcklst-ca3ae0db6e8e3f2cf99423797c60f5c2cc66a780.tar.bz2 blcklst-ca3ae0db6e8e3f2cf99423797c60f5c2cc66a780.zip |
feat: added the product page and enhanced the UI
- added the product page where the product details is shown
- enhanced the nav bar to handle the rendering
- added the loading screen to the page
- fixed the rendering method to the navigation-menu
-rw-r--r-- | frontend/package-lock.json | 37 | ||||
-rw-r--r-- | frontend/package.json | 1 | ||||
-rw-r--r-- | frontend/src/app/globals.css | 686 | ||||
-rw-r--r-- | frontend/src/app/layout.tsx | 18 | ||||
-rw-r--r-- | frontend/src/app/loading.tsx | 54 | ||||
-rw-r--r-- | frontend/src/app/men/page.tsx | 178 | ||||
-rw-r--r-- | frontend/src/app/page.tsx | 117 | ||||
-rw-r--r-- | frontend/src/app/products/[id]/page.tsx | 32 | ||||
-rw-r--r-- | frontend/src/app/unisex/page.tsx | 178 | ||||
-rw-r--r-- | frontend/src/app/women/page.tsx | 178 | ||||
-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 |
19 files changed, 1821 insertions, 1106 deletions
diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c0135a9..33db14d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -19,6 +19,7 @@ "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slider": "^1.3.5", "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-tabs": "^1.1.12", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.511.0", @@ -1696,6 +1697,36 @@ } } }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.12.tgz", + "integrity": "sha512-GTVAlRVrQrSw3cEARM0nAx73ixrWDPNZAruETn3oHCNP6SbZ/hNxdxp+u7VkIEv3/sFoLq1PfcHrl7Pnp0CDpw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", @@ -2230,7 +2261,7 @@ "version": "19.1.6", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.6.tgz", "integrity": "sha512-JeG0rEWak0N6Itr6QUx+X60uQmN+5t3j9r/OVDtWzFXKaj6kD1BwJzOksD0FF6iWxZlbE1kB0q9vtnU2ekqa1Q==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -2240,7 +2271,7 @@ "version": "19.1.5", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.5.tgz", "integrity": "sha512-CMCjrWucUBZvohgZxkjd6S9h0nZxXjzus6yDfUb+xLxYM7VvjKNH1tQrE9GWLql1XoOP4/Ds3bwFqShHUYraGg==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.0.0" @@ -3314,7 +3345,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { diff --git a/frontend/package.json b/frontend/package.json index 72820a5..595afee 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,6 +20,7 @@ "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slider": "^1.3.5", "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-tabs": "^1.1.12", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.511.0", diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 5e91d83..8b5bbb3 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -3,13 +3,10 @@ @custom-variant dark (&:is(.dark *)); -/* Custom responsive breakpoints */ -@custom-variant xs (&:is(@media (min-width: 475px) *)); - @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); - --font-sans: var(--font-inter), ui-sans-serif, system-ui, sans-serif; + --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif; --font-mono: ui-monospace, "SFMono-Regular", "Consolas", monospace; --color-sidebar-ring: var(--sidebar-ring); --color-sidebar-border: var(--sidebar-border); @@ -124,698 +121,137 @@ } } -/* Complete override for ghost variant default styles */ -/* This targets the specific ghost variant classes from the button component */ +/* Simple transparent navigation buttons */ .nav-button-transparent { background: transparent !important; background-color: transparent !important; - background-image: none !important; border: none !important; - border-color: transparent !important; box-shadow: none !important; - --tw-bg-opacity: 0 !important; - --accent: transparent !important; - --background: transparent !important; - --tw-border-opacity: 0 !important; - --tw-shadow: none !important; - --tw-ring-shadow: none !important; - --tw-ring-offset-shadow: none !important; - cursor: pointer !important; + backdrop-filter: none !important; + border-radius: 0 !important; + transition: color 0.2s ease !important; } -/* Force override ghost variant styles completely */ -button[class*="nav-button-transparent"], -[data-slot="button"][class*="nav-button-transparent"], -[data-slot="navigation-menu-trigger"][class*="nav-button-transparent"] { +/* Remove all hover backgrounds */ +.nav-button-transparent:hover { background: transparent !important; background-color: transparent !important; - border: none !important; box-shadow: none !important; -} - -/* Override ghost variant hover specifically */ -button[class*="nav-button-transparent"][class*="hover:bg-accent"], -[data-slot="button"][class*="nav-button-transparent"][class*="hover:bg-accent"], -[data-slot="navigation-menu-trigger"][class*="nav-button-transparent"][class*="hover:bg-accent"] { - background: transparent !important; - background-color: transparent !important; - border: none !important; -} - -/* Dark mode ghost variant overrides */ -.dark button[class*="nav-button-transparent"][class*="dark:hover:bg-accent"], -.dark [data-slot="button"][class*="nav-button-transparent"][class*="dark:hover:bg-accent"], -.dark [data-slot="navigation-menu-trigger"][class*="nav-button-transparent"][class*="dark:hover:bg-accent"] { - background: transparent !important; - background-color: transparent !important; - border: none !important; -} - -/* Remove focus rings and outlines for nav buttons */ -.nav-button-transparent:focus, -.nav-button-transparent:focus-visible { - outline: none !important; - ring: none !important; - --tw-ring-shadow: none !important; - border: none !important; - background: transparent !important; - background-color: transparent !important; -} - -/* Light mode subtle hover effects - only show on hover */ -.nav-button-transparent:hover { - background: rgba(0, 0, 0, 0.08) !important; - background-color: rgba(0, 0, 0, 0.08) !important; - --tw-bg-opacity: 1 !important; + backdrop-filter: none !important; border: none !important; - transition: background-color 0.2s ease-in-out !important; } -/* Dark mode even more subtle hover effects */ +/* Remove all dark mode hover backgrounds */ .dark .nav-button-transparent:hover { - background: rgba(255, 255, 255, 0.06) !important; - background-color: rgba(255, 255, 255, 0.06) !important; - transition: background-color 0.2s ease-in-out !important; -} - -/* Override NavigationMenuTrigger specific classes with maximum specificity */ -.nav-button-transparent.bg-background { background: transparent !important; background-color: transparent !important; - border: none !important; -} - -/* Focus states - only subtle background, no borders or rings */ -.nav-button-transparent:focus:not(:hover) { - background: rgba(0, 0, 0, 0.04) !important; - background-color: rgba(0, 0, 0, 0.04) !important; - border: none !important; - box-shadow: none !important; -} - -.dark .nav-button-transparent:focus:not(:hover) { - background: rgba(255, 255, 255, 0.03) !important; - background-color: rgba(255, 255, 255, 0.03) !important; - border: none !important; box-shadow: none !important; -} - -/* Override open states */ -.nav-button-transparent[data-state="open"] { - background: rgba(0, 0, 0, 0.08) !important; - background-color: rgba(0, 0, 0, 0.08) !important; - border: none !important; -} - -.dark .nav-button-transparent[data-state="open"] { - background: rgba(255, 255, 255, 0.06) !important; - background-color: rgba(255, 255, 255, 0.06) !important; + backdrop-filter: none !important; border: none !important; } -/* Ensure all shadcn/ui utility classes are overridden with high specificity */ -button.nav-button-transparent, -[data-slot="button"].nav-button-transparent, -[data-slot="navigation-menu-trigger"].nav-button-transparent { +/* Remove all focus backgrounds */ +.nav-button-transparent:focus { background: transparent !important; background-color: transparent !important; - border: none !important; - border-color: transparent !important; - box-shadow: none !important; - cursor: pointer !important; -} - -/* Hover states for all button types */ -button.nav-button-transparent:hover, -[data-slot="button"].nav-button-transparent:hover, -[data-slot="navigation-menu-trigger"].nav-button-transparent:hover { - background: rgba(0, 0, 0, 0.08) !important; - background-color: rgba(0, 0, 0, 0.08) !important; - border: none !important; - transition: background-color 0.2s ease-in-out !important; - cursor: pointer !important; -} - -/* Focus states for all button types */ -button.nav-button-transparent:focus, -[data-slot="button"].nav-button-transparent:focus, -[data-slot="navigation-menu-trigger"].nav-button-transparent:focus { - background: rgba(0, 0, 0, 0.04) !important; - background-color: rgba(0, 0, 0, 0.04) !important; - border: none !important; - box-shadow: none !important; outline: none !important; - cursor: pointer !important; -} - -/* Dark mode hover and focus states */ -.dark button.nav-button-transparent:hover, -.dark [data-slot="button"].nav-button-transparent:hover, -.dark [data-slot="navigation-menu-trigger"].nav-button-transparent:hover { - background: rgba(255, 255, 255, 0.06) !important; - background-color: rgba(255, 255, 255, 0.06) !important; - border: none !important; - transition: background-color 0.2s ease-in-out !important; - cursor: pointer !important; -} - -.dark button.nav-button-transparent:focus, -.dark [data-slot="button"].nav-button-transparent:focus, -.dark [data-slot="navigation-menu-trigger"].nav-button-transparent:focus { - background: rgba(255, 255, 255, 0.03) !important; - background-color: rgba(255, 255, 255, 0.03) !important; - border: none !important; box-shadow: none !important; - outline: none !important; - cursor: pointer !important; -} - -/* Dropdown and navigation item transparency */ -.nav-dropdown-transparent { - background: rgba(255, 255, 255, 0.85) !important; - backdrop-filter: blur(20px) saturate(180%) !important; - border: 1px solid rgba(255, 255, 255, 0.2) !important; - box-shadow: - 0 8px 32px rgba(0, 0, 0, 0.12), - 0 2px 16px rgba(0, 0, 0, 0.08), - inset 0 1px 0 rgba(255, 255, 255, 0.4) !important; - border-radius: 0.75rem !important; -} - -.dark .nav-dropdown-transparent { - background: rgba(15, 15, 15, 0.85) !important; - border: 1px solid rgba(82, 82, 82, 0.3) !important; - box-shadow: - 0 8px 32px rgba(0, 0, 0, 0.4), - 0 2px 16px rgba(0, 0, 0, 0.3), - inset 0 1px 0 rgba(255, 255, 255, 0.08) !important; -} - -.nav-dropdown-item { - background: transparent !important; - border: none !important; - transition: background-color 0.2s ease-in-out !important; -} - -.nav-dropdown-item:hover { - background: rgba(0, 0, 0, 0.08) !important; - border: none !important; -} - -.dark .nav-dropdown-item:hover { - background: rgba(255, 255, 255, 0.08) !important; - border: none !important; -} - -/* Additional overrides for complete transparency */ -/* Handle any remaining shadcn/ui styling conflicts */ -.nav-button-transparent[data-variant="ghost"] { - background: transparent !important; - background-color: transparent !important; - border: none !important; - cursor: pointer !important; -} - -.nav-button-transparent[data-variant="ghost"]:hover { - background: rgba(0, 0, 0, 0.08) !important; - background-color: rgba(0, 0, 0, 0.08) !important; - border: none !important; - cursor: pointer !important; -} - -.dark .nav-button-transparent[data-variant="ghost"]:hover { - background: rgba(255, 255, 255, 0.06) !important; - background-color: rgba(255, 255, 255, 0.06) !important; - border: none !important; - cursor: pointer !important; -} - -/* Override any remaining background utilities */ -.nav-button-transparent.bg-transparent, -.nav-button-transparent.bg-accent, -.nav-button-transparent.hover\:bg-accent, -.nav-button-transparent.focus\:bg-accent { - background: transparent !important; - background-color: transparent !important; - border: none !important; - cursor: pointer !important; -} - -/* Ensure link elements in navigation also have transparent styling */ -a.nav-button-transparent { - background: transparent !important; - background-color: transparent !important; - border: none !important; - text-decoration: none !important; - cursor: pointer !important; -} - -a.nav-button-transparent:hover { - background: rgba(0, 0, 0, 0.08) !important; - background-color: rgba(0, 0, 0, 0.08) !important; - border: none !important; - text-decoration: none !important; - cursor: pointer !important; -} - -.dark a.nav-button-transparent:hover { - background: rgba(255, 255, 255, 0.06) !important; - background-color: rgba(255, 255, 255, 0.06) !important; - border: none !important; - text-decoration: none !important; - cursor: pointer !important; -} - -/* Ultra-specific overrides to ensure transparency takes precedence */ -/* Handle all possible combinations of button classes */ -button.nav-button-transparent.hover\:bg-accent, -button.nav-button-transparent.dark\:hover\:bg-accent\/50, -button.nav-button-transparent[class*="hover:bg"], -[data-slot="button"].nav-button-transparent.hover\:bg-accent, -[data-slot="button"].nav-button-transparent.dark\:hover\:bg-accent\/50, -[data-slot="button"].nav-button-transparent[class*="hover:bg"], -[data-slot="navigation-menu-trigger"].nav-button-transparent.hover\:bg-accent, -[data-slot="navigation-menu-trigger"].nav-button-transparent.dark\:hover\:bg-accent\/50, -[data-slot="navigation-menu-trigger"].nav-button-transparent[class*="hover:bg"] { - background: transparent !important; - background-color: transparent !important; + backdrop-filter: none !important; border: none !important; } -/* Override any CSS custom properties that might affect background */ -.nav-button-transparent { - --tw-bg-accent: transparent !important; - --tw-bg-background: transparent !important; - --tw-bg-muted: transparent !important; - --tw-bg-secondary: transparent !important; -} - -/* Ensure no background on any state for nav buttons */ -.nav-button-transparent, -.nav-button-transparent:not(:hover):not(:focus):not(:active) { +.dark .nav-button-transparent:focus { background: transparent !important; background-color: transparent !important; box-shadow: none !important; + backdrop-filter: none !important; border: none !important; } -/* Maximum specificity overrides - nuclear option for transparency */ -html body header button.nav-button-transparent, -html body header [data-slot="button"].nav-button-transparent, -html body header [data-slot="navigation-menu-trigger"].nav-button-transparent, -html body header div button.nav-button-transparent, -html body header div [data-slot="button"].nav-button-transparent, -html body header div [data-slot="navigation-menu-trigger"].nav-button-transparent { +/* Remove any active state backgrounds */ +.nav-button-transparent:active { background: transparent !important; background-color: transparent !important; - background-image: none !important; - border: none !important; - border-color: transparent !important; box-shadow: none !important; backdrop-filter: none !important; -} - -/* Override any inline styles or computed styles */ -.nav-button-transparent[style*="background"] { - background: transparent !important; - background-color: transparent !important; -} - -/* Target specific button classes that might have background */ -.nav-button-transparent.inline-flex, -.nav-button-transparent[class*="inline-flex"] { - background: transparent !important; - background-color: transparent !important; border: none !important; } -/* Enhanced link element transparency for Sale button and similar nav links */ -/* Apply maximum specificity for link elements in navigation */ -html body header a.nav-button-transparent, -html body header div a.nav-button-transparent, -html body header nav a.nav-button-transparent, -[data-slot="navigation-menu-link"].nav-button-transparent { +.dark .nav-button-transparent:active { background: transparent !important; background-color: transparent !important; - background-image: none !important; - border: none !important; - border-color: transparent !important; box-shadow: none !important; backdrop-filter: none !important; - cursor: pointer !important; -} - -/* Override any NavigationMenuLink default styles */ -a.nav-button-transparent[data-slot="navigation-menu-link"], -[data-slot="navigation-menu-link"].nav-button-transparent { - background: transparent !important; - background-color: transparent !important; - border: none !important; - box-shadow: none !important; -} - -/* Ensure link hover states maintain transparency in default state */ -a.nav-button-transparent:not(:hover):not(:focus):not(:active) { - background: transparent !important; - background-color: transparent !important; border: none !important; } -/* Specific overrides for NavigationMenuLink component (Sale button) */ -[data-slot="navigation-menu-link"].nav-button-transparent { - background: transparent !important; - background-color: transparent !important; - border: none !important; - box-shadow: none !important; - cursor: pointer !important; +/* Navigation dropdowns */ +.nav-dropdown-transparent { + background: rgba(255, 255, 255, 0.95) !important; + backdrop-filter: blur(20px) !important; + border: 1px solid rgba(255, 255, 255, 0.3) !important; + border-radius: 0.75rem !important; } -/* Override NavigationMenuLink hover and focus states */ -[data-slot="navigation-menu-link"].nav-button-transparent.hover\:bg-accent, -[data-slot="navigation-menu-link"].nav-button-transparent.focus\:bg-accent, -[data-slot="navigation-menu-link"].nav-button-transparent[class*="hover:bg-accent"], -[data-slot="navigation-menu-link"].nav-button-transparent[class*="focus:bg-accent"], -[data-slot="navigation-menu-link"].nav-button-transparent[class*="data-[active=true]:bg-accent"] { - background: transparent !important; - background-color: transparent !important; - border: none !important; +.dark .nav-dropdown-transparent { + background: rgba(23, 23, 23, 0.95) !important; + border: 1px solid rgba(82, 82, 82, 0.4) !important; } -/* Override NavigationMenuLink data states */ -[data-slot="navigation-menu-link"].nav-button-transparent[data-active="true"] { +/* Dropdown items */ +.nav-dropdown-item { background: transparent !important; - background-color: transparent !important; border: none !important; + transition: all 0.2s ease !important; } -/* Ensure NavigationMenuLink has proper hover effect */ -[data-slot="navigation-menu-link"].nav-button-transparent:hover { - background: rgba(0, 0, 0, 0.08) !important; - background-color: rgba(0, 0, 0, 0.08) !important; - border: none !important; - transition: background-color 0.2s ease-in-out !important; -} - -.dark [data-slot="navigation-menu-link"].nav-button-transparent:hover { - background: rgba(255, 255, 255, 0.06) !important; - background-color: rgba(255, 255, 255, 0.06) !important; -} - -/* Enhanced Theme Toggle Transparency Effects */ -/* Theme toggle specific enhancements for better navbar integration */ -.nav-button-transparent.group { - position: relative; - overflow: hidden; - backdrop-filter: blur(8px) saturate(150%); - border-radius: 0.5rem; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); -} - -/* Enhanced hover state for theme toggle */ -.nav-button-transparent.group:hover { - background: rgba(0, 0, 0, 0.08) !important; - background-color: rgba(0, 0, 0, 0.08) !important; - transform: scale(1.05); - backdrop-filter: blur(12px) saturate(180%); +.nav-dropdown-item:hover { + background: rgba(0, 0, 0, 0.06) !important; } -.dark .nav-button-transparent.group:hover { +.dark .nav-dropdown-item:hover { background: rgba(255, 255, 255, 0.06) !important; - background-color: rgba(255, 255, 255, 0.06) !important; -} - -/* Active state for theme toggle */ -.nav-button-transparent.group:active { - transform: scale(0.95); - transition: transform 0.1s ease-in-out; -} - -/* Enhanced focus state for theme toggle */ -.nav-button-transparent.group:focus-visible { - background: rgba(0, 0, 0, 0.04) !important; - background-color: rgba(0, 0, 0, 0.04) !important; - box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1) !important; - outline: none !important; -} - -.dark .nav-button-transparent.group:focus-visible { - background: rgba(255, 255, 255, 0.03) !important; - background-color: rgba(255, 255, 255, 0.03) !important; - box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1) !important; -} - - -/* Enhanced dropdown items for theme toggle */ -.nav-dropdown-item.group\/item { - background: transparent !important; - border: none !important; - border-radius: 0.5rem !important; - transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1) !important; - margin: 2px !important; - padding: 0.5rem 0.75rem !important; -} - -.nav-dropdown-item.group\/item:hover { - background: rgba(0, 0, 0, 0.08) !important; - backdrop-filter: blur(8px) !important; - transform: translateX(2px) !important; -} - -.dark .nav-dropdown-item.group\/item:hover { - background: rgba(255, 255, 255, 0.08) !important; -} - -/* Smooth icon transitions for theme toggle */ -.nav-button-transparent.group svg { - transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); - filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1)); -} - -.dark .nav-button-transparent.group svg { - filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3)); -} - -/* Enhanced glow effect for theme toggle */ -.nav-button-transparent.group:hover svg { - filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.15)); -} - -.dark .nav-button-transparent.group:hover svg { - filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.4)); -} - -/* Ultra-smooth dropdown animations */ -@keyframes dropdown-in { - from { - opacity: 0; - transform: scale(0.95) translateY(-4px); - } - to { - opacity: 1; - transform: scale(1) translateY(0); - } -} - -@keyframes dropdown-out { - from { - opacity: 1; - transform: scale(1) translateY(0); - } - to { - opacity: 0; - transform: scale(0.95) translateY(-4px); - } -} - -/* Apply dropdown animation to nav dropdowns */ -.nav-dropdown-transparent[data-state="open"] { - animation: dropdown-in 0.2s cubic-bezier(0.4, 0, 0.2, 1); -} - -/* Improved checkmark animation */ -.nav-dropdown-item span[class*="animate-in"] { - animation: checkmark-in 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); -} - -@keyframes checkmark-in { - from { - opacity: 0; - transform: scale(0.3) rotate(-12deg); - } - to { - opacity: 1; - transform: scale(1) rotate(0deg); - } } -/* Prevent any conflicting backgrounds on theme toggle */ -.nav-button-transparent.group[data-state="open"] { - background: rgba(0, 0, 0, 0.08) !important; - background-color: rgba(0, 0, 0, 0.08) !important; - border: none !important; - transform: scale(1.05); +/* Prevent flash of unstyled content */ +html { + visibility: hidden; + opacity: 0; } -.dark .nav-button-transparent.group[data-state="open"] { - background: rgba(255, 255, 255, 0.08) !important; - backdrop-filter: blur(8px) !important; - border-radius: 0.5rem !important; +html.loaded { + visibility: visible; + opacity: 1; + transition: opacity 0.1s ease-in-out; } -/* Mobile-friendly enhancements */ -@media (max-width: 768px) { - /* Ensure touch targets are at least 44px */ - .touch-manipulation { - touch-action: manipulation; - -webkit-tap-highlight-color: transparent; - } - - /* Better mobile spacing for action buttons */ - .mobile-action-buttons { - gap: 0.25rem !important; - } - - /* Mobile menu improvements */ - .mobile-menu-item { - min-height: 44px; - padding: 0.75rem 1rem; - display: flex; - align-items: center; - justify-content: space-between; - border-radius: 0.5rem; - transition: all 0.2s ease-in-out; - } - - .mobile-menu-item:active { - transform: scale(0.98); - background: rgba(0, 0, 0, 0.05); - } - - .dark .mobile-menu-item:active { - background: rgba(255, 255, 255, 0.05); - } - - /* Prevent text selection on mobile UI elements */ - .no-select { - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - } - - /* Improve mobile sheet content scrolling */ - .mobile-sheet-content { - overscroll-behavior: contain; - -webkit-overflow-scrolling: touch; - } - - /* Hide scrollbar in mobile menu while maintaining scroll functionality */ - [data-radix-dialog-content] { - scrollbar-width: none; /* Firefox */ - -ms-overflow-style: none; /* Internet Explorer and Edge */ - } - - [data-radix-dialog-content]::-webkit-scrollbar { - display: none; /* Chrome, Safari, Opera */ - width: 0; - height: 0; - } - - /* Specifically target Sheet content scrolling areas */ - [data-radix-dialog-content] .overflow-y-auto, - [data-radix-dialog-content] .overflow-auto { - scrollbar-width: none; /* Firefox */ - -ms-overflow-style: none; /* Internet Explorer and Edge */ - } - - [data-radix-dialog-content] .overflow-y-auto::-webkit-scrollbar, - [data-radix-dialog-content] .overflow-auto::-webkit-scrollbar { - display: none; /* Chrome, Safari, Opera */ - width: 0; - height: 0; - } - - /* Mobile navigation specific scrollbar hiding */ - .mobile-nav-scroll { - scrollbar-width: none !important; /* Firefox */ - -ms-overflow-style: none !important; /* Internet Explorer and Edge */ - } - - .mobile-nav-scroll::-webkit-scrollbar { - display: none !important; /* Chrome, Safari, Opera */ - width: 0 !important; - height: 0 !important; - background: transparent !important; - } - - /* Additional scrollbar hiding for all mobile menu elements */ - [data-radix-dialog-content] *, - [data-radix-dialog-content] *::-webkit-scrollbar { - scrollbar-width: none !important; - -ms-overflow-style: none !important; - display: none !important; - width: 0 !important; - height: 0 !important; - } +/* Ensure smooth theme transitions */ +* { + transition: background-color 0.15s ease-in-out, color 0.15s ease-in-out, border-color 0.15s ease-in-out; } -/* Small mobile optimizations */ -@media (max-width: 475px) { - /* Tighter spacing on very small screens */ - .action-buttons-compact { - gap: 0.125rem !important; - } - - /* Slightly smaller touch targets for very small screens */ - .compact-touch-target { - min-width: 40px !important; - min-height: 40px !important; - } - - /* Reduce header height on very small screens */ - .compact-header { - height: 3.5rem !important; /* 56px */ - } +/* Prevent layout shifts during hydration */ +img[priority] { + content-visibility: auto; } -/* Accessibility improvements */ -@media (prefers-reduced-motion: reduce) { - .nav-button-transparent, - .nav-dropdown-item, - .mobile-menu-item { - transition: none !important; - animation: none !important; +/* Loading animation */ +@keyframes loading { + 0% { + width: 0%; + transform: translateX(-100%); } -} - -/* High contrast mode support */ -@media (prefers-contrast: high) { - .nav-button-transparent { - border: 1px solid currentColor !important; + 50% { + width: 100%; + transform: translateX(0%); } - - .nav-dropdown-transparent { - border: 1px solid currentColor !important; - background: var(--background) !important; + 100% { + width: 0%; + transform: translateX(100%); } } -/* Focus visible for keyboard navigation */ -.nav-button-transparent:focus-visible { - outline: 2px solid var(--ring) !important; - outline-offset: 2px !important; -} - -/* Mobile menu icon stroke uniformity */ -.mobile-nav-scroll svg, -[data-radix-dialog-content] svg { - stroke-linecap: round !important; - stroke-linejoin: round !important; -} - -/* Force consistent stroke width for mobile quick action icons */ -@media (max-width: 768px) { - [data-radix-dialog-content] button svg { - stroke-width: inherit !important; - } +/* Ensure loading screen is on top */ +.loading-screen { + z-index: 9999; } diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index ffa571b..60f0601 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -35,6 +35,20 @@ export default function RootLayout({ as="image" type="image/png" /> + <script dangerouslySetInnerHTML={{ + __html: ` + (function() { + function addLoadedClass() { + document.documentElement.classList.add('loaded'); + } + if (document.readyState === 'complete') { + addLoadedClass(); + } else { + window.addEventListener('load', addLoadedClass); + } + })(); + ` + }} /> </head> <body className={`${inter.variable} font-sans antialiased`}> <ThemeProvider @@ -42,8 +56,8 @@ export default function RootLayout({ defaultTheme="system" enableSystem disableTransitionOnChange - > - {children} + > + {children} <Toaster /> </ThemeProvider> </body> diff --git a/frontend/src/app/loading.tsx b/frontend/src/app/loading.tsx new file mode 100644 index 0000000..56455c3 --- /dev/null +++ b/frontend/src/app/loading.tsx @@ -0,0 +1,54 @@ +import Image from "next/image"; + +export default function Loading() { + 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> + ); +}
\ No newline at end of file diff --git a/frontend/src/app/men/page.tsx b/frontend/src/app/men/page.tsx new file mode 100644 index 0000000..8c0dd51 --- /dev/null +++ b/frontend/src/app/men/page.tsx @@ -0,0 +1,178 @@ +import { Header } from "@/components/header"; +import { Footer } from "@/components/footer"; +import { ProductCard } from "@/components/product-card"; + +export default function MenPage() { + // Mock men's products data + const menProducts = [ + { + id: "1", + name: "Oversized Cotton Hoodie", + price: 89, + originalPrice: 129, + image: "/api/placeholder/400/500", + images: ["/api/placeholder/400/500", "/api/placeholder/400/500"], + rating: 4.5, + reviewCount: 234, + isNew: true, + isSale: true, + category: "Hoodies", + colors: ["#000000", "#FFFFFF", "#6B7280"], + sizes: ["S", "M", "L", "XL"], + }, + { + id: "2", + name: "Classic Denim Jacket", + price: 159, + originalPrice: 199, + image: "/api/placeholder/400/500", + rating: 4.7, + reviewCount: 189, + isSale: true, + category: "Jackets", + colors: ["#1E40AF", "#000000"], + sizes: ["M", "L", "XL"], + }, + { + id: "3", + name: "Minimal White T-Shirt", + price: 45, + image: "/api/placeholder/400/500", + rating: 4.3, + reviewCount: 456, + isNew: true, + category: "T-Shirts", + colors: ["#FFFFFF", "#000000", "#6B7280"], + sizes: ["XS", "S", "M", "L", "XL"], + }, + { + id: "4", + name: "Cargo Pants", + price: 119, + originalPrice: 149, + image: "/api/placeholder/400/500", + rating: 4.2, + reviewCount: 98, + isSale: true, + category: "Pants", + colors: ["#000000", "#4B5563", "#059669"], + sizes: ["28", "30", "32", "34", "36"], + }, + { + id: "5", + name: "Oxford Button Shirt", + price: 79, + image: "/api/placeholder/400/500", + rating: 4.6, + reviewCount: 156, + category: "Shirts", + colors: ["#FFFFFF", "#3B82F6", "#6B7280"], + sizes: ["S", "M", "L", "XL"], + }, + { + id: "6", + name: "Leather Sneakers", + price: 189, + originalPrice: 229, + image: "/api/placeholder/400/500", + rating: 4.8, + reviewCount: 267, + isSale: true, + category: "Shoes", + colors: ["#FFFFFF", "#000000", "#8B4513"], + sizes: ["8", "9", "10", "11", "12"], + }, + { + id: "7", + name: "Wool Blazer", + price: 299, + image: "/api/placeholder/400/500", + rating: 4.9, + reviewCount: 78, + isNew: true, + category: "Blazers", + colors: ["#000000", "#374151", "#1E40AF"], + sizes: ["S", "M", "L", "XL"], + }, + { + id: "8", + name: "Casual Chinos", + price: 89, + originalPrice: 109, + image: "/api/placeholder/400/500", + rating: 4.4, + reviewCount: 234, + isSale: true, + category: "Pants", + colors: ["#D2B48C", "#000000", "#1F2937"], + sizes: ["28", "30", "32", "34", "36"], + }, + ]; + + return ( + <> + <Header /> + <main className="min-h-screen bg-background"> + {/* Hero Section */} + <section className="relative h-96 bg-gradient-to-r from-neutral-900 to-neutral-700 flex items-center justify-center"> + <div className="text-center text-white space-y-4"> + <h1 className="text-4xl md:text-6xl font-bold">Men's Collection</h1> + <p className="text-lg md:text-xl max-w-2xl mx-auto px-4"> + Discover our latest men's fashion collection featuring premium quality pieces for the modern man + </p> + </div> + </section> + + {/* Filter Section */} + <section className="border-b dark:border-neutral-800 py-6"> + <div className="container mx-auto px-4"> + <div className="flex flex-wrap items-center justify-between gap-4"> + <div className="flex items-center space-x-4"> + <span className="text-sm text-muted-foreground"> + Showing {menProducts.length} products + </span> + </div> + <div className="flex items-center space-x-4"> + <select className="px-3 py-2 border rounded-lg text-sm bg-background"> + <option>Sort by: Featured</option> + <option>Price: Low to High</option> + <option>Price: High to Low</option> + <option>Newest First</option> + <option>Best Rating</option> + </select> + </div> + </div> + </div> + </section> + + {/* Products Grid */} + <section className="py-12"> + <div className="container mx-auto px-4"> + <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"> + {menProducts.map((product) => ( + <ProductCard key={product.id} {...product} /> + ))} + </div> + </div> + </section> + + {/* Load More Section */} + <section className="py-12 border-t dark:border-neutral-800"> + <div className="container mx-auto px-4 text-center"> + <button className="px-8 py-3 border border-primary text-primary hover:bg-primary hover:text-primary-foreground transition-colors rounded-lg"> + Load More Products + </button> + </div> + </section> + </main> + <Footer /> + </> + ); +} + +export async function generateMetadata() { + return { + title: "Men's Fashion Collection | blcklst", + description: "Shop the latest men's fashion at blcklst. Premium quality clothing, shoes, and accessories for the modern man.", + }; +}
\ No newline at end of file diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 7d6b7a2..dce3b54 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -2,6 +2,7 @@ import { Header } from "@/components/header"; import { HeroSection } from "@/components/hero-section"; import { ProductCard } from "@/components/product-card"; import { Footer } from "@/components/footer"; +import { PageWrapper } from "@/components/loading"; export default function Home() { // Sample product data for demonstration @@ -86,67 +87,69 @@ export default function Home() { ]; return ( - <div className="min-h-screen bg-background"> - <Header /> - - <main> - <HeroSection /> + <PageWrapper> + <div className="min-h-screen bg-background" suppressHydrationWarning> + <Header /> - {/* Featured Products Section */} - <section className="py-16 bg-white dark:bg-neutral-950"> - <div className="container mx-auto px-4"> - <div className="text-center mb-12"> - <h2 className="text-3xl font-bold mb-4 text-foreground">Featured Products</h2> - <p className="text-muted-foreground max-w-2xl mx-auto"> - Discover our handpicked selection of trending items that our customers love most. - </p> + <main> + <HeroSection /> + + {/* Featured Products Section */} + <section className="py-16 bg-white dark:bg-neutral-950"> + <div className="container mx-auto px-4"> + <div className="text-center mb-12"> + <h2 className="text-3xl font-bold mb-4 text-foreground">Featured Products</h2> + <p className="text-muted-foreground max-w-2xl mx-auto"> + Discover our handpicked selection of trending items that our customers love most. + </p> + </div> + + <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"> + {featuredProducts.map((product) => ( + <ProductCard key={product.id} {...product} /> + ))} + </div> + + <div className="text-center mt-12"> + <button className="bg-primary text-primary-foreground px-8 py-3 rounded-lg font-medium hover:bg-primary/90 transition-colors"> + View All Products + </button> + </div> </div> - - <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"> - {featuredProducts.map((product) => ( - <ProductCard key={product.id} {...product} /> - ))} - </div> - - <div className="text-center mt-12"> - <button className="bg-primary text-primary-foreground px-8 py-3 rounded-lg font-medium hover:bg-primary/90 transition-colors"> - View All Products - </button> - </div> - </div> - </section> + </section> - {/* Categories Section */} - <section className="py-16 bg-neutral-50 dark:bg-neutral-900"> - <div className="container mx-auto px-4"> - <div className="text-center mb-12"> - <h2 className="text-3xl font-bold mb-4">Shop by Category</h2> - <p className="text-muted-foreground"> - Explore our diverse range of fashion categories - </p> - </div> - - <div className="grid grid-cols-1 md:grid-cols-3 gap-6"> - {[ - { name: "Women", image: "/api/placeholder/600/400", count: "2,345+ items" }, - { name: "Men", image: "/api/placeholder/600/400", count: "1,892+ items" }, - { name: "Kids", image: "/api/placeholder/600/400", count: "956+ items" }, - ].map((category) => ( - <div key={category.name} className="group relative overflow-hidden rounded-2xl bg-neutral-100 dark:bg-neutral-800 aspect-[4/3]"> - <div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent" /> - <div className="absolute bottom-6 left-6 text-white"> - <h3 className="text-2xl font-bold mb-1">{category.name}</h3> - <p className="text-neutral-200">{category.count}</p> + {/* Categories Section */} + <section className="py-16 bg-neutral-50 dark:bg-neutral-900"> + <div className="container mx-auto px-4"> + <div className="text-center mb-12"> + <h2 className="text-3xl font-bold mb-4">Shop by Category</h2> + <p className="text-muted-foreground"> + Explore our diverse range of fashion categories + </p> + </div> + + <div className="grid grid-cols-1 md:grid-cols-3 gap-6"> + {[ + { name: "Women", image: "/api/placeholder/600/400", count: "2,345+ items" }, + { name: "Men", image: "/api/placeholder/600/400", count: "1,892+ items" }, + { name: "Kids", image: "/api/placeholder/600/400", count: "956+ items" }, + ].map((category) => ( + <div key={category.name} className="group relative overflow-hidden rounded-2xl bg-neutral-100 dark:bg-neutral-800 aspect-[4/3]"> + <div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent" /> + <div className="absolute bottom-6 left-6 text-white"> + <h3 className="text-2xl font-bold mb-1">{category.name}</h3> + <p className="text-neutral-200">{category.count}</p> + </div> + <div className="absolute inset-0 bg-black/20 opacity-0 group-hover:opacity-100 transition-opacity duration-300" /> </div> - <div className="absolute inset-0 bg-black/20 opacity-0 group-hover:opacity-100 transition-opacity duration-300" /> - </div> - ))} + ))} + </div> </div> - </div> - </section> - </main> - - <Footer /> - </div> + </section> + </main> + + <Footer /> + </div> + </PageWrapper> ); } diff --git a/frontend/src/app/products/[id]/page.tsx b/frontend/src/app/products/[id]/page.tsx new file mode 100644 index 0000000..31ceded --- /dev/null +++ b/frontend/src/app/products/[id]/page.tsx @@ -0,0 +1,32 @@ +import { Header } from "@/components/header"; +import { Footer } from "@/components/footer"; +import { ProductPage } from "@/components/product-page"; + +interface ProductPageProps { + params: Promise<{ + id: string; + }>; +} + +export default async function Product({ params }: ProductPageProps) { + const { id } = await params; + + return ( + <> + <Header /> + <ProductPage productId={id} /> + <Footer /> + </> + ); +} + +export async function generateMetadata({ params }: ProductPageProps) { + // In a real app, you'd fetch the product data here based on params.id + const { id } = await params; + const productName = id === "1" ? "Oversized Cotton Hoodie" : "Product"; + + return { + title: `${productName} | blcklst`, + description: `Shop the ${productName} at blcklst. Premium quality fashion pieces that define modern elegance.`, + }; +}
\ No newline at end of file diff --git a/frontend/src/app/unisex/page.tsx b/frontend/src/app/unisex/page.tsx new file mode 100644 index 0000000..3c7c0d1 --- /dev/null +++ b/frontend/src/app/unisex/page.tsx @@ -0,0 +1,178 @@ +import { Header } from "@/components/header"; +import { Footer } from "@/components/footer"; +import { ProductCard } from "@/components/product-card"; + +export default function UnisexPage() { + // Mock unisex products data + const unisexProducts = [ + { + id: "17", + name: "Organic Cotton Hoodie", + price: 89, + originalPrice: 119, + image: "/api/placeholder/400/500", + images: ["/api/placeholder/400/500", "/api/placeholder/400/500"], + rating: 4.7, + reviewCount: 342, + isNew: true, + isSale: true, + category: "Hoodies", + colors: ["#000000", "#FFFFFF", "#6B7280", "#059669"], + sizes: ["XS", "S", "M", "L", "XL", "XXL"], + }, + { + id: "18", + name: "Minimalist T-Shirt", + price: 39, + originalPrice: 49, + image: "/api/placeholder/400/500", + rating: 4.5, + reviewCount: 567, + isSale: true, + category: "T-Shirts", + colors: ["#FFFFFF", "#000000", "#6B7280", "#1E40AF"], + sizes: ["XS", "S", "M", "L", "XL", "XXL"], + }, + { + id: "19", + name: "Relaxed Fit Jeans", + price: 119, + image: "/api/placeholder/400/500", + rating: 4.6, + reviewCount: 234, + isNew: true, + category: "Jeans", + colors: ["#4169E1", "#000000", "#6B7280"], + sizes: ["26", "28", "30", "32", "34", "36"], + }, + { + id: "20", + name: "Oversized Sweatshirt", + price: 79, + originalPrice: 99, + image: "/api/placeholder/400/500", + rating: 4.8, + reviewCount: 189, + isSale: true, + category: "Sweatshirts", + colors: ["#F5F5DC", "#000000", "#6B7280", "#059669"], + sizes: ["XS", "S", "M", "L", "XL", "XXL"], + }, + { + id: "21", + name: "Canvas Backpack", + price: 89, + image: "/api/placeholder/400/500", + rating: 4.4, + reviewCount: 156, + category: "Accessories", + colors: ["#000000", "#8B4513", "#6B7280", "#FFFFFF"], + sizes: ["One Size"], + }, + { + id: "22", + name: "Cargo Joggers", + price: 99, + originalPrice: 129, + image: "/api/placeholder/400/500", + rating: 4.3, + reviewCount: 278, + isSale: true, + category: "Pants", + colors: ["#000000", "#6B7280", "#059669", "#8B4513"], + sizes: ["XS", "S", "M", "L", "XL", "XXL"], + }, + { + id: "23", + name: "Bomber Jacket", + price: 159, + image: "/api/placeholder/400/500", + rating: 4.9, + reviewCount: 123, + isNew: true, + category: "Outerwear", + colors: ["#000000", "#1E40AF", "#6B7280", "#8B4513"], + sizes: ["XS", "S", "M", "L", "XL", "XXL"], + }, + { + id: "24", + name: "Classic Sneakers", + price: 129, + originalPrice: 159, + image: "/api/placeholder/400/500", + rating: 4.6, + reviewCount: 445, + isSale: true, + category: "Shoes", + colors: ["#FFFFFF", "#000000", "#6B7280"], + sizes: ["36", "37", "38", "39", "40", "41", "42", "43", "44"], + }, + ]; + + return ( + <> + <Header /> + <main className="min-h-screen bg-background"> + {/* Hero Section */} + <section className="relative h-96 bg-gradient-to-r from-neutral-100 to-neutral-200 dark:from-neutral-800 dark:to-neutral-900 flex items-center justify-center"> + <div className="text-center space-y-4"> + <h1 className="text-4xl md:text-6xl font-bold text-foreground">Unisex Collection</h1> + <p className="text-lg md:text-xl max-w-2xl mx-auto px-4 text-muted-foreground"> + Gender-neutral fashion for everyone. Discover versatile pieces designed for all body types and styles + </p> + </div> + </section> + + {/* Filter Section */} + <section className="border-b dark:border-neutral-800 py-6"> + <div className="container mx-auto px-4"> + <div className="flex flex-wrap items-center justify-between gap-4"> + <div className="flex items-center space-x-4"> + <span className="text-sm text-muted-foreground"> + Showing {unisexProducts.length} products + </span> + </div> + <div className="flex items-center space-x-4"> + <select className="px-3 py-2 border rounded-lg text-sm bg-background"> + <option>Sort by: Featured</option> + <option>Price: Low to High</option> + <option>Price: High to Low</option> + <option>Newest First</option> + <option>Best Rating</option> + </select> + </div> + </div> + </div> + </section> + + {/* Products Grid */} + <section className="py-12"> + <div className="container mx-auto px-4"> + <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"> + {unisexProducts.map((product) => ( + <ProductCard key={product.id} {...product} /> + ))} + </div> + </div> + </section> + + {/* Load More Section */} + <section className="py-12 border-t dark:border-neutral-800"> + <div className="container mx-auto px-4 text-center"> + <button className="px-8 py-3 border border-primary text-primary hover:bg-primary hover:text-primary-foreground transition-colors rounded-lg"> + Load More Products + </button> + </div> + </section> + </main> + <Footer /> + </> + ); +} + +export async function generateMetadata() { + return { + title: "Unisex Fashion Collection | blcklst", + description: "Shop gender-neutral fashion at blcklst. Versatile, inclusive clothing designed for all body types and personal styles.", + }; +}
\ No newline at end of file diff --git a/frontend/src/app/women/page.tsx b/frontend/src/app/women/page.tsx new file mode 100644 index 0000000..6814760 --- /dev/null +++ b/frontend/src/app/women/page.tsx @@ -0,0 +1,178 @@ +import { Header } from "@/components/header"; +import { Footer } from "@/components/footer"; +import { ProductCard } from "@/components/product-card"; + +export default function WomenPage() { + // Mock women's products data + const womenProducts = [ + { + id: "9", + name: "Floral Summer Dress", + price: 129, + originalPrice: 169, + image: "/api/placeholder/400/500", + images: ["/api/placeholder/400/500", "/api/placeholder/400/500"], + rating: 4.8, + reviewCount: 324, + isNew: true, + isSale: true, + category: "Dresses", + colors: ["#FFB6C1", "#FFFFFF", "#98FB98"], + sizes: ["XS", "S", "M", "L", "XL"], + }, + { + id: "10", + name: "Silk Blouse", + price: 89, + originalPrice: 119, + image: "/api/placeholder/400/500", + rating: 4.6, + reviewCount: 198, + isSale: true, + category: "Tops", + colors: ["#FFFFFF", "#000000", "#FFB6C1"], + sizes: ["XS", "S", "M", "L"], + }, + { + id: "11", + name: "High-Waist Jeans", + price: 109, + image: "/api/placeholder/400/500", + rating: 4.7, + reviewCount: 456, + isNew: true, + category: "Jeans", + colors: ["#4169E1", "#000000", "#FFFFFF"], + sizes: ["24", "26", "28", "30", "32"], + }, + { + id: "12", + name: "Cashmere Sweater", + price: 199, + originalPrice: 249, + image: "/api/placeholder/400/500", + rating: 4.9, + reviewCount: 167, + isSale: true, + category: "Sweaters", + colors: ["#F5F5DC", "#FFB6C1", "#6B7280"], + sizes: ["XS", "S", "M", "L", "XL"], + }, + { + id: "13", + name: "Leather Handbag", + price: 249, + image: "/api/placeholder/400/500", + rating: 4.5, + reviewCount: 89, + category: "Accessories", + colors: ["#000000", "#8B4513", "#FFFFFF"], + sizes: ["One Size"], + }, + { + id: "14", + name: "Midi Skirt", + price: 79, + originalPrice: 99, + image: "/api/placeholder/400/500", + rating: 4.4, + reviewCount: 234, + isSale: true, + category: "Skirts", + colors: ["#000000", "#FFB6C1", "#6B7280"], + sizes: ["XS", "S", "M", "L", "XL"], + }, + { + id: "15", + name: "Trench Coat", + price: 299, + image: "/api/placeholder/400/500", + rating: 4.8, + reviewCount: 156, + isNew: true, + category: "Outerwear", + colors: ["#F5F5DC", "#000000", "#8B4513"], + sizes: ["XS", "S", "M", "L", "XL"], + }, + { + id: "16", + name: "Ankle Boots", + price: 179, + originalPrice: 219, + image: "/api/placeholder/400/500", + rating: 4.6, + reviewCount: 278, + isSale: true, + category: "Shoes", + colors: ["#000000", "#8B4513", "#6B7280"], + sizes: ["5", "6", "7", "8", "9", "10"], + }, + ]; + + return ( + <> + <Header /> + <main className="min-h-screen bg-background"> + {/* Hero Section */} + <section className="relative h-96 bg-gradient-to-r from-pink-100 to-purple-100 dark:from-pink-900/20 dark:to-purple-900/20 flex items-center justify-center"> + <div className="text-center space-y-4"> + <h1 className="text-4xl md:text-6xl font-bold text-foreground">Women's Collection</h1> + <p className="text-lg md:text-xl max-w-2xl mx-auto px-4 text-muted-foreground"> + Embrace your style with our curated women's fashion collection featuring elegant and contemporary pieces + </p> + </div> + </section> + + {/* Filter Section */} + <section className="border-b dark:border-neutral-800 py-6"> + <div className="container mx-auto px-4"> + <div className="flex flex-wrap items-center justify-between gap-4"> + <div className="flex items-center space-x-4"> + <span className="text-sm text-muted-foreground"> + Showing {womenProducts.length} products + </span> + </div> + <div className="flex items-center space-x-4"> + <select className="px-3 py-2 border rounded-lg text-sm bg-background"> + <option>Sort by: Featured</option> + <option>Price: Low to High</option> + <option>Price: High to Low</option> + <option>Newest First</option> + <option>Best Rating</option> + </select> + </div> + </div> + </div> + </section> + + {/* Products Grid */} + <section className="py-12"> + <div className="container mx-auto px-4"> + <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"> + {womenProducts.map((product) => ( + <ProductCard key={product.id} {...product} /> + ))} + </div> + </div> + </section> + + {/* Load More Section */} + <section className="py-12 border-t dark:border-neutral-800"> + <div className="container mx-auto px-4 text-center"> + <button className="px-8 py-3 border border-primary text-primary hover:bg-primary hover:text-primary-foreground transition-colors rounded-lg"> + Load More Products + </button> + </div> + </section> + </main> + <Footer /> + </> + ); +} + +export async function generateMetadata() { + return { + title: "Women's Fashion Collection | blcklst", + description: "Shop the latest women's fashion at blcklst. Elegant dresses, tops, accessories, and more for the modern woman.", + }; +}
\ No newline at end of file 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 |