@pradip1995/theme-sahsha 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +29 -0
  2. package/assets/hero-desktop.svg +10 -0
  3. package/assets/hero-mobile.svg +9 -0
  4. package/assets/logo.svg +3 -0
  5. package/package.json +60 -0
  6. package/src/blocks/home/Features/index.tsx +87 -0
  7. package/src/blocks/home/Hero/index.tsx +98 -0
  8. package/src/blocks/home/LovedByMoms/bestsellers-carousel.tsx +1 -0
  9. package/src/blocks/home/LovedByMoms/index.tsx +43 -0
  10. package/src/blocks/home/LovedByMoms/loved-by-moms-section.tsx +46 -0
  11. package/src/blocks/home/NewArrivals/index.tsx +91 -0
  12. package/src/blocks/home/PromotionalBanners/index.tsx +81 -0
  13. package/src/blocks/home/ShopByAge/collections-showcase-client.tsx +131 -0
  14. package/src/blocks/home/ShopByAge/collections-showcase-types.ts +4 -0
  15. package/src/blocks/home/ShopByAge/index.tsx +168 -0
  16. package/src/blocks/home/ShopByCategory/index.tsx +111 -0
  17. package/src/blocks/home/Testimonials/index.tsx +25 -0
  18. package/src/blocks/home/Testimonials/reviews-scroll.tsx +122 -0
  19. package/src/blocks/home/Testimonials/testimonials-client.tsx +127 -0
  20. package/src/blocks/home/WhyChooseUs/index.tsx +39 -0
  21. package/src/components/product-carousel.tsx +79 -0
  22. package/src/layouts/MainLayoutShell.tsx +14 -0
  23. package/src/primitives/Button.tsx +31 -0
  24. package/src/primitives/Card.tsx +32 -0
  25. package/src/primitives/index.ts +2 -0
  26. package/src/slots/account/ForgotPassword/index.tsx +1 -0
  27. package/src/slots/account/GoogleLogin/index.tsx +28 -0
  28. package/src/slots/account/Login/index.tsx +1 -0
  29. package/src/slots/account/LoginTemplate/index.tsx +12 -0
  30. package/src/slots/account/LoginTemplate/login-template-client.tsx +83 -0
  31. package/src/slots/account/Register/index.tsx +1 -0
  32. package/src/slots/cart/CartItem/index.tsx +11 -0
  33. package/src/slots/cart/CartSummary/index.tsx +8 -0
  34. package/src/slots/checkout/CheckoutForm/index.tsx +1 -0
  35. package/src/slots/checkout/CheckoutSummary/index.tsx +1 -0
  36. package/src/slots/layout/Footer/index.tsx +95 -0
  37. package/src/slots/layout/Nav/index.tsx +50 -0
  38. package/src/slots/layout/Nav/nav-categories-dropdown.tsx +74 -0
  39. package/src/slots/layout/Nav/nav-collections-dropdown.tsx +106 -0
  40. package/src/slots/layout/Nav/nav-header-content.tsx +165 -0
  41. package/src/slots/layout/Nav/nav-header-shell.tsx +47 -0
  42. package/src/slots/layout/Nav/nav-link-luxury.tsx +15 -0
  43. package/src/slots/layout/PromoBar/index.tsx +9 -0
  44. package/src/slots/layout/PromoBar/promo-bar-content.tsx +118 -0
  45. package/src/slots/order/OrderDetails/index.tsx +12 -0
  46. package/src/slots/product/ProductActions/ProductCTASection.tsx +232 -0
  47. package/src/slots/product/ProductActions/ProductDetailsSection.tsx +200 -0
  48. package/src/slots/product/ProductActions/ProductFeaturePanel.tsx +150 -0
  49. package/src/slots/product/ProductActions/ProductHighlightsSection.tsx +112 -0
  50. package/src/slots/product/ProductActions/ProductOptionsSection.tsx +215 -0
  51. package/src/slots/product/ProductActions/ProductPriceSection.tsx +53 -0
  52. package/src/slots/product/ProductActions/ProductTrustSection.tsx +84 -0
  53. package/src/slots/product/ProductActions/SizeChartPanel.tsx +93 -0
  54. package/src/slots/product/ProductActions/index.tsx +156 -0
  55. package/src/slots/product/ProductActions/product-metadata-fields.ts +503 -0
  56. package/src/slots/product/ProductActions/size-chart-data.ts +108 -0
  57. package/src/slots/product/ProductCard/index.tsx +258 -0
  58. package/src/slots/product/ProductInfo/index.tsx +35 -0
  59. package/src/templates/CollectionsPage/index.tsx +72 -0
  60. package/src/templates/StorePage/index.tsx +134 -0
  61. package/src/tokens/colors.ts +21 -0
  62. package/src/tokens/fonts.ts +16 -0
  63. package/src/tokens/index.ts +3 -0
  64. package/src/tokens/spacing.ts +9 -0
  65. package/src/tokens/theme.css +12754 -0
@@ -0,0 +1,39 @@
1
+ import Image from "next/image"
2
+ import type { WhyChooseUsBlockData } from "@core/types/home"
3
+
4
+ const WhyChooseUs = ({ title, features }: WhyChooseUsBlockData) => {
5
+ if (features.length === 0) {
6
+ return null
7
+ }
8
+
9
+ return (
10
+ <section className="why-choose w-full bg-white py-8 sm:py-14 md:py-16">
11
+ <div className="why-choose__inner">
12
+ <h2 className="why-choose__title">{title}</h2>
13
+
14
+ <div className="why-choose__grid">
15
+ {features.map((feature, index) => (
16
+ <div
17
+ key={`${feature.name}-${index}`}
18
+ className="why-choose__item"
19
+ >
20
+ <div className="why-choose__icon-wrap">
21
+ <Image
22
+ src={feature.icon}
23
+ alt={feature.name}
24
+ width={120}
25
+ height={120}
26
+ className="why-choose__icon"
27
+ unoptimized
28
+ />
29
+ </div>
30
+ <h3 className="why-choose__label">{feature.name}</h3>
31
+ </div>
32
+ ))}
33
+ </div>
34
+ </div>
35
+ </section>
36
+ )
37
+ }
38
+
39
+ export default WhyChooseUs
@@ -0,0 +1,79 @@
1
+ "use client"
2
+
3
+ import { useRef } from "react"
4
+ import { ChevronLeft, ChevronRight } from "lucide-react"
5
+ import { HttpTypes } from "@medusajs/types"
6
+ import type { ProductCardRating } from "@core/types/product-card"
7
+ import ProductCard from "@theme/slots/product/ProductCard"
8
+
9
+ type ProductCarouselProps = {
10
+ products: HttpTypes.StoreProduct[]
11
+ region: HttpTypes.StoreRegion
12
+ ratings?: ProductCardRating[]
13
+ }
14
+
15
+ const ProductCarousel = ({ products, region, ratings }: ProductCarouselProps) => {
16
+ const viewportRef = useRef<HTMLDivElement>(null)
17
+ const trackRef = useRef<HTMLDivElement>(null)
18
+
19
+ const scrollBySlide = (direction: "prev" | "next") => {
20
+ const viewport = viewportRef.current
21
+ const track = trackRef.current
22
+ if (!viewport || !track) return
23
+
24
+ const firstSlide = track.querySelector<HTMLElement>(".bestsellers-carousel__slide")
25
+ if (!firstSlide) return
26
+
27
+ const trackStyles = getComputedStyle(track)
28
+ const gap =
29
+ Number.parseFloat(trackStyles.columnGap || trackStyles.gap || "0") || 0
30
+ const slideStep = firstSlide.offsetWidth + gap
31
+
32
+ viewport.scrollBy({
33
+ left: direction === "next" ? slideStep : -slideStep,
34
+ behavior: "smooth",
35
+ })
36
+ }
37
+
38
+ return (
39
+ <div className="bestsellers-carousel">
40
+ <button
41
+ type="button"
42
+ className="bestsellers-carousel__nav bestsellers-carousel__nav--prev"
43
+ onClick={() => scrollBySlide("prev")}
44
+ aria-label="Previous products"
45
+ >
46
+ <ChevronLeft size={20} strokeWidth={1.75} />
47
+ </button>
48
+
49
+ <div ref={viewportRef} className="bestsellers-carousel__viewport">
50
+ <div ref={trackRef} className="bestsellers-carousel__track">
51
+ {products.map((product) => (
52
+ <div key={product.id} className="bestsellers-carousel__slide">
53
+ <ProductCard
54
+ product={product}
55
+ region={region}
56
+ rating={ratings?.find((r) => r.product_id === product.id)}
57
+ className="h-full"
58
+ imageClassName="shop-grid__card-image"
59
+ quickAddClassName="product-card__quick-add--hero"
60
+ showWishlistIcon
61
+ />
62
+ </div>
63
+ ))}
64
+ </div>
65
+ </div>
66
+
67
+ <button
68
+ type="button"
69
+ className="bestsellers-carousel__nav bestsellers-carousel__nav--next"
70
+ onClick={() => scrollBySlide("next")}
71
+ aria-label="Next products"
72
+ >
73
+ <ChevronRight size={20} strokeWidth={1.75} />
74
+ </button>
75
+ </div>
76
+ )
77
+ }
78
+
79
+ export default ProductCarousel
@@ -0,0 +1,14 @@
1
+ import type { ReactNode } from "react"
2
+ import { colorClasses } from "@theme/tokens"
3
+
4
+ type MainLayoutShellProps = {
5
+ children: ReactNode
6
+ }
7
+
8
+ export function MainLayoutShell({ children }: MainLayoutShellProps) {
9
+ return (
10
+ <main className={`${colorClasses.pageBg} min-h-screen w-full mobile-layout-shell`}>
11
+ {children}
12
+ </main>
13
+ )
14
+ }
@@ -0,0 +1,31 @@
1
+ import { Button as MedusaButton } from "@medusajs/ui"
2
+ import { clx } from "@medusajs/ui"
3
+ import type { ComponentProps } from "react"
4
+
5
+ type ButtonVariant = "primary" | "secondary" | "ghost"
6
+
7
+ type ButtonProps = Omit<ComponentProps<typeof MedusaButton>, "variant"> & {
8
+ variant?: ButtonVariant
9
+ }
10
+
11
+ const variantClasses: Record<ButtonVariant, string> = {
12
+ primary:
13
+ "bg-brand-accent hover:bg-brand-accent-hover text-white rounded-none font-semibold uppercase tracking-[var(--letter-spacing-nav)] text-xs",
14
+ secondary:
15
+ "bg-transparent border border-brand-accent text-brand-accent rounded-none font-semibold uppercase tracking-[var(--letter-spacing-nav)] text-xs hover:bg-brand-accent hover:text-white",
16
+ ghost:
17
+ "bg-transparent text-heading hover:opacity-70 rounded-none font-semibold uppercase tracking-[var(--letter-spacing-nav)] text-xs",
18
+ }
19
+
20
+ export function Button({
21
+ variant = "primary",
22
+ className,
23
+ children,
24
+ ...props
25
+ }: ButtonProps) {
26
+ return (
27
+ <MedusaButton className={clx(variantClasses[variant], className)} {...props}>
28
+ {children}
29
+ </MedusaButton>
30
+ )
31
+ }
@@ -0,0 +1,32 @@
1
+ import { clx } from "@medusajs/ui"
2
+ import type { HTMLAttributes } from "react"
3
+
4
+ type CardProps = HTMLAttributes<HTMLDivElement> & {
5
+ padding?: "sm" | "md" | "lg"
6
+ }
7
+
8
+ const paddingClasses = {
9
+ sm: "p-4",
10
+ md: "p-6",
11
+ lg: "p-8",
12
+ }
13
+
14
+ export function Card({
15
+ padding = "md",
16
+ className,
17
+ children,
18
+ ...props
19
+ }: CardProps) {
20
+ return (
21
+ <div
22
+ className={clx(
23
+ "rounded-xl bg-white border border-gray-100 shadow-sm",
24
+ paddingClasses[padding],
25
+ className
26
+ )}
27
+ {...props}
28
+ >
29
+ {children}
30
+ </div>
31
+ )
32
+ }
@@ -0,0 +1,2 @@
1
+ export { Button } from "./Button"
2
+ export { Card } from "./Card"
@@ -0,0 +1 @@
1
+ export { default } from "@pradip1995/commerce-auth/components/forgot-password"
@@ -0,0 +1,28 @@
1
+ "use client"
2
+
3
+ import { initiateGoogleAuth } from "@core/data/customer"
4
+ import { GoogleLoginButton } from "@pradip1995/commerce-auth/components/google-login"
5
+ import { useParams } from "next/navigation"
6
+
7
+ const BUTTON_CLASS =
8
+ "w-full flex items-center justify-center gap-2.5 py-3 px-4 border border-gray-200 bg-surface hover:bg-gray-50 transition-all shadow-sm hover:shadow-md active:scale-[0.98]"
9
+
10
+ type GoogleLoginProps = {
11
+ countryCode?: string
12
+ }
13
+
14
+ export default function GoogleLogin({
15
+ countryCode: countryCodeProp,
16
+ }: GoogleLoginProps = {}) {
17
+ const params = useParams()
18
+
19
+ return (
20
+ <GoogleLoginButton
21
+ countryCode={
22
+ countryCodeProp ?? (params?.countryCode as string | undefined)
23
+ }
24
+ initiateAuth={initiateGoogleAuth}
25
+ className={BUTTON_CLASS}
26
+ />
27
+ )
28
+ }
@@ -0,0 +1 @@
1
+ export { default } from "@modules/account/components/login"
@@ -0,0 +1,12 @@
1
+ import type { AccountPageData } from "@core/types/account"
2
+ import { getLoginPagePanelFromConfig } from "@lib/data/login-panel"
3
+
4
+ import LoginTemplateClient from "./login-template-client"
5
+
6
+ type LoginTemplateProps = Pick<AccountPageData, "countryCode">
7
+
8
+ export default async function LoginTemplate(_props: LoginTemplateProps) {
9
+ const panelImage = await getLoginPagePanelFromConfig()
10
+
11
+ return <LoginTemplateClient panelImage={panelImage} />
12
+ }
@@ -0,0 +1,83 @@
1
+ "use client"
2
+
3
+ import { Suspense, useState } from "react"
4
+
5
+ import { LOGIN_VIEW } from "@core/types/account"
6
+ import Login from "@theme/slots/account/Login"
7
+ import Register from "@theme/slots/account/Register"
8
+ import ForgotPassword from "@theme/slots/account/ForgotPassword"
9
+ import Spinner from "@modules/common/icons/spinner"
10
+
11
+ type LoginTemplateClientProps = {
12
+ panelImage: string
13
+ }
14
+
15
+ const VIEW_COPY = {
16
+ [LOGIN_VIEW.SIGN_IN]: {
17
+ title: "Sign in",
18
+ subtitle: "Access your exclusive collection",
19
+ },
20
+ [LOGIN_VIEW.REGISTER]: {
21
+ title: "Create account",
22
+ subtitle: "Join Sahsha for a curated experience",
23
+ },
24
+ [LOGIN_VIEW.FORGOT_PASSWORD]: {
25
+ title: "Forgot password",
26
+ subtitle: "We'll help you get back in",
27
+ },
28
+ } as const
29
+
30
+ export default function LoginTemplateClient({
31
+ panelImage,
32
+ }: LoginTemplateClientProps) {
33
+ const [currentView, setCurrentView] = useState(LOGIN_VIEW.SIGN_IN)
34
+ const copy = VIEW_COPY[currentView]
35
+
36
+ const renderView = () => {
37
+ switch (currentView) {
38
+ case LOGIN_VIEW.REGISTER:
39
+ return <Register setCurrentView={setCurrentView} />
40
+ case LOGIN_VIEW.FORGOT_PASSWORD:
41
+ return <ForgotPassword setCurrentView={setCurrentView} />
42
+ default:
43
+ return <Login setCurrentView={setCurrentView} />
44
+ }
45
+ }
46
+
47
+ return (
48
+ <div className="account-login">
49
+ <div className="account-login__hero" aria-hidden>
50
+ <img
51
+ src={panelImage}
52
+ alt=""
53
+ className="account-login__hero-image"
54
+ loading="eager"
55
+ decoding="async"
56
+ />
57
+ </div>
58
+
59
+ <div className="account-login__panel">
60
+ <div className="account-login__panel-inner">
61
+ <header className="account-login__header">
62
+ <h1 className="account-login__title">{copy.title}</h1>
63
+ <p className="account-login__subtitle">{copy.subtitle}</p>
64
+ </header>
65
+
66
+ <div
67
+ className={`account-login__card account-auth account-auth--${currentView}`}
68
+ >
69
+ <Suspense
70
+ fallback={
71
+ <div className="flex justify-center py-12">
72
+ <Spinner />
73
+ </div>
74
+ }
75
+ >
76
+ {renderView()}
77
+ </Suspense>
78
+ </div>
79
+ </div>
80
+ </div>
81
+ </div>
82
+ )
83
+ }
@@ -0,0 +1 @@
1
+ export { default } from "@modules/account/components/register"
@@ -0,0 +1,11 @@
1
+ import { HttpTypes } from "@medusajs/types"
2
+ import CartItemCard from "@modules/cart/components/cart-item-card"
3
+
4
+ export type CartItemSlotProps = {
5
+ item: HttpTypes.StoreCartLineItem
6
+ currencyCode: string
7
+ }
8
+
9
+ export default function CartItem({ item, currencyCode }: CartItemSlotProps) {
10
+ return <CartItemCard item={item} currencyCode={currencyCode} />
11
+ }
@@ -0,0 +1,8 @@
1
+ import type { CartSummaryProps } from "@core/types/cart"
2
+ import Summary from "@modules/cart/templates/summary"
3
+
4
+ export type CartSummarySlotProps = CartSummaryProps
5
+
6
+ export default function CartSummary({ cart, customer }: CartSummarySlotProps) {
7
+ return <Summary cart={cart} customer={customer} />
8
+ }
@@ -0,0 +1 @@
1
+ export { default } from "@modules/checkout/templates/checkout-form"
@@ -0,0 +1 @@
1
+ export { default } from "@modules/checkout/templates/checkout-summary"
@@ -0,0 +1,95 @@
1
+ import type { FooterSlotData } from "@core/types/layout"
2
+ import LocalizedClientLink from "@modules/common/components/localized-client-link"
3
+ import FooterLogo from "@modules/layout/components/footer-logo"
4
+ import FooterNewsletter from "@modules/layout/components/footer-newsletter"
5
+ import FooterSocial from "@modules/layout/components/footer-social"
6
+
7
+ const COMPANY_LINKS = [
8
+ { href: "/", label: "Home" },
9
+ { href: "/store", label: "Shop" },
10
+ { href: "/about", label: "About" },
11
+ { href: "/contact", label: "Contact" },
12
+ { href: "/help", label: "Help" },
13
+ { href: "/privacy-policy", label: "Privacy Policy" },
14
+ { href: "/terms-of-use", label: "Terms of Use" },
15
+ ]
16
+
17
+ export default function Footer({ categories, socialLinks }: FooterSlotData) {
18
+ return (
19
+ <footer className="site-footer">
20
+ <div
21
+ className="site-footer__inner mx-auto px-4 sm:px-6 lg:px-8 py-12 lg:py-16"
22
+ style={{ maxWidth: "var(--container-max)" }}
23
+ >
24
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-10 lg:gap-12">
25
+ <div className="sm:col-span-2 lg:col-span-1">
26
+ <FooterLogo />
27
+ <p className="site-footer__text max-w-xs mt-4">
28
+ Kurtis and ethnic wear curated for everyday elegance. Handpicked
29
+ styles, crafted with comfort and grace.
30
+ </p>
31
+ </div>
32
+
33
+ <div>
34
+ <h3 className="site-footer__label">Shop</h3>
35
+ <ul className="space-y-2.5">
36
+ {(categories?.slice(0, 6) ?? []).map((cat) => (
37
+ <li key={cat.id}>
38
+ <LocalizedClientLink
39
+ href={`/store?category=${cat.id}`}
40
+ className="site-footer__link"
41
+ >
42
+ {cat.name}
43
+ </LocalizedClientLink>
44
+ </li>
45
+ ))}
46
+ {!categories?.length && (
47
+ <li>
48
+ <LocalizedClientLink href="/store" className="site-footer__link">
49
+ All products
50
+ </LocalizedClientLink>
51
+ </li>
52
+ )}
53
+ </ul>
54
+ </div>
55
+
56
+ <div>
57
+ <h3 className="site-footer__label">Company</h3>
58
+ <ul className="space-y-2.5">
59
+ {COMPANY_LINKS.map((link) => (
60
+ <li key={link.href}>
61
+ <LocalizedClientLink href={link.href} className="site-footer__link">
62
+ {link.label}
63
+ </LocalizedClientLink>
64
+ </li>
65
+ ))}
66
+ </ul>
67
+ </div>
68
+
69
+ <div>
70
+ <h3 className="site-footer__label">Newsletter</h3>
71
+ <p className="site-footer__text mb-4">
72
+ Subscribe for exclusive offers and new arrivals.
73
+ </p>
74
+ <FooterNewsletter />
75
+ <FooterSocial links={socialLinks ?? []} />
76
+ </div>
77
+ </div>
78
+
79
+ <div className="site-footer__divider mt-12 pt-8 flex flex-col sm:flex-row items-center justify-between gap-4">
80
+ <p className="site-footer__copyright">
81
+ © {new Date().getFullYear()} Sahsha. All rights reserved.
82
+ </p>
83
+ <div className="flex gap-6">
84
+ <LocalizedClientLink href="/privacy-policy" className="site-footer__link text-xs">
85
+ Privacy
86
+ </LocalizedClientLink>
87
+ <LocalizedClientLink href="/terms-of-use" className="site-footer__link text-xs">
88
+ Terms
89
+ </LocalizedClientLink>
90
+ </div>
91
+ </div>
92
+ </div>
93
+ </footer>
94
+ )
95
+ }
@@ -0,0 +1,50 @@
1
+ import { Suspense } from "react"
2
+ import type { NavLayoutData } from "@controllers/layout/load-layout-data"
3
+ import DesktopSearch from "@modules/layout/components/desktop-search"
4
+ import DynamicLogo from "@modules/layout/components/dynamic-logo"
5
+ import CartButton from "@modules/layout/components/cart-button"
6
+ import LocalizedClientLink from "@modules/common/components/localized-client-link"
7
+ import NavHeaderShell from "./nav-header-shell"
8
+ import NavHeaderContent from "./nav-header-content"
9
+
10
+ export default function Nav({ currentLocale, customer, categories, collections }: NavLayoutData) {
11
+ const cartButton = (
12
+ <Suspense
13
+ fallback={
14
+ <LocalizedClientLink
15
+ href="/cart"
16
+ className="nav-header-icon-link flex items-center"
17
+ data-testid="nav-cart-link"
18
+ >
19
+ {/* eslint-disable-next-line @next/next/no-img-element */}
20
+ <img
21
+ src="/shopping-bag (3).svg"
22
+ alt="Cart"
23
+ width={22}
24
+ height={22}
25
+ className="nav-header-icon w-[22px] h-[22px]"
26
+ />
27
+ </LocalizedClientLink>
28
+ }
29
+ >
30
+ <CartButton />
31
+ </Suspense>
32
+ )
33
+
34
+ return (
35
+ <Suspense fallback={null}>
36
+ <DesktopSearch countryCode={currentLocale || "in"} hideNavLinks>
37
+ <NavHeaderShell>
38
+ <NavHeaderContent
39
+ currentLocale={currentLocale || "in"}
40
+ customer={customer}
41
+ categories={categories}
42
+ collections={collections}
43
+ logo={<DynamicLogo />}
44
+ cartButton={cartButton}
45
+ />
46
+ </NavHeaderShell>
47
+ </DesktopSearch>
48
+ </Suspense>
49
+ )
50
+ }
@@ -0,0 +1,74 @@
1
+ "use client"
2
+
3
+ import { useRef, useState } from "react"
4
+ import { HttpTypes } from "@medusajs/types"
5
+ import LocalizedClientLink from "@modules/common/components/localized-client-link"
6
+
7
+ type NavCategoriesDropdownProps = {
8
+ categories: HttpTypes.StoreProductCategory[]
9
+ }
10
+
11
+ export default function NavCategoriesDropdown({
12
+ categories,
13
+ }: NavCategoriesDropdownProps) {
14
+ const [open, setOpen] = useState(false)
15
+ const closeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
16
+
17
+ const handleOpen = () => {
18
+ if (closeTimerRef.current) {
19
+ clearTimeout(closeTimerRef.current)
20
+ closeTimerRef.current = null
21
+ }
22
+ setOpen(true)
23
+ }
24
+
25
+ const handleClose = () => {
26
+ closeTimerRef.current = setTimeout(() => setOpen(false), 180)
27
+ }
28
+
29
+ if (!categories.length) {
30
+ return (
31
+ <LocalizedClientLink href="/store" className="nav-luxury-link group">
32
+ <span className="nav-luxury-link-label">Categories</span>
33
+ <span className="nav-luxury-link-line" aria-hidden />
34
+ </LocalizedClientLink>
35
+ )
36
+ }
37
+
38
+ return (
39
+ <div
40
+ className={`nav-categories${open ? " nav-categories--open" : ""}`}
41
+ onMouseEnter={handleOpen}
42
+ onMouseLeave={handleClose}
43
+ onFocus={handleOpen}
44
+ onBlur={handleClose}
45
+ >
46
+ <button
47
+ type="button"
48
+ className="nav-luxury-link nav-categories__trigger group"
49
+ aria-expanded={open}
50
+ aria-haspopup="true"
51
+ >
52
+ <span className="nav-luxury-link-label">Categories</span>
53
+ <span className="nav-luxury-link-line" aria-hidden />
54
+ </button>
55
+
56
+ <div className="nav-categories__panel" role="menu" aria-hidden={!open}>
57
+ <ul className="nav-categories__list">
58
+ {categories.map((category) => (
59
+ <li key={category.id} role="none">
60
+ <LocalizedClientLink
61
+ href={`/store?category=${category.id}`}
62
+ className="nav-categories__link"
63
+ role="menuitem"
64
+ onClick={() => setOpen(false)}
65
+ >
66
+ <span className="nav-categories__link-text">{category.name}</span>
67
+ </LocalizedClientLink>
68
+ </li>
69
+ ))}
70
+ </ul>
71
+ </div>
72
+ </div>
73
+ )
74
+ }