@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,108 @@
1
+ export type SizeChartRow = Record<string, string>
2
+
3
+ export type SizeChartSection = {
4
+ id: string
5
+ title: string
6
+ subtitle?: string
7
+ columns: { key: string; label: string; align?: "left" | "center" }[]
8
+ rows: SizeChartRow[]
9
+ }
10
+
11
+ export const KURTI_SIZE_CHART: SizeChartSection = {
12
+ id: "kurti",
13
+ title: "Kurti & Kurti Sets",
14
+ subtitle: "Body measurements in inches",
15
+ columns: [
16
+ { key: "size", label: "Size" },
17
+ { key: "bust", label: "Bust", align: "center" },
18
+ { key: "waist", label: "Waist", align: "center" },
19
+ { key: "hip", label: "Hip", align: "center" },
20
+ { key: "length", label: "Length", align: "center" },
21
+ ],
22
+ rows: [
23
+ { size: "XS", bust: "34", waist: "30", hip: "36", length: "38" },
24
+ { size: "S", bust: "36", waist: "32", hip: "38", length: "40" },
25
+ { size: "M", bust: "38", waist: "34", hip: "40", length: "42" },
26
+ { size: "L", bust: "40", waist: "36", hip: "42", length: "44" },
27
+ { size: "XL", bust: "42", waist: "38", hip: "44", length: "46" },
28
+ { size: "XXL", bust: "44", waist: "40", hip: "46", length: "48" },
29
+ ],
30
+ }
31
+
32
+ export const SAREE_BLOUSE_SIZE_CHART: SizeChartSection = {
33
+ id: "saree-blouse",
34
+ title: "Saree Blouse",
35
+ subtitle: "Blouse size by bust measurement (inches)",
36
+ columns: [
37
+ { key: "size", label: "Size" },
38
+ { key: "bust", label: "Bust", align: "center" },
39
+ { key: "underbust", label: "Under", align: "center" },
40
+ { key: "shoulder", label: "Shldr", align: "center" },
41
+ { key: "length", label: "Len", align: "center" },
42
+ ],
43
+ rows: [
44
+ { size: "32", bust: "32", underbust: "28", shoulder: "13", length: "14" },
45
+ { size: "34", bust: "34", underbust: "30", shoulder: "13.5", length: "14" },
46
+ { size: "36", bust: "36", underbust: "32", shoulder: "14", length: "15" },
47
+ { size: "38", bust: "38", underbust: "34", shoulder: "14.5", length: "15" },
48
+ { size: "40", bust: "40", underbust: "36", shoulder: "15", length: "16" },
49
+ { size: "42", bust: "42", underbust: "38", shoulder: "15.5", length: "16" },
50
+ ],
51
+ }
52
+
53
+ export const SAREE_LENGTH_CHART: SizeChartSection = {
54
+ id: "saree-length",
55
+ title: "Saree Drape Guide",
56
+ subtitle: "Standard unstitched saree dimensions",
57
+ columns: [
58
+ { key: "type", label: "Type" },
59
+ { key: "length", label: "Length", align: "center" },
60
+ { key: "width", label: "Width", align: "center" },
61
+ { key: "blouse", label: "Blouse", align: "center" },
62
+ ],
63
+ rows: [
64
+ {
65
+ type: "Standard",
66
+ length: "5.5 m",
67
+ width: "44–48\"",
68
+ blouse: "0.8 m",
69
+ },
70
+ {
71
+ type: "Designer",
72
+ length: "5.5–6 m",
73
+ width: "48\"",
74
+ blouse: "1 m",
75
+ },
76
+ {
77
+ type: "Pre-Stitched",
78
+ length: "Free",
79
+ width: "Adj.",
80
+ blouse: "Yes",
81
+ },
82
+ ],
83
+ }
84
+
85
+ export const SIZE_CHART_SECTIONS = [
86
+ KURTI_SIZE_CHART,
87
+ SAREE_BLOUSE_SIZE_CHART,
88
+ SAREE_LENGTH_CHART,
89
+ ]
90
+
91
+ export const SIZE_CHART_MEASURE_TIPS = [
92
+ {
93
+ title: "Bust",
94
+ text: "Measure around the fullest part of your chest, keeping the tape parallel to the floor.",
95
+ },
96
+ {
97
+ title: "Waist",
98
+ text: "Measure around your natural waistline — typically the narrowest part of your torso.",
99
+ },
100
+ {
101
+ title: "Hip",
102
+ text: "Measure around the fullest part of your hips, about 7–9 inches below the waist.",
103
+ },
104
+ {
105
+ title: "Length",
106
+ text: "For kurtis, measure from shoulder to desired hem. For blouses, shoulder to waist.",
107
+ },
108
+ ]
@@ -0,0 +1,258 @@
1
+ "use client"
2
+
3
+ import { useEffect, useMemo, useState } from "react"
4
+ import Image from "next/image"
5
+ import LocalizedClientLink from "@modules/common/components/localized-client-link"
6
+ import { getProductPrice } from "@core/util/get-product-price"
7
+ import { convertToLocale } from "@core/util/money"
8
+ import PlaceholderImage from "@modules/common/icons/placeholder-image"
9
+ import ProductCardRatingDisplay from "@modules/common/components/product/product-card-rating"
10
+ import WishlistIcon from "@modules/common/components/product/wishlist-icon"
11
+ import {
12
+ QuickAddContent,
13
+ useProductQuickAdd,
14
+ } from "@modules/wishlist/components/wishlist-item"
15
+ import { clx } from "@medusajs/ui"
16
+ import type { ProductCardProps } from "@core/types/product-card"
17
+
18
+ type Props = ProductCardProps & {
19
+ imageClassName?: string
20
+ quickAddClassName?: string
21
+ showWishlistIcon?: boolean
22
+ }
23
+
24
+ export default function ProductCard({
25
+ product,
26
+ region,
27
+ className,
28
+ imageClassName,
29
+ quickAddClassName,
30
+ showWishlistIcon,
31
+ rating,
32
+ }: Props) {
33
+ const [imageHover, setImageHover] = useState(false)
34
+ const {
35
+ showQuickAdd,
36
+ closeQuickAdd,
37
+ selectedOptions,
38
+ setOptionValue,
39
+ quantity,
40
+ setQuantity,
41
+ showError,
42
+ isAdding,
43
+ selectedVariantPrice,
44
+ handleQuickAdd,
45
+ cart,
46
+ } = useProductQuickAdd(product)
47
+
48
+ const { cheapestPrice } = getProductPrice({ product })
49
+ const thumbnail = product.thumbnail
50
+ const secondImage = product.images?.[1]?.url
51
+
52
+ const priceLabel = useMemo(() => {
53
+ if (!cheapestPrice) return null
54
+ return convertToLocale({
55
+ amount: cheapestPrice.calculated_price_number,
56
+ currency_code: cheapestPrice.currency_code,
57
+ })
58
+ }, [cheapestPrice])
59
+
60
+ const compareLabel = useMemo(() => {
61
+ if (!cheapestPrice?.original_price_number) return null
62
+ if (cheapestPrice.calculated_price_number >= cheapestPrice.original_price_number)
63
+ return null
64
+ return convertToLocale({
65
+ amount: cheapestPrice.original_price_number,
66
+ currency_code: cheapestPrice.currency_code,
67
+ })
68
+ }, [cheapestPrice])
69
+
70
+ const discountPercent = useMemo(() => {
71
+ if (!cheapestPrice?.original_price_number) return null
72
+ if (cheapestPrice.calculated_price_number >= cheapestPrice.original_price_number)
73
+ return null
74
+
75
+ const fromField = Number.parseInt(
76
+ String(cheapestPrice.percentage_diff ?? "").replace(/\D/g, ""),
77
+ 10
78
+ )
79
+ if (fromField > 0) return fromField
80
+
81
+ return Math.round(
82
+ ((cheapestPrice.original_price_number - cheapestPrice.calculated_price_number) /
83
+ cheapestPrice.original_price_number) *
84
+ 100
85
+ )
86
+ }, [cheapestPrice])
87
+
88
+ const onQuickAdd = (e: React.MouseEvent) => {
89
+ handleQuickAdd(e)
90
+ }
91
+
92
+ useEffect(() => {
93
+ const handleClickOutside = (e: MouseEvent) => {
94
+ if (!showQuickAdd || !product.id) return
95
+ const target = e.target as HTMLElement
96
+ if (!target.closest(`[data-product-id="${product.id}"]`)) {
97
+ closeQuickAdd()
98
+ }
99
+ }
100
+
101
+ if (showQuickAdd) {
102
+ document.addEventListener("mousedown", handleClickOutside)
103
+ }
104
+
105
+ return () => {
106
+ document.removeEventListener("mousedown", handleClickOutside)
107
+ }
108
+ }, [showQuickAdd, product.id, closeQuickAdd])
109
+
110
+ return (
111
+ <article
112
+ data-product-id={product.id}
113
+ className={clx("product-card group flex flex-col w-full h-full relative", className)}
114
+ >
115
+ <LocalizedClientLink
116
+ href={`/products/${product.handle}`}
117
+ className="product-card__link flex flex-col h-full"
118
+ data-testid="product-card"
119
+ >
120
+ <div
121
+ className={clx(
122
+ "product-card__media relative overflow-hidden bg-surface-muted",
123
+ imageClassName
124
+ )}
125
+ onMouseEnter={() => setImageHover(true)}
126
+ onMouseLeave={() => setImageHover(false)}
127
+ >
128
+ {thumbnail ? (
129
+ <>
130
+ <Image
131
+ src={thumbnail}
132
+ alt={product.title || "Product"}
133
+ fill
134
+ sizes="(max-width:640px) 50vw, (max-width:1024px) 33vw, 25vw"
135
+ className={clx(
136
+ "object-cover transition-opacity duration-500",
137
+ imageHover && secondImage ? "opacity-0" : "opacity-100"
138
+ )}
139
+ />
140
+ {secondImage && (
141
+ <Image
142
+ src={secondImage}
143
+ alt=""
144
+ fill
145
+ sizes="(max-width:640px) 50vw, (max-width:1024px) 33vw, 25vw"
146
+ className={clx(
147
+ "object-cover transition-opacity duration-500",
148
+ imageHover ? "opacity-100" : "opacity-0"
149
+ )}
150
+ />
151
+ )}
152
+ </>
153
+ ) : (
154
+ <div className="absolute inset-0 flex items-center justify-center">
155
+ <PlaceholderImage size={48} />
156
+ </div>
157
+ )}
158
+
159
+ <div
160
+ className="product-card__actions"
161
+ onClick={(e) => {
162
+ e.preventDefault()
163
+ e.stopPropagation()
164
+ }}
165
+ >
166
+ {showWishlistIcon && product.id && (
167
+ <WishlistIcon productId={product.id} variant="card" />
168
+ )}
169
+ <button
170
+ type="button"
171
+ className="product-card__cart-btn"
172
+ onClick={onQuickAdd}
173
+ disabled={isAdding}
174
+ aria-label={isAdding ? "Adding to cart" : "Add to cart"}
175
+ >
176
+ <svg
177
+ xmlns="http://www.w3.org/2000/svg"
178
+ viewBox="0 0 24 24"
179
+ fill="none"
180
+ strokeWidth="2"
181
+ strokeLinecap="round"
182
+ strokeLinejoin="round"
183
+ className="product-card__cart-icon"
184
+ aria-hidden
185
+ >
186
+ <path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"></path>
187
+ <line x1="3" y1="6" x2="21" y2="6"></line>
188
+ <path d="M16 10a4 4 0 0 1-8 0"></path>
189
+ </svg>
190
+ </button>
191
+ </div>
192
+
193
+ {!showQuickAdd && (
194
+ <div className="product-card__quick-add-wrap">
195
+ <button
196
+ type="button"
197
+ onClick={onQuickAdd}
198
+ disabled={isAdding}
199
+ className={clx(
200
+ "product-card__quick-add w-full disabled:opacity-60",
201
+ quickAddClassName
202
+ )}
203
+ >
204
+ <span className="product-card__quick-add-label">
205
+ {isAdding ? "Adding…" : "Quick add"}
206
+ </span>
207
+ </button>
208
+ </div>
209
+ )}
210
+
211
+ {showQuickAdd && (
212
+ <div
213
+ className="product-card__quick-add-panel"
214
+ onClick={(e) => {
215
+ e.preventDefault()
216
+ e.stopPropagation()
217
+ }}
218
+ >
219
+ <QuickAddContent
220
+ product={product}
221
+ selectedOptions={selectedOptions}
222
+ setOptionValue={setOptionValue}
223
+ handleAddToCart={handleQuickAdd}
224
+ isAdding={isAdding}
225
+ showError={showError}
226
+ onClose={closeQuickAdd}
227
+ isMobile={false}
228
+ selectedVariantPrice={selectedVariantPrice}
229
+ quantity={quantity}
230
+ setQuantity={setQuantity}
231
+ cart={cart}
232
+ actionLabel="Add to Bag"
233
+ />
234
+ </div>
235
+ )}
236
+ </div>
237
+
238
+ <div className="product-card__info flex-1">
239
+ <h3 className="product-card__title text-heading group-hover:underline underline-offset-2">
240
+ {product.title}
241
+ </h3>
242
+ <ProductCardRatingDisplay product={product} rating={rating} />
243
+ <div className="product-card__prices">
244
+ {priceLabel && (
245
+ <span className="product-card__price">{priceLabel}</span>
246
+ )}
247
+ {compareLabel && (
248
+ <span className="product-card__compare">{compareLabel}</span>
249
+ )}
250
+ {discountPercent != null && discountPercent > 0 && (
251
+ <span className="product-card__discount">{discountPercent}% OFF</span>
252
+ )}
253
+ </div>
254
+ </div>
255
+ </LocalizedClientLink>
256
+ </article>
257
+ )
258
+ }
@@ -0,0 +1,35 @@
1
+ import { HttpTypes } from "@medusajs/types"
2
+ import ProductRating from "@modules/common/components/product/product-rating"
3
+ import ShareButton from "@modules/common/components/product/share-button"
4
+ import WishlistIcon from "@modules/common/components/product/wishlist-icon"
5
+
6
+ type ProductInfoProps = {
7
+ product: HttpTypes.StoreProduct
8
+ region: HttpTypes.StoreRegion
9
+ }
10
+
11
+ export default function ProductInfo({ product }: ProductInfoProps) {
12
+ const metadataBrand = product.metadata?.brand as string | undefined
13
+ const brand = metadataBrand || product.collection?.title || "Sahsha"
14
+
15
+ return (
16
+ <div id="product-info" className="product-info">
17
+ <p className="product-info__brand">{brand}</p>
18
+
19
+ <div className="product-info__title-row">
20
+ <h1 className="product-info__title" data-testid="product-title">
21
+ {product.title}
22
+ </h1>
23
+ <div className="product-info__actions">
24
+ {product.id && <WishlistIcon productId={product.id} />}
25
+ <ShareButton
26
+ productTitle={product.title ?? ""}
27
+ productHandle={product.handle ?? ""}
28
+ />
29
+ </div>
30
+ </div>
31
+
32
+ <ProductRating product={product} showButton={false} variant="badge" />
33
+ </div>
34
+ )
35
+ }
@@ -0,0 +1,72 @@
1
+ import Image from "next/image"
2
+ import { HttpTypes } from "@medusajs/types"
3
+ import LocalizedClientLink from "@modules/common/components/localized-client-link"
4
+ import PlaceholderImage from "@modules/common/icons/placeholder-image"
5
+ import {
6
+ getCollectionImage,
7
+ getCollectionProductCount,
8
+ } from "@lib/category-collection-images"
9
+
10
+ type CollectionsPageProps = {
11
+ collections: HttpTypes.StoreCollection[]
12
+ }
13
+
14
+ export default function CollectionsPage({ collections }: CollectionsPageProps) {
15
+ return (
16
+ <div className="collections-index min-h-screen bg-page-bg">
17
+ <div
18
+ className="collections-index__inner mx-auto"
19
+ style={{ maxWidth: "var(--container-max)" }}
20
+ >
21
+ <nav className="collections-index__breadcrumb" aria-label="Breadcrumb">
22
+ <LocalizedClientLink href="/">Home</LocalizedClientLink>
23
+ <span aria-hidden>/</span>
24
+ <span>Collections</span>
25
+ </nav>
26
+
27
+ <h1 className="collections-index__title">Collections</h1>
28
+
29
+ <ul className="collections-index__grid">
30
+ {collections.map((collection) => {
31
+ const imageUrl = getCollectionImage(collection)
32
+ const productCount = getCollectionProductCount(collection)
33
+ const title = collection.title || "Collection"
34
+
35
+ return (
36
+ <li key={collection.id} className="collections-index__item">
37
+ <LocalizedClientLink
38
+ href={`/store?collection=${collection.id}`}
39
+ className="collections-index__link"
40
+ >
41
+ <div className="collections-index__media">
42
+ {imageUrl ? (
43
+ <Image
44
+ src={imageUrl}
45
+ alt={title}
46
+ fill
47
+ unoptimized
48
+ sizes="(max-width: 640px) 50vw, 25vw"
49
+ className="collections-index__image"
50
+ />
51
+ ) : (
52
+ <div className="collections-index__placeholder">
53
+ <PlaceholderImage size={40} />
54
+ </div>
55
+ )}
56
+ </div>
57
+
58
+ <p className="collections-index__name">
59
+ {title}
60
+ {productCount > 0 && (
61
+ <sup className="collections-index__count">{productCount}</sup>
62
+ )}
63
+ </p>
64
+ </LocalizedClientLink>
65
+ </li>
66
+ )
67
+ })}
68
+ </ul>
69
+ </div>
70
+ </div>
71
+ )
72
+ }
@@ -0,0 +1,134 @@
1
+ import { Suspense } from "react"
2
+ import SkeletonProductGrid from "@modules/skeletons/templates/skeleton-product-grid"
3
+ import RefinementList from "@modules/store/components/refinement-list"
4
+ import { SortOptions } from "@modules/store/components/refinement-list/sort-products"
5
+ import PaginatedProducts from "@modules/store/templates/paginated-products"
6
+ import ShopPageBanner from "@modules/store/components/shop-page-banner"
7
+ import { HttpTypes } from "@medusajs/types"
8
+ import LocalizedClientLink from "@modules/common/components/localized-client-link"
9
+ import type { StoreMetadataFilterKey } from "@lib/store-filter-config"
10
+
11
+ type StorePageProps = {
12
+ sortBy?: SortOptions
13
+ page?: string
14
+ countryCode: string
15
+ collections?: HttpTypes.StoreCollection[]
16
+ collection?: string | string[]
17
+ category?: string | string[]
18
+ color?: string | string[]
19
+ size?: string | string[]
20
+ style?: string | string[]
21
+ neck?: string | string[]
22
+ ornamentation?: string | string[]
23
+ minPrice?: string
24
+ maxPrice?: string
25
+ colorOptions?: { value: string; label: string; hex?: string }[]
26
+ sizeOptions?: { value: string; label: string }[]
27
+ metadataFilters?: {
28
+ param: StoreMetadataFilterKey
29
+ title: string
30
+ options: { value: string; label: string }[]
31
+ }[]
32
+ categoryOptions?: { value: string; label: string }[]
33
+ q?: string
34
+ }
35
+
36
+ export default function StorePage({
37
+ sortBy,
38
+ page,
39
+ countryCode,
40
+ collections,
41
+ collection,
42
+ category,
43
+ color,
44
+ size,
45
+ style,
46
+ neck,
47
+ ornamentation,
48
+ minPrice,
49
+ maxPrice,
50
+ colorOptions,
51
+ sizeOptions,
52
+ metadataFilters,
53
+ categoryOptions,
54
+ q,
55
+ }: StorePageProps) {
56
+ const pageNumber = page ? parseInt(page, 10) : 1
57
+ const sort = sortBy || "created_at_desc"
58
+
59
+ const filterProps = {
60
+ sortBy: sort,
61
+ collections,
62
+ collectionValue: collection,
63
+ categoryValue: category,
64
+ categoryOptions,
65
+ colorValue: color,
66
+ sizeValue: size,
67
+ styleValue: style,
68
+ neckValue: neck,
69
+ ornamentationValue: ornamentation,
70
+ minPrice,
71
+ maxPrice,
72
+ colorOptions,
73
+ sizeOptions,
74
+ metadataFilters,
75
+ q,
76
+ }
77
+
78
+ return (
79
+ <div className="store-page min-h-screen bg-white">
80
+ <ShopPageBanner />
81
+
82
+ <div className="store-page__container">
83
+ <nav className="store-page__breadcrumb" aria-label="Breadcrumb">
84
+ <LocalizedClientLink href="/">Home</LocalizedClientLink>
85
+ <span aria-hidden>/</span>
86
+ <span>Shop</span>
87
+ </nav>
88
+
89
+ <div className="store-page__layout">
90
+ <aside className="store-page__sidebar">
91
+ <h2 className="store-page__sidebar-title">Shop by</h2>
92
+ <RefinementList {...filterProps} />
93
+ </aside>
94
+
95
+ <div className="store-page__main">
96
+ {q && (
97
+ <div className="store-page__search-banner">
98
+ <span>
99
+ Results for <strong>&ldquo;{q}&rdquo;</strong>
100
+ </span>
101
+ <LocalizedClientLink href="/store">Clear</LocalizedClientLink>
102
+ </div>
103
+ )}
104
+
105
+ <Suspense fallback={<SkeletonProductGrid />}>
106
+ <PaginatedProducts
107
+ sortBy={sort}
108
+ page={pageNumber}
109
+ countryCode={countryCode}
110
+ collectionId={
111
+ Array.isArray(collection) ? collection.join(",") : collection
112
+ }
113
+ categoryId={
114
+ Array.isArray(category) ? category.join(",") : category
115
+ }
116
+ color={Array.isArray(color) ? color.join(",") : color}
117
+ size={Array.isArray(size) ? size.join(",") : size}
118
+ style={Array.isArray(style) ? style.join(",") : style}
119
+ neck={Array.isArray(neck) ? neck.join(",") : neck}
120
+ ornamentation={
121
+ Array.isArray(ornamentation) ? ornamentation.join(",") : ornamentation
122
+ }
123
+ min_price={minPrice}
124
+ max_price={maxPrice}
125
+ q={q}
126
+ filterProps={filterProps}
127
+ />
128
+ </Suspense>
129
+ </div>
130
+ </div>
131
+ </div>
132
+ </div>
133
+ )
134
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Semantic Tailwind classes mapped to CSS variables in theme.css.
3
+ * Do not hardcode hex values here — edit theme.css instead.
4
+ */
5
+ export const colorClasses = {
6
+ pageBg: "bg-page-bg",
7
+ surface: "bg-surface",
8
+ surfaceMuted: "bg-surface-muted",
9
+ brandAccent: "bg-brand-accent text-inverse",
10
+ brandAccentText: "text-brand-accent",
11
+ brandAccentBorder: "border-brand-accent",
12
+ brandAccentMuted: "bg-brand-accent-muted",
13
+ heading: "text-heading",
14
+ headingSub: "text-heading-sub",
15
+ body: "text-body",
16
+ muted: "text-muted",
17
+ inverse: "text-inverse",
18
+ footerBg: "bg-brand-footer",
19
+ promoGradient: "bg-promo-gradient",
20
+ cartBorder: "bg-cart-border",
21
+ } as const
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Sitewide typography — only two families:
3
+ * - Playfair Display: page titles, section headings (h1–h6, .font-display)
4
+ * - Montserrat: body, paragraphs, buttons, product names, prices, labels, nav
5
+ */
6
+ export const fontClasses = {
7
+ sans: "font-sans",
8
+ heading: "font-heading",
9
+ display: "font-display",
10
+ quote: "font-quote",
11
+ mono: "font-sans",
12
+ body: "font-sans leading-body tracking-body",
13
+ pageTitle: "font-heading font-normal leading-heading tracking-heading",
14
+ uiLabel: "font-sans text-xs font-semibold uppercase tracking-[var(--letter-spacing-nav)]",
15
+ productName: "font-sans font-medium",
16
+ } as const
@@ -0,0 +1,3 @@
1
+ export * from "./colors"
2
+ export * from "./fonts"
3
+ export * from "./spacing"
@@ -0,0 +1,9 @@
1
+ export const spacing = {
2
+ sectionY: {
3
+ sm: "py-8",
4
+ md: "py-12",
5
+ lg: "py-16",
6
+ },
7
+ container: "max-w-[1440px] mx-auto",
8
+ content: "max-w-[1360px] mx-auto",
9
+ } as const