@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.
- package/README.md +29 -0
- package/assets/hero-desktop.svg +10 -0
- package/assets/hero-mobile.svg +9 -0
- package/assets/logo.svg +3 -0
- package/package.json +60 -0
- package/src/blocks/home/Features/index.tsx +87 -0
- package/src/blocks/home/Hero/index.tsx +98 -0
- package/src/blocks/home/LovedByMoms/bestsellers-carousel.tsx +1 -0
- package/src/blocks/home/LovedByMoms/index.tsx +43 -0
- package/src/blocks/home/LovedByMoms/loved-by-moms-section.tsx +46 -0
- package/src/blocks/home/NewArrivals/index.tsx +91 -0
- package/src/blocks/home/PromotionalBanners/index.tsx +81 -0
- package/src/blocks/home/ShopByAge/collections-showcase-client.tsx +131 -0
- package/src/blocks/home/ShopByAge/collections-showcase-types.ts +4 -0
- package/src/blocks/home/ShopByAge/index.tsx +168 -0
- package/src/blocks/home/ShopByCategory/index.tsx +111 -0
- package/src/blocks/home/Testimonials/index.tsx +25 -0
- package/src/blocks/home/Testimonials/reviews-scroll.tsx +122 -0
- package/src/blocks/home/Testimonials/testimonials-client.tsx +127 -0
- package/src/blocks/home/WhyChooseUs/index.tsx +39 -0
- package/src/components/product-carousel.tsx +79 -0
- package/src/layouts/MainLayoutShell.tsx +14 -0
- package/src/primitives/Button.tsx +31 -0
- package/src/primitives/Card.tsx +32 -0
- package/src/primitives/index.ts +2 -0
- package/src/slots/account/ForgotPassword/index.tsx +1 -0
- package/src/slots/account/GoogleLogin/index.tsx +28 -0
- package/src/slots/account/Login/index.tsx +1 -0
- package/src/slots/account/LoginTemplate/index.tsx +12 -0
- package/src/slots/account/LoginTemplate/login-template-client.tsx +83 -0
- package/src/slots/account/Register/index.tsx +1 -0
- package/src/slots/cart/CartItem/index.tsx +11 -0
- package/src/slots/cart/CartSummary/index.tsx +8 -0
- package/src/slots/checkout/CheckoutForm/index.tsx +1 -0
- package/src/slots/checkout/CheckoutSummary/index.tsx +1 -0
- package/src/slots/layout/Footer/index.tsx +95 -0
- package/src/slots/layout/Nav/index.tsx +50 -0
- package/src/slots/layout/Nav/nav-categories-dropdown.tsx +74 -0
- package/src/slots/layout/Nav/nav-collections-dropdown.tsx +106 -0
- package/src/slots/layout/Nav/nav-header-content.tsx +165 -0
- package/src/slots/layout/Nav/nav-header-shell.tsx +47 -0
- package/src/slots/layout/Nav/nav-link-luxury.tsx +15 -0
- package/src/slots/layout/PromoBar/index.tsx +9 -0
- package/src/slots/layout/PromoBar/promo-bar-content.tsx +118 -0
- package/src/slots/order/OrderDetails/index.tsx +12 -0
- package/src/slots/product/ProductActions/ProductCTASection.tsx +232 -0
- package/src/slots/product/ProductActions/ProductDetailsSection.tsx +200 -0
- package/src/slots/product/ProductActions/ProductFeaturePanel.tsx +150 -0
- package/src/slots/product/ProductActions/ProductHighlightsSection.tsx +112 -0
- package/src/slots/product/ProductActions/ProductOptionsSection.tsx +215 -0
- package/src/slots/product/ProductActions/ProductPriceSection.tsx +53 -0
- package/src/slots/product/ProductActions/ProductTrustSection.tsx +84 -0
- package/src/slots/product/ProductActions/SizeChartPanel.tsx +93 -0
- package/src/slots/product/ProductActions/index.tsx +156 -0
- package/src/slots/product/ProductActions/product-metadata-fields.ts +503 -0
- package/src/slots/product/ProductActions/size-chart-data.ts +108 -0
- package/src/slots/product/ProductCard/index.tsx +258 -0
- package/src/slots/product/ProductInfo/index.tsx +35 -0
- package/src/templates/CollectionsPage/index.tsx +72 -0
- package/src/templates/StorePage/index.tsx +134 -0
- package/src/tokens/colors.ts +21 -0
- package/src/tokens/fonts.ts +16 -0
- package/src/tokens/index.ts +3 -0
- package/src/tokens/spacing.ts +9 -0
- 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>“{q}”</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
|