@pradip1995/theme-valero 1.0.1

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 (44) hide show
  1. package/README.md +16 -0
  2. package/package.json +59 -0
  3. package/src/blocks/home/Features/index.tsx +61 -0
  4. package/src/blocks/home/Hero/index.tsx +102 -0
  5. package/src/blocks/home/LovedByMoms/index.tsx +407 -0
  6. package/src/blocks/home/NewArrivals/index.tsx +48 -0
  7. package/src/blocks/home/ShopByAge/index.tsx +128 -0
  8. package/src/blocks/home/ShopByCategory/index.tsx +409 -0
  9. package/src/blocks/home/Testimonials/index.tsx +697 -0
  10. package/src/blocks/home/WhyChooseUs/index.tsx +62 -0
  11. package/src/layouts/MainLayoutShell.tsx +14 -0
  12. package/src/primitives/Button.tsx +30 -0
  13. package/src/primitives/Card.tsx +32 -0
  14. package/src/primitives/index.ts +2 -0
  15. package/src/slots/account/ForgotPassword/index.tsx +1 -0
  16. package/src/slots/account/Login/index.tsx +1 -0
  17. package/src/slots/account/LoginTemplate/index.tsx +44 -0
  18. package/src/slots/account/Register/index.tsx +1 -0
  19. package/src/slots/cart/CartItem/index.tsx +11 -0
  20. package/src/slots/cart/CartSummary/index.tsx +13 -0
  21. package/src/slots/checkout/CheckoutForm/index.tsx +1 -0
  22. package/src/slots/checkout/CheckoutSummary/index.tsx +1 -0
  23. package/src/slots/layout/Footer/index.tsx +104 -0
  24. package/src/slots/layout/Nav/index.tsx +97 -0
  25. package/src/slots/layout/PromoBar/index.tsx +19 -0
  26. package/src/slots/layout/PromoBar/promo-bar-content.tsx +174 -0
  27. package/src/slots/order/OrderDetails/index.tsx +12 -0
  28. package/src/slots/product/ProductActions/ProductCTASection.tsx +191 -0
  29. package/src/slots/product/ProductActions/ProductDetailsSection.tsx +137 -0
  30. package/src/slots/product/ProductActions/ProductFeaturePanel.tsx +245 -0
  31. package/src/slots/product/ProductActions/ProductHighlightsSection.tsx +99 -0
  32. package/src/slots/product/ProductActions/ProductOptionsSection.tsx +234 -0
  33. package/src/slots/product/ProductActions/ProductPriceSection.tsx +53 -0
  34. package/src/slots/product/ProductActions/ProductTrustSection.tsx +84 -0
  35. package/src/slots/product/ProductActions/index.tsx +161 -0
  36. package/src/slots/product/ProductCard/index.tsx +132 -0
  37. package/src/slots/product/ProductInfo/index.tsx +40 -0
  38. package/src/templates/StorePage/index.tsx +154 -0
  39. package/src/tokens/colors.js +16 -0
  40. package/src/tokens/colors.ts +21 -0
  41. package/src/tokens/fonts.ts +13 -0
  42. package/src/tokens/index.ts +3 -0
  43. package/src/tokens/spacing.ts +9 -0
  44. package/src/tokens/theme.css +91 -0
@@ -0,0 +1,128 @@
1
+ import Image from "next/image"
2
+ import LocalizedClientLink from "@modules/common/components/localized-client-link"
3
+ import { HttpTypes } from "@medusajs/types"
4
+ import PlaceholderImage from "@modules/common/icons/placeholder-image"
5
+
6
+ type ShopByAgeProps = {
7
+ collections: HttpTypes.StoreCollection[]
8
+ }
9
+
10
+ const getIconForCollection = (collection: HttpTypes.StoreCollection): string | null => {
11
+ // First try to get icon from metadata
12
+ const metadata = (collection as any)?.metadata || {}
13
+ const collectionIcon = metadata.collection_icon || null
14
+
15
+ if (collectionIcon) {
16
+ return collectionIcon
17
+ }
18
+
19
+ // Fallback to hardcoded logic if metadata doesn't have icon
20
+ const handle = collection.handle?.toLowerCase() || ""
21
+ const title = collection.title?.toLowerCase() || ""
22
+ const searchText = `${handle} ${title}`
23
+
24
+ if (searchText.includes("newborn") || searchText.includes("new-born") || searchText.includes("0-12")) {
25
+ return "/Newborn.svg"
26
+ }
27
+ if (searchText.includes("toddler") || searchText.includes("1-3")) {
28
+ return "/Toddler.svg"
29
+ }
30
+ if (searchText.includes("kids") || searchText.includes("kid") || searchText.includes("4-7")) {
31
+ return "/Kids.svg"
32
+ }
33
+ if (searchText.includes("juniors") || searchText.includes("junior") || searchText.includes("8-12")) {
34
+ return "/Juniors.svg"
35
+ }
36
+ if (searchText.includes("teens") || searchText.includes("teen") || searchText.includes("13-15")) {
37
+ return "/Teens.svg"
38
+ }
39
+ return null
40
+ }
41
+
42
+ const ShopByAge = ({ collections }: ShopByAgeProps) => {
43
+ if (!collections || collections.length === 0) {
44
+ return null
45
+ }
46
+
47
+ // Reverse collections so that last added appears last (API returns newest first)
48
+ const reversedCollections = [...collections].reverse()
49
+
50
+ return (
51
+ <div className="w-full py-8 sm:py-10 md:py-12 lg:py-16 px-4 sm:px-5 md:px-6">
52
+ <div className="max-w-[1440px] mx-auto">
53
+ <h2 className="text-xl sm:text-2xl md:text-3xl font-bold text-center mb-6 sm:mb-8 md:mb-10 lg:mb-12 text-gray-900">
54
+ Shop By Age
55
+ </h2>
56
+ <div className="grid grid-cols-2 min-[500px]:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4 sm:gap-5 md:gap-6 lg:gap-8 xl:gap-12 justify-items-center">
57
+ {reversedCollections.map((collection) => {
58
+ const metadata = (collection as any)?.metadata || {}
59
+ const collectionImage = metadata.image || (collection as any)?.thumbnail || null
60
+ const iconPath = getIconForCollection(collection)
61
+ const collectionAge = metadata.collection_age || null
62
+
63
+ return (
64
+ <LocalizedClientLink
65
+ key={collection.id}
66
+ href={`/store?collection=${collection.id}`}
67
+ data-ga-event="shop_by_age_click"
68
+ data-ga-label={collection.title || "Collection"}
69
+ data-ga-collection-name={collection.title || ""}
70
+ data-ga-collection-handle={collection.handle || ""}
71
+ data-ga-collection-id={collection.id || ""}
72
+ data-ga-collection-age={collectionAge || ""}
73
+ data-ga-section="Shop by Age"
74
+ className="flex flex-col items-center group transition-transform duration-200 hover:scale-105 w-full"
75
+ >
76
+ {/* Wrap all content in a div with pointer-events-none to force GTM to target the Link */}
77
+ <div className="flex flex-col items-center pointer-events-none">
78
+ <div className="relative w-24 h-24 sm:w-28 sm:h-28 md:w-32 md:h-32 lg:w-40 lg:h-40 xl:w-[180px] xl:h-[180px] mb-2 sm:mb-3 md:mb-4 rounded-full overflow-hidden bg-gray-100 flex items-center justify-center">
79
+ {collectionImage ? (
80
+ <Image
81
+ src={collectionImage}
82
+ alt={collection.title || "Collection"}
83
+ fill
84
+ sizes="(max-width: 640px) 50vw, (max-width: 1024px) 25vw, 20vw"
85
+ className="w-full h-full object-cover"
86
+ />
87
+ ) : iconPath ? (
88
+ <Image
89
+ src={iconPath}
90
+ alt={collection.title || "Collection"}
91
+ fill
92
+ sizes="(max-width: 640px) 50vw, (max-width: 1024px) 25vw, 20vw"
93
+ className="w-full h-full object-contain"
94
+ />
95
+ ) : (
96
+ <div className="w-full h-full flex items-center justify-center">
97
+ <PlaceholderImage size={80} />
98
+ </div>
99
+ )}
100
+ </div>
101
+ <h3 className="text-xs sm:text-sm md:text-base lg:text-lg font-semibold text-gray-900 mb-0.5 sm:mb-1 group-hover:text-pink-500 transition-colors text-center px-1">
102
+ {collection.title}
103
+ </h3>
104
+ {collectionAge && (
105
+ <p className="text-[10px] sm:text-xs md:text-sm text-gray-600 text-center mb-0.5 sm:mb-1 px-1">
106
+ {collectionAge}
107
+ </p>
108
+ )}
109
+ {collection.handle && !collectionAge && (
110
+ <p className="text-[10px] sm:text-xs md:text-sm text-gray-600 text-center px-1">
111
+ {collection.handle
112
+ .split(" ")
113
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
114
+ .join(" ")}
115
+ </p>
116
+ )}
117
+ </div>
118
+ </LocalizedClientLink>
119
+ )
120
+ })}
121
+ </div>
122
+ </div>
123
+ </div>
124
+ )
125
+ }
126
+
127
+ export default ShopByAge
128
+
@@ -0,0 +1,409 @@
1
+ import Image from "next/image"
2
+ import LocalizedClientLink from "@modules/common/components/localized-client-link"
3
+ import { HttpTypes } from "@medusajs/types"
4
+ import PlaceholderImage from "@modules/common/icons/placeholder-image"
5
+
6
+ type ShopByCategoryProps = {
7
+ categories: HttpTypes.StoreProductCategory[]
8
+ }
9
+
10
+ const ShopByCategory = ({ categories }: ShopByCategoryProps) => {
11
+ if (!categories || categories.length === 0) {
12
+ return null
13
+ }
14
+
15
+ // Filter for subcategories (categories with parent_category_id)
16
+ const subcategories = categories.filter(
17
+ (category) => category.parent_category_id && category.parent_category
18
+ )
19
+
20
+ // Find Traditional and Dresses categories
21
+ const traditionalCategory = subcategories.find(c => {
22
+ const name = (c.name || "").toLowerCase()
23
+ return name.includes("traditional")
24
+ })
25
+
26
+ const dressesCategory = subcategories.find(c => {
27
+ const name = (c.name || "").toLowerCase()
28
+ return name.includes("dress")
29
+ })
30
+
31
+ // Get other categories (excluding Traditional and Dresses)
32
+ const otherCategories = subcategories.filter(c => {
33
+ const name = (c.name || "").toLowerCase()
34
+ return !name.includes("traditional") && !name.includes("dress")
35
+ })
36
+
37
+ // Build display order: first 3 other categories, then Traditional (index 3), Dresses (index 4), then 6th category
38
+ const displayCategories: typeof subcategories = []
39
+ let otherIndex = 0
40
+
41
+ // 1. Fill up to first 3 slots with other categories
42
+ while (displayCategories.length < 3 && otherIndex < otherCategories.length) {
43
+ displayCategories.push(otherCategories[otherIndex++])
44
+ }
45
+
46
+ // 2. Add Traditional or next other category
47
+ if (traditionalCategory) {
48
+ displayCategories.push(traditionalCategory)
49
+ } else if (otherIndex < otherCategories.length) {
50
+ displayCategories.push(otherCategories[otherIndex++])
51
+ }
52
+
53
+ // 3. Add Dresses or next other category
54
+ if (dressesCategory) {
55
+ displayCategories.push(dressesCategory)
56
+ } else if (otherIndex < otherCategories.length) {
57
+ displayCategories.push(otherCategories[otherIndex++])
58
+ }
59
+
60
+ // 4. Fill remaining slots with any remaining other categories
61
+ while (otherIndex < otherCategories.length) {
62
+ displayCategories.push(otherCategories[otherIndex++])
63
+ }
64
+
65
+ // No longer slice to 6 - allow all categories
66
+ const finalDisplayCategories = displayCategories
67
+
68
+ if (finalDisplayCategories.length === 0) {
69
+ return null
70
+ }
71
+
72
+ return (
73
+ <div className="w-full py-8 lg:py-16 bg-page-bg">
74
+ <div className="w-full px-4 sm:px-6 md:px-8 lg:px-12 xl:px-16">
75
+ <div className="w-full max-w-[1360px] mx-auto">
76
+ <h2 className="font-display text-2xl sm:text-3xl text-heading text-center mb-8 lg:mb-12">
77
+ Shop by category
78
+ </h2>
79
+ {/* Tablet Grid Layout - 768px to 1023px */}
80
+ <div
81
+ className="hidden md:grid lg:hidden gap-3 md:gap-4"
82
+ style={{
83
+ gridTemplateColumns: 'repeat(4, 1fr)',
84
+ gridAutoRows: 'minmax(280px, 350px)',
85
+ }}
86
+ >
87
+ {finalDisplayCategories.map((category, index) => {
88
+ // Calculate grid positioning based on 6-item repeating pattern
89
+ // Row A (Indices 0,1,2): Large (2 col), Small (1 col), Small (1 col)
90
+ // Row B (Indices 3,4,5): Small (1 col), Small (1 col), Large (2 col)
91
+
92
+ const positionInPattern = index % 6
93
+ const rowGroup = Math.floor(index / 6)
94
+ const rowBase = (rowGroup * 2) + 1
95
+
96
+ let gridColumn = ""
97
+ let gridRow = ""
98
+
99
+ if (positionInPattern === 0) {
100
+ gridColumn = "1 / 3" // Span 2 cols
101
+ gridRow = `${rowBase}`
102
+ } else if (positionInPattern === 1) {
103
+ gridColumn = "3"
104
+ gridRow = `${rowBase}`
105
+ } else if (positionInPattern === 2) {
106
+ gridColumn = "4"
107
+ gridRow = `${rowBase}`
108
+ } else if (positionInPattern === 3) {
109
+ gridColumn = "1"
110
+ gridRow = `${rowBase + 1}`
111
+ } else if (positionInPattern === 4) {
112
+ gridColumn = "2"
113
+ gridRow = `${rowBase + 1}`
114
+ } else if (positionInPattern === 5) {
115
+ gridColumn = "3 / 5" // Span 2 cols
116
+ gridRow = `${rowBase + 1}`
117
+ }
118
+
119
+ // Skip if position not set (shouldn't happen, but safety check)
120
+ if (!gridColumn || !gridRow) {
121
+ return null
122
+ }
123
+
124
+ // Get metadata
125
+ const metadata = (category as any)?.metadata || {}
126
+ const categoryImage = metadata.category_image || null
127
+ const backgroundColor = metadata.category_background_color || "#F3F4F6"
128
+ const showShopNow = metadata.show_shop_now === "true" || metadata.show_shop_now === true
129
+
130
+ // Get parent category name as subtitle
131
+ const parentCategory = (category as any)?.parent_category
132
+ const subtitle = parentCategory?.name || ""
133
+
134
+ // Get subcategory name as main title (in uppercase)
135
+ const mainTitle = (category.name || "").toUpperCase()
136
+
137
+ // Build category URL - use store page with category filter
138
+ const categoryUrl = category.id
139
+ ? `/store?category=${category.id}`
140
+ : "#"
141
+
142
+ return (
143
+ <LocalizedClientLink
144
+ key={`tablet-${category.id}`}
145
+ href={categoryUrl}
146
+ data-ga-event="shop_by_category_click"
147
+ data-ga-label={category.name || "Category"}
148
+ data-ga-category-name={category.name || ""}
149
+ data-ga-category-handle={category.handle || ""}
150
+ data-ga-category-id={category.id || ""}
151
+ data-ga-parent-category={subtitle || ""}
152
+ className="relative rounded-lg overflow-hidden shadow-sm hover:shadow-md transition-shadow duration-200 flex flex-col group w-full"
153
+ style={{
154
+ backgroundColor: backgroundColor,
155
+ gridColumn: gridColumn,
156
+ gridRow: gridRow,
157
+ minHeight: "280px",
158
+ }}
159
+ >
160
+ <div className="flex flex-col h-full p-3 md:p-4 pb-0 md:pb-0 pointer-events-none">
161
+ {/* Category Title */}
162
+ <div className="mb-2 md:mb-3">
163
+ {subtitle && (
164
+ <p className="text-xs text-gray-700 mb-1">{subtitle}</p>
165
+ )}
166
+ <h3 className="text-sm md:text-base lg:text-lg font-bold text-gray-900">
167
+ {mainTitle}
168
+ </h3>
169
+ </div>
170
+
171
+ {/* Category Image */}
172
+ <div className="flex-1 flex items-end justify-center relative" style={{ minHeight: 0 }}>
173
+ {categoryImage ? (
174
+ <div className="w-full h-full flex items-end justify-center">
175
+ <Image
176
+ src={categoryImage}
177
+ alt={category.name || "Category"}
178
+ fill
179
+ sizes="(max-width: 768px) 50vw, (max-width: 1024px) 33vw, 25vw"
180
+ className="object-contain object-bottom"
181
+ />
182
+ </div>
183
+ ) : (
184
+ <div className="w-full h-full flex items-end justify-center">
185
+ <PlaceholderImage size={90} />
186
+ </div>
187
+ )}
188
+ </div>
189
+ </div>
190
+ </LocalizedClientLink>
191
+ )
192
+ })}
193
+ </div>
194
+
195
+ {/* Desktop Grid Layout - 1024px and above */}
196
+ <div
197
+ className="hidden lg:grid gap-4 xl:gap-6"
198
+ style={{
199
+ gridTemplateColumns: 'repeat(4, 1fr)',
200
+ gridAutoRows: 'minmax(300px, 390px)',
201
+ }}
202
+ >
203
+ {finalDisplayCategories.map((category, index) => {
204
+ // Same 4-column logic for Desktop
205
+ const positionInPattern = index % 6
206
+ const rowGroup = Math.floor(index / 6)
207
+ const rowBase = (rowGroup * 2) + 1
208
+
209
+ let gridColumn = ""
210
+ let gridRow = ""
211
+
212
+ if (positionInPattern === 0) {
213
+ gridColumn = "1 / 3"
214
+ gridRow = `${rowBase}`
215
+ } else if (positionInPattern === 1) {
216
+ gridColumn = "3"
217
+ gridRow = `${rowBase}`
218
+ } else if (positionInPattern === 2) {
219
+ gridColumn = "4"
220
+ gridRow = `${rowBase}`
221
+ } else if (positionInPattern === 3) {
222
+ gridColumn = "1"
223
+ gridRow = `${rowBase + 1}`
224
+ } else if (positionInPattern === 4) {
225
+ gridColumn = "2"
226
+ gridRow = `${rowBase + 1}`
227
+ } else if (positionInPattern === 5) {
228
+ gridColumn = "3 / 5"
229
+ gridRow = `${rowBase + 1}`
230
+ }
231
+
232
+ // Skip if position not set (shouldn't happen, but safety check)
233
+ if (!gridColumn || !gridRow) {
234
+ return null
235
+ }
236
+
237
+ // Get metadata
238
+ const metadata = (category as any)?.metadata || {}
239
+ const categoryImage = metadata.category_image || null
240
+ const backgroundColor = metadata.category_background_color || "#F3F4F6"
241
+ const showShopNow = metadata.show_shop_now === "true" || metadata.show_shop_now === true
242
+
243
+ // Get parent category name as subtitle
244
+ const parentCategory = (category as any)?.parent_category
245
+ const subtitle = parentCategory?.name || ""
246
+
247
+ // Get subcategory name as main title (in uppercase)
248
+ const mainTitle = (category.name || "").toUpperCase()
249
+
250
+ // Build category URL - use store page with category filter
251
+ const categoryUrl = category.id
252
+ ? `/store?category=${category.id}`
253
+ : "#"
254
+
255
+ return (
256
+ <LocalizedClientLink
257
+ key={category.id}
258
+ href={categoryUrl}
259
+ data-ga-event="shop_by_category_click"
260
+ data-ga-label={category.name || "Category"}
261
+ data-ga-category-name={category.name || ""}
262
+ data-ga-category-handle={category.handle || ""}
263
+ data-ga-category-id={category.id || ""}
264
+ data-ga-parent-category={subtitle || ""}
265
+ className="relative rounded-lg overflow-hidden shadow-sm hover:shadow-md transition-shadow duration-200 flex flex-col group w-full"
266
+ style={{
267
+ backgroundColor: backgroundColor,
268
+ gridColumn: gridColumn,
269
+ gridRow: gridRow,
270
+ minHeight: "300px",
271
+ }}
272
+ >
273
+ <div className="flex flex-col h-full p-4 sm:p-5 lg:p-6 pb-0 sm:pb-0 lg:pb-0 pointer-events-none">
274
+ {/* Category Title */}
275
+ <div className="mb-3 lg:mb-4">
276
+ {subtitle && (
277
+ <p className="text-xs sm:text-sm text-gray-700 mb-1">{subtitle}</p>
278
+ )}
279
+ <h3 className="text-lg sm:text-xl lg:text-2xl font-bold text-gray-900">
280
+ {mainTitle}
281
+ </h3>
282
+ </div>
283
+
284
+ {/* Category Image */}
285
+ <div className="flex-1 flex items-end justify-center relative" style={{ minHeight: 0 }}>
286
+ {categoryImage ? (
287
+ <div className="w-full h-full flex items-end justify-center">
288
+ <Image
289
+ src={categoryImage}
290
+ alt={category.name || "Category"}
291
+ fill
292
+ sizes="(max-width: 768px) 50vw, (max-width: 1024px) 33vw, 25vw"
293
+ className="object-contain object-bottom"
294
+ />
295
+ </div>
296
+ ) : (
297
+ <div className="w-full h-full flex items-end justify-center">
298
+ <PlaceholderImage size={110} />
299
+ </div>
300
+ )}
301
+ </div>
302
+ </div>
303
+ </LocalizedClientLink>
304
+ )
305
+ })}
306
+ </div>
307
+
308
+ {/* Mobile Layout - Below 768px */}
309
+ <style dangerouslySetInnerHTML={{
310
+ __html: `
311
+ @media (min-width: 350px) and (max-width: 767px) {
312
+ .shop-category-mobile-container {
313
+ grid-template-columns: repeat(2, 1fr) !important;
314
+ }
315
+ }
316
+ @media (min-width: 350px) and (max-width: 450px) {
317
+ .shop-category-mobile-card {
318
+ aspect-ratio: 0.55 !important;
319
+ min-height: 210px !important;
320
+ height: 210px !important;
321
+ }
322
+ }
323
+ @media (min-width: 450px) and (max-width: 550px) {
324
+ .shop-category-mobile-card {
325
+ aspect-ratio: 0.75 !important;
326
+ min-height: 250px !important;
327
+ }
328
+ }
329
+ @media (min-width: 550px) and (max-width: 767px) {
330
+ .shop-category-mobile-card {
331
+ aspect-ratio: 0.80 !important;
332
+ min-height: 280px !important;
333
+ }
334
+ }
335
+ `
336
+ }} />
337
+ <div
338
+ className="grid grid-cols-1 md:hidden gap-3 sm:gap-4 shop-category-mobile-container"
339
+ >
340
+ {finalDisplayCategories.map((category) => {
341
+ const metadata = (category as any)?.metadata || {}
342
+ const categoryImage = metadata.category_image || null
343
+ const backgroundColor = metadata.category_background_color || "#F3F4F6"
344
+ const showShopNow = metadata.show_shop_now === "true" || metadata.show_shop_now === true
345
+
346
+ const parentCategory = (category as any)?.parent_category
347
+ const subtitle = parentCategory?.name || ""
348
+ const mainTitle = (category.name || "").toUpperCase()
349
+ const categoryUrl = category.id
350
+ ? `/store?category=${category.id}`
351
+ : "#"
352
+
353
+ return (
354
+ <LocalizedClientLink
355
+ key={category.id}
356
+ href={categoryUrl}
357
+ data-ga-event="shop_by_category_click"
358
+ data-ga-label={category.name || "Category"}
359
+ data-ga-category-name={category.name || ""}
360
+ data-ga-category-handle={category.handle || ""}
361
+ data-ga-category-id={category.id || ""}
362
+ data-ga-parent-category={subtitle || ""}
363
+ className="relative rounded-lg overflow-hidden shadow-sm hover:shadow-md transition-shadow duration-200 flex flex-col group w-full shop-category-mobile-card"
364
+ style={{
365
+ backgroundColor: backgroundColor,
366
+ width: '100%',
367
+ aspectRatio: '0.85', // Default aspect ratio
368
+ minHeight: '300px',
369
+ }}
370
+ >
371
+ <div className="flex flex-col h-full p-4 sm:p-5 pb-0 sm:pb-0 pointer-events-none">
372
+ <div className="mb-3">
373
+ {subtitle && (
374
+ <p className="text-xs sm:text-sm text-gray-700 mb-1">{subtitle}</p>
375
+ )}
376
+ <h3 className="text-lg sm:text-xl font-bold text-gray-900">
377
+ {mainTitle}
378
+ </h3>
379
+ </div>
380
+ <div className="flex-1 flex items-end justify-center relative" style={{ minHeight: 0 }}>
381
+ {categoryImage ? (
382
+ <div className="w-full h-full flex items-end justify-center">
383
+ <Image
384
+ src={categoryImage}
385
+ alt={category.name || "Category"}
386
+ fill
387
+ sizes="(max-width: 768px) 50vw, (max-width: 1024px) 33vw, 25vw"
388
+ className="object-contain object-bottom"
389
+ />
390
+ </div>
391
+ ) : (
392
+ <div className="w-full h-full flex items-end justify-center">
393
+ <PlaceholderImage size={120} />
394
+ </div>
395
+ )}
396
+ </div>
397
+ </div>
398
+ </LocalizedClientLink>
399
+ )
400
+ })}
401
+ </div>
402
+ </div>
403
+ </div>
404
+ </div>
405
+ )
406
+ }
407
+
408
+ export default ShopByCategory
409
+