@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,112 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { clx } from "@medusajs/ui"
|
|
4
|
+
import { HttpTypes } from "@medusajs/types"
|
|
5
|
+
import { formatMetadataValue, PRODUCT_METADATA_FIELDS } from "./product-metadata-fields"
|
|
6
|
+
|
|
7
|
+
function getMetadataDisplay(
|
|
8
|
+
metadata: Record<string, unknown> | null | undefined,
|
|
9
|
+
key: string
|
|
10
|
+
) {
|
|
11
|
+
const field = PRODUCT_METADATA_FIELDS.find((item) => item.key === key)
|
|
12
|
+
if (!field || !metadata) {
|
|
13
|
+
return null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return formatMetadataValue(metadata[key], field)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function ProductHighlightsSection({ product }: { product: HttpTypes.StoreProduct }) {
|
|
20
|
+
return (
|
|
21
|
+
<div className="product-highlights flex flex-col gap-6 py-8 border-t border-[#e5e5e5]">
|
|
22
|
+
<div className="flex items-center gap-3">
|
|
23
|
+
<div className="product-highlights__accent" aria-hidden />
|
|
24
|
+
<h3 className="product-highlights__title">Product highlights</h3>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div className="grid grid-cols-2 gap-3 sm:gap-4 mt-2">
|
|
28
|
+
{(() => {
|
|
29
|
+
// Extract unique colors from variants/options instead of metadata
|
|
30
|
+
const colorOpt = product.options?.find((opt: HttpTypes.StoreProductOption) =>
|
|
31
|
+
opt.title?.toLowerCase().includes("color") ||
|
|
32
|
+
opt.title?.toLowerCase().includes("colour")
|
|
33
|
+
)
|
|
34
|
+
const variantColors =
|
|
35
|
+
colorOpt?.values?.map((v: { value?: string }) => v.value).filter(Boolean) || []
|
|
36
|
+
const uniqueColors = Array.from(new Set(variantColors))
|
|
37
|
+
const colorValue = uniqueColors.length > 0 ? uniqueColors.join(', ') : null
|
|
38
|
+
|
|
39
|
+
const highlights = [
|
|
40
|
+
{ id: 'style', label: "Style", value: getMetadataDisplay(product.metadata, 'style'), icon: 'M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5' },
|
|
41
|
+
{ id: 'fabric', label: "Fabric", value: getMetadataDisplay(product.metadata, 'fabric'), icon: 'M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5' },
|
|
42
|
+
{ id: 'top_pattern', label: "Pattern", value: getMetadataDisplay(product.metadata, 'top_pattern'), icon: 'M20.59 13.41l-7.17 7.17a2 2 0 01-2.83 0L2 12V2h10l8.59 8.59a2 2 0 010 2.82z' },
|
|
43
|
+
{ id: 'top_length', label: "Length", value: getMetadataDisplay(product.metadata, 'top_length'), icon: 'M12 19V5M5 12l7-7 7 7' },
|
|
44
|
+
{ id: 'color', label: "Available Colors", value: colorValue, icon: 'M12 2.25l-7.5 12.13a6.75 6.75 0 1015 0L12 2.25z' },
|
|
45
|
+
].filter(h => h.value)
|
|
46
|
+
|
|
47
|
+
return highlights.map((highlight) => (
|
|
48
|
+
<div
|
|
49
|
+
key={highlight.id}
|
|
50
|
+
className={clx(
|
|
51
|
+
"product-highlights__card group flex items-center gap-3 sm:gap-4 p-3 sm:p-4",
|
|
52
|
+
highlight.id === 'color' && "col-span-2"
|
|
53
|
+
)}
|
|
54
|
+
>
|
|
55
|
+
<div className="flex-shrink-0 w-10 h-10 sm:w-12 sm:h-12 rounded-xl bg-surface-muted flex items-center justify-center group-hover:bg-purple-50 transition-all duration-300 shadow-inner overflow-hidden">
|
|
56
|
+
{highlight.id === 'color' ? (
|
|
57
|
+
<svg width="28" height="28" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg" className="transform group-hover:scale-110 transition-transform duration-300">
|
|
58
|
+
{/* Palette Body */}
|
|
59
|
+
<path d="M75 55c0 15-12 27-27 27S21 70 21 55s12-27 27-27 27 12 27 27z" fill="#E8B088" stroke="#333" strokeWidth="3" />
|
|
60
|
+
|
|
61
|
+
{/* Color Dots */}
|
|
62
|
+
<circle cx="40" cy="45" r="5" fill="#FF4D4D" stroke="#333" strokeWidth="1.5" /> {/* Red */}
|
|
63
|
+
<circle cx="56" cy="46" r="5" fill="#2ECC71" stroke="#333" strokeWidth="1.5" /> {/* Green */}
|
|
64
|
+
<circle cx="42" cy="62" r="5" fill="#3498DB" stroke="#333" strokeWidth="1.5" /> {/* Blue */}
|
|
65
|
+
<circle cx="58" cy="64" r="5" fill="#F06292" stroke="#333" strokeWidth="1.5" /> {/* Pink */}
|
|
66
|
+
|
|
67
|
+
{/* Brush */}
|
|
68
|
+
<g transform="rotate(25 70 40)">
|
|
69
|
+
<rect x="68" y="15" width="6" height="35" rx="3" fill="#F39C12" stroke="#333" strokeWidth="2" /> {/* Handle */}
|
|
70
|
+
<path d="M68 12c0-4 3-8 3-8s3 4 3 8H68z" fill="#AED6F1" stroke="#333" strokeWidth="2" /> {/* Tip */}
|
|
71
|
+
<rect x="68" y="45" width="6" height="4" fill="#BDC3C7" stroke="#333" strokeWidth="1.5" /> {/* Ferrule */}
|
|
72
|
+
</g>
|
|
73
|
+
</svg>
|
|
74
|
+
) : (
|
|
75
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" className="w-[18px] h-[18px] sm:w-5 sm:h-5 text-brand-accent">
|
|
76
|
+
<path d={highlight.icon} />
|
|
77
|
+
</svg>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
80
|
+
<div className="flex flex-col min-w-0 flex-1">
|
|
81
|
+
<span className="text-[9px] sm:text-[10px] font-black text-gray-800 uppercase tracking-widest mb-0.5 sm:mb-1">{highlight.label}</span>
|
|
82
|
+
<div className="flex items-center gap-2">
|
|
83
|
+
{highlight.id === 'color' ? (
|
|
84
|
+
<div className="flex flex-wrap gap-x-4 gap-y-1.5">
|
|
85
|
+
{String(highlight.value || '').split(/[,\s]+/).map((color, i) => {
|
|
86
|
+
const trimmedColor = color.trim().toLowerCase()
|
|
87
|
+
if (!trimmedColor) return null
|
|
88
|
+
return (
|
|
89
|
+
<div key={i} className="flex items-center gap-2">
|
|
90
|
+
<div
|
|
91
|
+
className="w-3.5 h-3.5 rounded-full border border-gray-100 shadow-sm"
|
|
92
|
+
style={{ backgroundColor: trimmedColor }}
|
|
93
|
+
/>
|
|
94
|
+
<span className="text-sm text-gray-800 font-semibold capitalize leading-none tracking-tight">{trimmedColor}</span>
|
|
95
|
+
</div>
|
|
96
|
+
)
|
|
97
|
+
})}
|
|
98
|
+
</div>
|
|
99
|
+
) : (
|
|
100
|
+
<span className="text-xs sm:text-sm text-gray-800 font-semibold tracking-tight truncate">
|
|
101
|
+
{String(highlight.value || '')}
|
|
102
|
+
</span>
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
))
|
|
108
|
+
})()}
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import Image from "next/image"
|
|
4
|
+
import { HttpTypes } from "@medusajs/types"
|
|
5
|
+
import OptionSelect from "@modules/products/components/product-actions/option-select"
|
|
6
|
+
import type { ProductOptions } from "@core/domain/product/variant-selection"
|
|
7
|
+
|
|
8
|
+
function getUniqueOptionValues(option: HttpTypes.StoreProductOption): string[] {
|
|
9
|
+
const values =
|
|
10
|
+
option.values?.map((v) => v.value).filter((v): v is string => Boolean(v)) ?? []
|
|
11
|
+
return Array.from(new Set(values))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function shouldShowOptionSection(
|
|
15
|
+
option: HttpTypes.StoreProductOption | undefined,
|
|
16
|
+
colorVariants: HttpTypes.StoreProductVariant[] = []
|
|
17
|
+
): boolean {
|
|
18
|
+
if (!option) return false
|
|
19
|
+
|
|
20
|
+
const isColor =
|
|
21
|
+
option.title?.toLowerCase().includes("color") ||
|
|
22
|
+
option.title?.toLowerCase().includes("colour")
|
|
23
|
+
|
|
24
|
+
if (isColor && colorVariants.length > 1) {
|
|
25
|
+
return true
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const uniqueValues = getUniqueOptionValues(option)
|
|
29
|
+
if (uniqueValues.length <= 1) {
|
|
30
|
+
return false
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const title = (option.title || "").toLowerCase()
|
|
34
|
+
if (title.includes("default")) {
|
|
35
|
+
return false
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return true
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type ProductOptionsSectionProps = {
|
|
42
|
+
product: HttpTypes.StoreProduct
|
|
43
|
+
disabled?: boolean
|
|
44
|
+
isAdding: boolean
|
|
45
|
+
options: ProductOptions
|
|
46
|
+
setOptionValue: (optionId: string, value: string) => void
|
|
47
|
+
setColorValue: (optionId: string, value: string) => void
|
|
48
|
+
validationErrors: Record<string, string>
|
|
49
|
+
colorOption?: HttpTypes.StoreProductOption
|
|
50
|
+
sizeOption?: HttpTypes.StoreProductOption
|
|
51
|
+
otherOptions: HttpTypes.StoreProductOption[]
|
|
52
|
+
colorVariants: HttpTypes.StoreProductVariant[]
|
|
53
|
+
selectedColorValue?: string
|
|
54
|
+
onOpenSizeChart: () => void
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function ProductOptionsSection({
|
|
58
|
+
product,
|
|
59
|
+
disabled,
|
|
60
|
+
isAdding,
|
|
61
|
+
options,
|
|
62
|
+
setOptionValue,
|
|
63
|
+
setColorValue,
|
|
64
|
+
validationErrors,
|
|
65
|
+
colorOption,
|
|
66
|
+
sizeOption,
|
|
67
|
+
otherOptions,
|
|
68
|
+
colorVariants,
|
|
69
|
+
selectedColorValue,
|
|
70
|
+
onOpenSizeChart,
|
|
71
|
+
}: ProductOptionsSectionProps) {
|
|
72
|
+
return (
|
|
73
|
+
<>
|
|
74
|
+
{shouldShowOptionSection(sizeOption) && sizeOption && (
|
|
75
|
+
<div className="flex flex-col gap-3">
|
|
76
|
+
<div className="flex items-center justify-between">
|
|
77
|
+
<div className="flex items-center gap-2">
|
|
78
|
+
<h3 className="font-display text-[11px] font-semibold text-heading uppercase tracking-[var(--letter-spacing-nav)]">
|
|
79
|
+
Select Size
|
|
80
|
+
</h3>
|
|
81
|
+
{validationErrors[sizeOption.id] && (
|
|
82
|
+
<span className="text-[10px] text-red-600 font-black uppercase tracking-wider animate-pulse border-l-2 border-red-600 pl-2">
|
|
83
|
+
Please Select {String(sizeOption.title || "")}
|
|
84
|
+
</span>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
<button
|
|
88
|
+
type="button"
|
|
89
|
+
onClick={onOpenSizeChart}
|
|
90
|
+
className="product-size-chart-link flex items-center gap-1.5 text-xs sm:text-sm font-medium hover:opacity-80"
|
|
91
|
+
>
|
|
92
|
+
<Image
|
|
93
|
+
src="/Size Chart.svg"
|
|
94
|
+
alt=""
|
|
95
|
+
width={16}
|
|
96
|
+
height={16}
|
|
97
|
+
className="product-size-chart-link__icon"
|
|
98
|
+
aria-hidden
|
|
99
|
+
/>
|
|
100
|
+
Size Chart
|
|
101
|
+
</button>
|
|
102
|
+
</div>
|
|
103
|
+
<OptionSelect
|
|
104
|
+
option={sizeOption}
|
|
105
|
+
current={options[sizeOption.id]}
|
|
106
|
+
updateOption={setOptionValue}
|
|
107
|
+
title={sizeOption.title ?? ""}
|
|
108
|
+
data-testid="product-options"
|
|
109
|
+
disabled={!!disabled || isAdding}
|
|
110
|
+
error={!!validationErrors[sizeOption.id]}
|
|
111
|
+
/>
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
114
|
+
|
|
115
|
+
{shouldShowOptionSection(colorOption, colorVariants) && colorOption && (
|
|
116
|
+
<div className="product-color-variants flex flex-col gap-3">
|
|
117
|
+
<div className="flex items-center gap-2">
|
|
118
|
+
<h3 className="font-display text-[11px] font-semibold text-heading uppercase tracking-[var(--letter-spacing-nav)]">
|
|
119
|
+
{colorVariants.length > 0
|
|
120
|
+
? "More Colors"
|
|
121
|
+
: `Select ${colorOption.title}`}
|
|
122
|
+
</h3>
|
|
123
|
+
{validationErrors[colorOption.id] && (
|
|
124
|
+
<span className="text-[10px] text-red-600 font-black uppercase tracking-wider animate-pulse border-l-2 border-red-600 pl-2">
|
|
125
|
+
Please Select {String(colorOption.title || "")}
|
|
126
|
+
</span>
|
|
127
|
+
)}
|
|
128
|
+
</div>
|
|
129
|
+
{colorVariants.length > 0 ? (
|
|
130
|
+
<div className="product-color-variants__list flex gap-2.5 sm:gap-3 overflow-x-auto pb-1">
|
|
131
|
+
{colorVariants.map((variant) => {
|
|
132
|
+
const variantImage =
|
|
133
|
+
(variant.metadata?.image as string) ||
|
|
134
|
+
variant.thumbnail ||
|
|
135
|
+
variant.images?.[0]?.url ||
|
|
136
|
+
product.thumbnail
|
|
137
|
+
const variantColorValue = variant.options?.find(
|
|
138
|
+
(opt: HttpTypes.StoreProductOptionValue) =>
|
|
139
|
+
opt.option_id === colorOption.id
|
|
140
|
+
)?.value
|
|
141
|
+
const isSelected = variantColorValue === selectedColorValue
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<button
|
|
145
|
+
key={variant.id}
|
|
146
|
+
type="button"
|
|
147
|
+
className={`product-color-variants__card relative flex-shrink-0 overflow-hidden border ${
|
|
148
|
+
isSelected
|
|
149
|
+
? "border-brand-accent ring-1 ring-brand-accent"
|
|
150
|
+
: "border-[var(--color-header-border)] hover:border-brand-accent-light"
|
|
151
|
+
}`}
|
|
152
|
+
onClick={() => {
|
|
153
|
+
if (colorOption && variantColorValue) {
|
|
154
|
+
setColorValue(colorOption.id, variantColorValue)
|
|
155
|
+
}
|
|
156
|
+
}}
|
|
157
|
+
title={variantColorValue}
|
|
158
|
+
>
|
|
159
|
+
{variantImage && (
|
|
160
|
+
<Image
|
|
161
|
+
src={variantImage}
|
|
162
|
+
alt={`Color variant: ${variantColorValue || "Unknown"}`}
|
|
163
|
+
fill
|
|
164
|
+
sizes="(max-width: 640px) 72px, 88px"
|
|
165
|
+
className="object-cover"
|
|
166
|
+
/>
|
|
167
|
+
)}
|
|
168
|
+
</button>
|
|
169
|
+
)
|
|
170
|
+
})}
|
|
171
|
+
</div>
|
|
172
|
+
) : (
|
|
173
|
+
<OptionSelect
|
|
174
|
+
option={colorOption}
|
|
175
|
+
current={options[colorOption.id]}
|
|
176
|
+
updateOption={setOptionValue}
|
|
177
|
+
title={colorOption.title ?? ""}
|
|
178
|
+
data-testid="product-options"
|
|
179
|
+
disabled={!!disabled || isAdding}
|
|
180
|
+
error={!!validationErrors[colorOption.id]}
|
|
181
|
+
/>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
)}
|
|
185
|
+
|
|
186
|
+
{otherOptions
|
|
187
|
+
.filter((option) => shouldShowOptionSection(option))
|
|
188
|
+
.sort((a, b) => (a.title || "").localeCompare(b.title || ""))
|
|
189
|
+
.map((option) => (
|
|
190
|
+
<div key={option.id} className="flex flex-col gap-3">
|
|
191
|
+
<div className="flex items-center gap-2">
|
|
192
|
+
<h3 className="text-[11px] font-semibold text-heading uppercase tracking-[var(--letter-spacing-nav)]">
|
|
193
|
+
Select {String(option.title || "")}
|
|
194
|
+
</h3>
|
|
195
|
+
{validationErrors[option.id] && (
|
|
196
|
+
<span className="text-[10px] text-red-600 font-black uppercase tracking-wider animate-pulse border-l-2 border-red-600 pl-2">
|
|
197
|
+
Please Select {String(option.title || "")}
|
|
198
|
+
</span>
|
|
199
|
+
)}
|
|
200
|
+
</div>
|
|
201
|
+
<OptionSelect
|
|
202
|
+
option={option}
|
|
203
|
+
current={options[option.id]}
|
|
204
|
+
updateOption={setOptionValue}
|
|
205
|
+
title={option.title ?? ""}
|
|
206
|
+
data-testid="product-options"
|
|
207
|
+
disabled={!!disabled || isAdding}
|
|
208
|
+
error={!!validationErrors[option.id]}
|
|
209
|
+
/>
|
|
210
|
+
</div>
|
|
211
|
+
))}
|
|
212
|
+
|
|
213
|
+
</>
|
|
214
|
+
)
|
|
215
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatDisplayPrice,
|
|
3
|
+
formatINRPrice,
|
|
4
|
+
type VariantPrice,
|
|
5
|
+
} from "@core/domain/product/pricing"
|
|
6
|
+
|
|
7
|
+
type ProductPriceSectionProps = {
|
|
8
|
+
displayPrice: VariantPrice
|
|
9
|
+
quantityInCart: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ProductPriceSection({
|
|
13
|
+
displayPrice,
|
|
14
|
+
quantityInCart,
|
|
15
|
+
}: ProductPriceSectionProps) {
|
|
16
|
+
const originalPrice = displayPrice?.original_price_number
|
|
17
|
+
const currentPrice = displayPrice?.calculated_price_number
|
|
18
|
+
const discountPercentage =
|
|
19
|
+
originalPrice && currentPrice && originalPrice > currentPrice
|
|
20
|
+
? Math.round(((originalPrice - currentPrice) / originalPrice) * 100)
|
|
21
|
+
: null
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className="product-price">
|
|
25
|
+
<div className="product-price__row">
|
|
26
|
+
<div className="product-price__main">
|
|
27
|
+
<span className="product-price__current">
|
|
28
|
+
{formatDisplayPrice(displayPrice, currentPrice)}
|
|
29
|
+
</span>
|
|
30
|
+
{originalPrice && currentPrice && originalPrice > currentPrice && (
|
|
31
|
+
<>
|
|
32
|
+
<span className="product-price__mrp">
|
|
33
|
+
MRP {formatINRPrice(originalPrice)}
|
|
34
|
+
</span>
|
|
35
|
+
{discountPercentage !== null && (
|
|
36
|
+
<span className="product-price__discount">
|
|
37
|
+
({discountPercentage}% OFF)
|
|
38
|
+
</span>
|
|
39
|
+
)}
|
|
40
|
+
</>
|
|
41
|
+
)}
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
{quantityInCart > 0 && (
|
|
45
|
+
<span className="product-price__in-bag">
|
|
46
|
+
{quantityInCart} in bag
|
|
47
|
+
</span>
|
|
48
|
+
)}
|
|
49
|
+
</div>
|
|
50
|
+
<p className="product-price__tax">inclusive of all taxes</p>
|
|
51
|
+
</div>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import Image from "next/image"
|
|
4
|
+
import PincodeChecker from "@modules/cart/components/pincode-checker"
|
|
5
|
+
|
|
6
|
+
type ProductTrustSectionProps = {
|
|
7
|
+
selectedVariantId?: string
|
|
8
|
+
onOpenFeature: (feature: string) => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function ProductTrustSection({ selectedVariantId, onOpenFeature }: ProductTrustSectionProps) {
|
|
12
|
+
return (
|
|
13
|
+
<>
|
|
14
|
+
<div className="product-trust-bar flex items-center justify-between py-3 px-4 bg-[#f8f4ef] border border-[#e5e5e5] mt-2 mb-2">
|
|
15
|
+
<div className="flex items-center gap-3 sm:gap-4">
|
|
16
|
+
<Image src="/Item.svg" alt="Mastercard" width={40} height={28} className="h-6 sm:h-7 w-auto grayscale-[0.2]" />
|
|
17
|
+
<Image src="/Item (1).svg" alt="Amex" width={40} height={28} className="h-6 sm:h-7 w-auto grayscale-[0.2]" />
|
|
18
|
+
<Image src="/Item (2).svg" alt="Diners" width={40} height={28} className="h-7 sm:h-7 w-auto grayscale-[0.2]" />
|
|
19
|
+
<Image src="/visa.svg" alt="Visa" width={40} height={28} className="h-5 sm:h-6 w-auto grayscale-[0.2]" />
|
|
20
|
+
</div>
|
|
21
|
+
<div className="flex items-center gap-1.5 sm:gap-2 ml-2 sm:ml-0">
|
|
22
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#156229" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
|
|
23
|
+
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
|
24
|
+
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
|
|
25
|
+
</svg>
|
|
26
|
+
<span className="text-[9px] sm:text-[11px] font-bold text-gray-700 uppercase tracking-wider whitespace-nowrap">Secure Payments</span>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
{/* Pincode Checker Section */}
|
|
31
|
+
<div className="mt-0.5 sm:-mt-1.5 mb-0">
|
|
32
|
+
<PincodeChecker variantId={selectedVariantId} />
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
{/* Service Features Section */}
|
|
36
|
+
<div className="product-trust-features flex flex-row overflow-x-auto no-scrollbar py-4 border-t border-[#e5e5e5] gap-4 min-[500px]:gap-0 min-[500px]:justify-between min-[500px]:overflow-visible">
|
|
37
|
+
{/* 7 Days Easy Return */}
|
|
38
|
+
<div className="flex flex-col items-center gap-2 flex-shrink-0 min-w-[120px] min-[500px]:flex-1 min-[500px]:min-w-0 group">
|
|
39
|
+
<div className="w-8 h-8 flex items-center justify-center">
|
|
40
|
+
<Image src="/Return.svg" alt="Return" width={28} height={26} />
|
|
41
|
+
</div>
|
|
42
|
+
<div className="flex items-center gap-1.5 cursor-pointer hover:opacity-80 transition-opacity" onClick={() => onOpenFeature('return')}>
|
|
43
|
+
<span className="text-xs text-gray-800 font-medium text-center whitespace-nowrap leading-none">7 Days Easy Return</span>
|
|
44
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3.5" className="product-trust-features__chevron">
|
|
45
|
+
<polyline points="9 18 15 12 9 6"></polyline>
|
|
46
|
+
</svg>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
{/* Separator */}
|
|
51
|
+
<div className="hidden min-[500px]:block w-px h-12 bg-gray-200 flex-shrink-0"></div>
|
|
52
|
+
|
|
53
|
+
{/* Fast Delivery */}
|
|
54
|
+
<div className="flex flex-col items-center gap-2 flex-shrink-0 min-w-[120px] min-[500px]:flex-1 min-[500px]:min-w-0 group">
|
|
55
|
+
<div className="w-8 h-8 flex items-center justify-center">
|
|
56
|
+
<Image src="/delivery.svg" alt="Delivery" width={30} height={18} />
|
|
57
|
+
</div>
|
|
58
|
+
<div className="flex items-center gap-1.5 cursor-pointer hover:opacity-80 transition-opacity" onClick={() => onOpenFeature('delivery')}>
|
|
59
|
+
<span className="text-xs text-gray-800 font-medium text-center whitespace-nowrap leading-none">Fast Delivery</span>
|
|
60
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3.5" className="product-trust-features__chevron">
|
|
61
|
+
<polyline points="9 18 15 12 9 6"></polyline>
|
|
62
|
+
</svg>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
{/* Separator */}
|
|
67
|
+
<div className="hidden min-[500px]:block w-px h-12 bg-gray-200 flex-shrink-0"></div>
|
|
68
|
+
|
|
69
|
+
{/* Cash On Delivery Available */}
|
|
70
|
+
<div className="flex flex-col items-center gap-2 flex-shrink-0 min-w-[120px] min-[500px]:flex-1 min-[500px]:min-w-0 group">
|
|
71
|
+
<div className="w-8 h-8 flex items-center justify-center">
|
|
72
|
+
<Image src="/Cod.svg" alt="COD" width={30} height={18} />
|
|
73
|
+
</div>
|
|
74
|
+
<div className="flex items-center gap-1.5 cursor-pointer hover:opacity-80 transition-opacity" onClick={() => onOpenFeature('cod')}>
|
|
75
|
+
<span className="text-xs text-gray-800 font-medium text-center whitespace-nowrap leading-none">Cash On Delivery Available</span>
|
|
76
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3.5" className="product-trust-features__chevron">
|
|
77
|
+
<polyline points="9 18 15 12 9 6"></polyline>
|
|
78
|
+
</svg>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SIZE_CHART_MEASURE_TIPS,
|
|
3
|
+
SIZE_CHART_SECTIONS,
|
|
4
|
+
type SizeChartSection,
|
|
5
|
+
} from "./size-chart-data"
|
|
6
|
+
|
|
7
|
+
function SizeChartTable({ section }: { section: SizeChartSection }) {
|
|
8
|
+
return (
|
|
9
|
+
<div className="size-chart__table-wrap">
|
|
10
|
+
<table className="size-chart__table">
|
|
11
|
+
<thead>
|
|
12
|
+
<tr>
|
|
13
|
+
{section.columns.map((column) => (
|
|
14
|
+
<th
|
|
15
|
+
key={column.key}
|
|
16
|
+
className={column.align === "center" ? "text-center" : undefined}
|
|
17
|
+
>
|
|
18
|
+
{column.label}
|
|
19
|
+
</th>
|
|
20
|
+
))}
|
|
21
|
+
</tr>
|
|
22
|
+
</thead>
|
|
23
|
+
<tbody>
|
|
24
|
+
{section.rows.map((row, index) => (
|
|
25
|
+
<tr key={`${section.id}-${index}`}>
|
|
26
|
+
{section.columns.map((column) => (
|
|
27
|
+
<td
|
|
28
|
+
key={column.key}
|
|
29
|
+
className={column.align === "center" ? "text-center" : undefined}
|
|
30
|
+
>
|
|
31
|
+
{row[column.key]}
|
|
32
|
+
</td>
|
|
33
|
+
))}
|
|
34
|
+
</tr>
|
|
35
|
+
))}
|
|
36
|
+
</tbody>
|
|
37
|
+
</table>
|
|
38
|
+
</div>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function SizeChartPanel() {
|
|
43
|
+
return (
|
|
44
|
+
<div className="size-chart">
|
|
45
|
+
<div className="size-chart__tip">
|
|
46
|
+
<div className="size-chart__tip-icon" aria-hidden>
|
|
47
|
+
💡
|
|
48
|
+
</div>
|
|
49
|
+
<div>
|
|
50
|
+
<h4 className="size-chart__tip-title">Pro Tip</h4>
|
|
51
|
+
<p className="size-chart__tip-text">
|
|
52
|
+
Measure yourself in inches before ordering. If you are between sizes,
|
|
53
|
+
choose the larger size for a comfortable ethnic fit. For saree blouses,
|
|
54
|
+
match your bust measurement to the blouse size chart.
|
|
55
|
+
</p>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<div className="size-chart__measure">
|
|
60
|
+
<h4 className="size-chart__section-title">How to measure</h4>
|
|
61
|
+
<ul className="size-chart__measure-list">
|
|
62
|
+
{SIZE_CHART_MEASURE_TIPS.map((tip) => (
|
|
63
|
+
<li key={tip.title}>
|
|
64
|
+
<strong>{tip.title}:</strong> {tip.text}
|
|
65
|
+
</li>
|
|
66
|
+
))}
|
|
67
|
+
</ul>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div className="size-chart__sections">
|
|
71
|
+
{SIZE_CHART_SECTIONS.map((section) => (
|
|
72
|
+
<section key={section.id} className="size-chart__section">
|
|
73
|
+
<div className="size-chart__section-head">
|
|
74
|
+
<div className="size-chart__section-icon" aria-hidden>
|
|
75
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
76
|
+
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
|
|
77
|
+
<circle cx="12" cy="7" r="4" />
|
|
78
|
+
</svg>
|
|
79
|
+
</div>
|
|
80
|
+
<div>
|
|
81
|
+
<h4 className="size-chart__section-title">{section.title}</h4>
|
|
82
|
+
{section.subtitle && (
|
|
83
|
+
<p className="size-chart__section-subtitle">{section.subtitle}</p>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
<SizeChartTable section={section} />
|
|
88
|
+
</section>
|
|
89
|
+
))}
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
)
|
|
93
|
+
}
|