@pradip1995/commerce-core 1.0.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 (82) hide show
  1. package/README.md +15 -0
  2. package/package.json +70 -0
  3. package/src/analytics/ga4-ecommerce.ts +96 -0
  4. package/src/config.ts +36 -0
  5. package/src/constants.tsx +84 -0
  6. package/src/context/modal-context.tsx +40 -0
  7. package/src/context/wishlist-context.tsx +96 -0
  8. package/src/data/cart/abandoned.ts +111 -0
  9. package/src/data/cart/buyNow.ts +184 -0
  10. package/src/data/cart/checkout.ts +487 -0
  11. package/src/data/cart/index.ts +7 -0
  12. package/src/data/cart/mutations.ts +189 -0
  13. package/src/data/cart/promotions.ts +121 -0
  14. package/src/data/cart/region.ts +66 -0
  15. package/src/data/cart/retrieve.ts +162 -0
  16. package/src/data/categories.ts +90 -0
  17. package/src/data/collections.ts +109 -0
  18. package/src/data/contact.ts +143 -0
  19. package/src/data/cookies.ts +170 -0
  20. package/src/data/customer-registration.ts +365 -0
  21. package/src/data/customer.ts +638 -0
  22. package/src/data/dynamic-config.ts +420 -0
  23. package/src/data/fulfillment.ts +95 -0
  24. package/src/data/guest.ts +357 -0
  25. package/src/data/locale-actions.ts +74 -0
  26. package/src/data/locales.ts +28 -0
  27. package/src/data/newsletter.ts +41 -0
  28. package/src/data/notifications.ts +22 -0
  29. package/src/data/onboarding.ts +9 -0
  30. package/src/data/orders.ts +500 -0
  31. package/src/data/payment-details.ts +68 -0
  32. package/src/data/payment.ts +32 -0
  33. package/src/data/products.ts +424 -0
  34. package/src/data/regions.ts +64 -0
  35. package/src/data/returns.ts +305 -0
  36. package/src/data/reviews.ts +279 -0
  37. package/src/data/swaps.ts +154 -0
  38. package/src/data/variants.ts +38 -0
  39. package/src/data/wishlist.ts +292 -0
  40. package/src/domain/cart/abandoned-carts.ts +49 -0
  41. package/src/domain/cart/buy-now.ts +15 -0
  42. package/src/domain/cart/checkout.ts +25 -0
  43. package/src/domain/cart/index.ts +8 -0
  44. package/src/domain/cart/metadata.ts +21 -0
  45. package/src/domain/cart/payment.ts +21 -0
  46. package/src/domain/cart/phone.ts +17 -0
  47. package/src/domain/cart/reorder.ts +19 -0
  48. package/src/domain/cart/validation.ts +43 -0
  49. package/src/domain/product/pricing.ts +49 -0
  50. package/src/domain/product/variant-selection.ts +193 -0
  51. package/src/firebase.ts +48 -0
  52. package/src/hooks/index.ts +8 -0
  53. package/src/hooks/use-add-to-cart.ts +63 -0
  54. package/src/hooks/use-cart.ts +132 -0
  55. package/src/hooks/use-checkout.ts +62 -0
  56. package/src/hooks/use-in-view.tsx +29 -0
  57. package/src/hooks/use-product-actions.ts +190 -0
  58. package/src/hooks/use-product-reviews.ts +18 -0
  59. package/src/hooks/use-product-variant.ts +142 -0
  60. package/src/hooks/use-server-action.ts +30 -0
  61. package/src/hooks/use-toggle-state.tsx +46 -0
  62. package/src/hooks/use-wishlist.ts +3 -0
  63. package/src/theme/inline-vars.ts +12 -0
  64. package/src/types/account.ts +21 -0
  65. package/src/types/cart.ts +13 -0
  66. package/src/types/home.ts +52 -0
  67. package/src/types/layout.ts +29 -0
  68. package/src/types/product-card.ts +17 -0
  69. package/src/util/compare-addresses.ts +28 -0
  70. package/src/util/env.ts +3 -0
  71. package/src/util/get-locale-header.ts +8 -0
  72. package/src/util/get-percentage-diff.ts +6 -0
  73. package/src/util/get-product-price.ts +78 -0
  74. package/src/util/google-oauth.ts +28 -0
  75. package/src/util/isEmpty.ts +11 -0
  76. package/src/util/medusa-error.ts +18 -0
  77. package/src/util/money.ts +26 -0
  78. package/src/util/order-status.tsx +179 -0
  79. package/src/util/product.ts +431 -0
  80. package/src/util/repeat.ts +5 -0
  81. package/src/util/returns.ts +71 -0
  82. package/src/util/sort-products.ts +48 -0
@@ -0,0 +1,431 @@
1
+ import { HttpTypes } from "@medusajs/types"
2
+
3
+ export const isSimpleProduct = (product: HttpTypes.StoreProduct): boolean => {
4
+ return product.options?.length === 1 && product.options[0].values?.length === 1
5
+ }
6
+
7
+ export function variantOptionsAsKeymap(
8
+ variantOptions: HttpTypes.StoreProductVariant["options"]
9
+ ): Record<string, string> {
10
+ return (
11
+ variantOptions?.reduce((acc: Record<string, string>, option) => {
12
+ if (option.option_id) {
13
+ acc[option.option_id] = option.value
14
+ }
15
+ return acc
16
+ }, {}) ?? {}
17
+ )
18
+ }
19
+
20
+ /**
21
+ * Resolves which variant should drive the product gallery.
22
+ * Uses the exact variant when all options match; otherwise the first variant
23
+ * that matches whatever options the shopper has already picked (e.g. color only).
24
+ */
25
+ export function resolveDisplayVariant(
26
+ product: HttpTypes.StoreProduct,
27
+ options: Record<string, string | undefined>
28
+ ): HttpTypes.StoreProductVariant | undefined {
29
+ const exactMatch = findExactVariant(product, options)
30
+
31
+ if (exactMatch) {
32
+ return exactMatch
33
+ }
34
+
35
+ const matchingVariants = resolveMatchingVariants(product, options)
36
+
37
+ return matchingVariants[0]
38
+ }
39
+
40
+ function findExactVariant(
41
+ product: HttpTypes.StoreProduct,
42
+ options: Record<string, string | undefined>
43
+ ): HttpTypes.StoreProductVariant | undefined {
44
+ if (!product.variants?.length) {
45
+ return undefined
46
+ }
47
+
48
+ return product.variants.find((variant) => {
49
+ const variantOptions = variantOptionsAsKeymap(variant.options)
50
+
51
+ return Object.entries(variantOptions).every(
52
+ ([optionId, value]) => options[optionId] === value
53
+ )
54
+ })
55
+ }
56
+
57
+ /** All variants matching the currently selected options (supports partial selection). */
58
+ export function resolveMatchingVariants(
59
+ product: HttpTypes.StoreProduct,
60
+ options: Record<string, string | undefined>
61
+ ): HttpTypes.StoreProductVariant[] {
62
+ const selectedEntries = Object.entries(options).filter(
63
+ (entry): entry is [string, string] => Boolean(entry[1])
64
+ )
65
+
66
+ if (!selectedEntries.length || !product.variants?.length) {
67
+ return []
68
+ }
69
+
70
+ return product.variants.filter((variant) => {
71
+ const variantOptions = variantOptionsAsKeymap(variant.options)
72
+
73
+ return selectedEntries.every(
74
+ ([optionId, value]) => variantOptions[optionId] === value
75
+ )
76
+ })
77
+ }
78
+
79
+ function dedupeImages(
80
+ images: HttpTypes.StoreProductImage[]
81
+ ): HttpTypes.StoreProductImage[] {
82
+ const seen: ImageMatchKeys = {
83
+ urlKeys: new Set<string>(),
84
+ idKeys: new Set<string>(),
85
+ }
86
+ const deduped: HttpTypes.StoreProductImage[] = []
87
+
88
+ images.forEach((image) => {
89
+ if (!image.url || imageMatchesKeys(image, seen)) {
90
+ return
91
+ }
92
+
93
+ deduped.push(image)
94
+ addImageToMatchKeys(image, seen)
95
+ })
96
+
97
+ return deduped
98
+ }
99
+
100
+ type ImageMatchKeys = {
101
+ urlKeys: Set<string>
102
+ idKeys: Set<string>
103
+ }
104
+
105
+ function addImageToMatchKeys(
106
+ image: Pick<HttpTypes.StoreProductImage, "url" | "id"> | undefined | null,
107
+ keys: ImageMatchKeys
108
+ ) {
109
+ if (!image?.url) {
110
+ return
111
+ }
112
+
113
+ const cleanUrl = image.url.split("?")[0].toLowerCase().trim()
114
+ const urlPath = cleanUrl.split("/").pop() || ""
115
+
116
+ keys.urlKeys.add(cleanUrl)
117
+ if (urlPath) {
118
+ keys.urlKeys.add(urlPath.toLowerCase())
119
+ }
120
+
121
+ if (image.id) {
122
+ keys.idKeys.add(image.id)
123
+ }
124
+ }
125
+
126
+ function imageMatchesKeys(
127
+ image: HttpTypes.StoreProductImage,
128
+ keys: ImageMatchKeys
129
+ ): boolean {
130
+ if (!image.url) {
131
+ return false
132
+ }
133
+
134
+ const cleanUrl = image.url.split("?")[0].toLowerCase().trim()
135
+ const urlPath = cleanUrl.split("/").pop() || ""
136
+
137
+ const urlMatches =
138
+ keys.urlKeys.has(cleanUrl) ||
139
+ (urlPath ? keys.urlKeys.has(urlPath.toLowerCase()) : false)
140
+ const idMatches = image.id ? keys.idKeys.has(image.id) : false
141
+
142
+ return urlMatches || idMatches
143
+ }
144
+
145
+ function getVariantAssignedImageKeys(
146
+ product: HttpTypes.StoreProduct,
147
+ excludeVariantId?: string
148
+ ): ImageMatchKeys {
149
+ const keys: ImageMatchKeys = {
150
+ urlKeys: new Set<string>(),
151
+ idKeys: new Set<string>(),
152
+ }
153
+
154
+ product.variants?.forEach((variant) => {
155
+ if (excludeVariantId && variant.id === excludeVariantId) {
156
+ return
157
+ }
158
+
159
+ variant.images?.forEach((image) => {
160
+ addImageToMatchKeys(image as HttpTypes.StoreProductImage, keys)
161
+ })
162
+ })
163
+
164
+ return keys
165
+ }
166
+
167
+ function getUnsupportedProductImages(
168
+ product: HttpTypes.StoreProduct,
169
+ selectedVariantId: string
170
+ ): HttpTypes.StoreProductImage[] {
171
+ const productImages = product.images || []
172
+ if (productImages.length === 0) {
173
+ return []
174
+ }
175
+
176
+ const assignedKeys = getVariantAssignedImageKeys(product, selectedVariantId)
177
+
178
+ return productImages.filter((image) => !imageMatchesKeys(image, assignedKeys))
179
+ }
180
+
181
+ function getKeysForImages(images: HttpTypes.StoreProductImage[]): ImageMatchKeys {
182
+ const keys: ImageMatchKeys = {
183
+ urlKeys: new Set<string>(),
184
+ idKeys: new Set<string>(),
185
+ }
186
+
187
+ images.forEach((image) => addImageToMatchKeys(image, keys))
188
+
189
+ return keys
190
+ }
191
+
192
+ function mergeVariantAndGeneralImages(
193
+ variantImages: HttpTypes.StoreProductImage[],
194
+ generalImages: HttpTypes.StoreProductImage[]
195
+ ): HttpTypes.StoreProductImage[] {
196
+ const variantKeys = getKeysForImages(variantImages)
197
+ const merged = [...variantImages]
198
+
199
+ generalImages.forEach((image) => {
200
+ if (!imageMatchesKeys(image, variantKeys)) {
201
+ merged.push(image)
202
+ }
203
+ })
204
+
205
+ return merged
206
+ }
207
+
208
+ /**
209
+ * Images assigned to this variant but not to any other variant.
210
+ * Medusa often includes shared product images on every variant; the admin-selected
211
+ * variant image is typically the extra one appended last for that variant.
212
+ */
213
+ function getVariantExclusiveImages(
214
+ product: HttpTypes.StoreProduct,
215
+ variant: HttpTypes.StoreProductVariant
216
+ ): HttpTypes.StoreProductImage[] {
217
+ const variantImages = (variant.images || []) as HttpTypes.StoreProductImage[]
218
+
219
+ if (!variantImages.length) {
220
+ return []
221
+ }
222
+
223
+ const otherVariantKeys = getVariantAssignedImageKeys(product, variant.id)
224
+
225
+ return variantImages.filter((image) => !imageMatchesKeys(image, otherVariantKeys))
226
+ }
227
+
228
+ function getPrimaryVariantImage(
229
+ product: HttpTypes.StoreProduct,
230
+ variant: HttpTypes.StoreProductVariant
231
+ ): HttpTypes.StoreProductImage | undefined {
232
+ const variantImages = (variant.images || []) as HttpTypes.StoreProductImage[]
233
+
234
+ if (!variantImages.length) {
235
+ return undefined
236
+ }
237
+
238
+ const exclusiveImages = getVariantExclusiveImages(product, variant)
239
+
240
+ if (exclusiveImages.length > 0) {
241
+ const exclusiveKeys = getKeysForImages(exclusiveImages)
242
+ let primaryIndex = -1
243
+
244
+ variantImages.forEach((image, index) => {
245
+ if (imageMatchesKeys(image, exclusiveKeys)) {
246
+ primaryIndex = index
247
+ }
248
+ })
249
+
250
+ if (primaryIndex >= 0) {
251
+ return variantImages[primaryIndex]
252
+ }
253
+
254
+ return exclusiveImages[exclusiveImages.length - 1]
255
+ }
256
+
257
+ return variantImages[0]
258
+ }
259
+
260
+ function reorderWithPrimaryFirst(
261
+ images: HttpTypes.StoreProductImage[],
262
+ primaryImage?: HttpTypes.StoreProductImage
263
+ ): HttpTypes.StoreProductImage[] {
264
+ if (!primaryImage) {
265
+ return images
266
+ }
267
+
268
+ const primaryKeys = getKeysForImages([primaryImage])
269
+ const primaryIndex = images.findIndex((image) => imageMatchesKeys(image, primaryKeys))
270
+
271
+ if (primaryIndex <= 0) {
272
+ return images
273
+ }
274
+
275
+ const reordered = [...images]
276
+ const [primary] = reordered.splice(primaryIndex, 1)
277
+
278
+ return [primary, ...reordered]
279
+ }
280
+
281
+ /**
282
+ * Index of the variant's primary assigned image inside the gallery list.
283
+ * Falls back to 0 when the variant has no dedicated media.
284
+ */
285
+ export function getDefaultImageIndexForVariant(
286
+ product: HttpTypes.StoreProduct,
287
+ selectedVariantId: string | undefined,
288
+ images: HttpTypes.StoreProductImage[]
289
+ ): number {
290
+ if (!images.length) {
291
+ return 0
292
+ }
293
+
294
+ if (!selectedVariantId || !product.variants) {
295
+ return 0
296
+ }
297
+
298
+ const variant = product.variants.find((v) => v.id === selectedVariantId)
299
+ const primaryVariantImage = variant
300
+ ? getPrimaryVariantImage(product, variant)
301
+ : undefined
302
+
303
+ if (!primaryVariantImage) {
304
+ return 0
305
+ }
306
+
307
+ const primaryKeys = getKeysForImages([primaryVariantImage])
308
+ const index = images.findIndex((image) => imageMatchesKeys(image, primaryKeys))
309
+
310
+ return index >= 0 ? index : 0
311
+ }
312
+
313
+ /**
314
+ * Consistently determines which images to show for a product or a specific variant.
315
+ * Used on both server and client to prevent flickering during hydration.
316
+ */
317
+ export function getImagesForVariant(
318
+ product: HttpTypes.StoreProduct,
319
+ selectedVariantId?: string
320
+ ): HttpTypes.StoreProductImage[] {
321
+ // 1. If no variant is selected, show general product images
322
+ if (!selectedVariantId || !product.variants) {
323
+ return product.images || []
324
+ }
325
+
326
+ // 2. Find the selected variant
327
+ const variant = product.variants.find((v) => v.id === selectedVariantId)
328
+
329
+ // 3. Variant has specific images — primary assigned image first, then the rest
330
+ if (variant?.images && variant.images.length > 0) {
331
+ const variantImages = [...variant.images] as HttpTypes.StoreProductImage[]
332
+ const generalImages = getUnsupportedProductImages(product, selectedVariantId)
333
+ const primaryImage = getPrimaryVariantImage(product, variant)
334
+ const merged = mergeVariantAndGeneralImages(variantImages, generalImages)
335
+
336
+ return reorderWithPrimaryFirst(merged, primaryImage)
337
+ }
338
+
339
+ // 4. No variant images: show product images not assigned to other variants
340
+ const unsupportedImages = getUnsupportedProductImages(product, selectedVariantId)
341
+ if (unsupportedImages.length > 0) {
342
+ return unsupportedImages
343
+ }
344
+
345
+ if (product.thumbnail) {
346
+ return [
347
+ {
348
+ id: "default-thumb",
349
+ url: product.thumbnail,
350
+ } as HttpTypes.StoreProductImage,
351
+ ]
352
+ }
353
+
354
+ return []
355
+ }
356
+
357
+ /**
358
+ * Gallery images for the current option selection.
359
+ * - No selection → all product images
360
+ * - Exact variant (color + size, etc.) → that variant's gallery
361
+ * - Partial selection (e.g. color only) → unique images from all matching variants
362
+ */
363
+ export function getImagesForSelection(
364
+ product: HttpTypes.StoreProduct,
365
+ options: Record<string, string | undefined>
366
+ ): HttpTypes.StoreProductImage[] {
367
+ const hasSelection = Object.values(options).some(Boolean)
368
+
369
+ if (!hasSelection) {
370
+ return product.images || []
371
+ }
372
+
373
+ const exactVariant = findExactVariant(product, options)
374
+
375
+ if (exactVariant) {
376
+ return getImagesForVariant(product, exactVariant.id)
377
+ }
378
+
379
+ const matchingVariants = resolveMatchingVariants(product, options)
380
+
381
+ if (!matchingVariants.length) {
382
+ return product.images || []
383
+ }
384
+
385
+ const aggregated: HttpTypes.StoreProductImage[] = []
386
+ const exclusivePrimaries: HttpTypes.StoreProductImage[] = []
387
+
388
+ matchingVariants.forEach((variant) => {
389
+ aggregated.push(...getImagesForVariant(product, variant.id))
390
+
391
+ const exclusiveImages = getVariantExclusiveImages(product, variant)
392
+
393
+ if (exclusiveImages.length > 0) {
394
+ const primary = getPrimaryVariantImage(product, variant)
395
+
396
+ if (primary) {
397
+ exclusivePrimaries.push(primary)
398
+ }
399
+ }
400
+ })
401
+
402
+ let images = dedupeImages(aggregated)
403
+ const uniquePrimaries = dedupeImages(exclusivePrimaries)
404
+
405
+ uniquePrimaries.forEach((primary) => {
406
+ images = reorderWithPrimaryFirst(images, primary)
407
+ })
408
+
409
+ return images
410
+ }
411
+
412
+ /**
413
+ * Default main-image index for the current option selection.
414
+ */
415
+ export function getDefaultImageIndexForSelection(
416
+ product: HttpTypes.StoreProduct,
417
+ options: Record<string, string | undefined>,
418
+ images: HttpTypes.StoreProductImage[]
419
+ ): number {
420
+ if (!images.length) {
421
+ return 0
422
+ }
423
+
424
+ const exactVariant = findExactVariant(product, options)
425
+
426
+ if (exactVariant) {
427
+ return getDefaultImageIndexForVariant(product, exactVariant.id, images)
428
+ }
429
+
430
+ return 0
431
+ }
@@ -0,0 +1,5 @@
1
+ const repeat = (times: number) => {
2
+ return Array.from(Array(times).keys())
3
+ }
4
+
5
+ export default repeat
@@ -0,0 +1,71 @@
1
+ import { HttpTypes } from "@medusajs/types"
2
+
3
+ export type ItemWithDeliveryStatus = HttpTypes.StoreOrderLineItem & {
4
+ returnable_quantity: number
5
+ delivered_quantity: number
6
+ return_requested_quantity: number
7
+ return_received_quantity: number
8
+ written_off_quantity: number
9
+ }
10
+
11
+ export const calculateReturnableQuantity = (
12
+ item: HttpTypes.StoreOrderLineItem
13
+ ): number => {
14
+ // In a real implementation, these would come from the item.detail or similar
15
+ // For now, we'll assume we can calculate it from available data or default to quantity
16
+ // Adjust this based on your actual data structure for item details
17
+
18
+ // Note: The actual data structure depends on how Medusa returns order item details
19
+ // Typically it's in item.detail associated with fulfillments/returns
20
+
21
+ // For the purpose of this storefront implementation (assuming standard Medusa):
22
+ // We strictly follow the logic: delivered - requested - received - written_off
23
+
24
+ // Checking if properties exist on item (they might be on a 'detail' object or top level depending on version)
25
+ const anyItem = item as any
26
+
27
+ // Defaulting to item.quantity if details missing (safe fallback for initial dev)
28
+ // BUT per requirements, we must implement the logic.
29
+
30
+ // If the backend provides these fields on the item directly:
31
+ const delivered = anyItem.delivered_quantity ?? anyItem.quantity ?? 0 // Fallback to quantity if assumed delivered
32
+ const requested = anyItem.return_requested_quantity ?? 0
33
+ const received = anyItem.return_received_quantity ?? 0
34
+ const writtenOff = anyItem.written_off_quantity ?? 0
35
+
36
+ // If fulfillment_status is not fulfilled/shipped/partially_shipped, returnable might be 0
37
+ // But strict formula:
38
+ const returnable = Math.max(0, delivered - requested - received - writtenOff)
39
+
40
+ return returnable
41
+ }
42
+
43
+ export const isItemReturnable = (item: HttpTypes.StoreOrderLineItem): boolean => {
44
+ return calculateReturnableQuantity(item) > 0
45
+ }
46
+
47
+ export const hasReturnableItems = (order: HttpTypes.StoreOrder): boolean => {
48
+ if (!order || !order.items) return false
49
+
50
+ // Simple check: if order is canceled, no returns
51
+ if (order.status === "canceled") return false
52
+
53
+ return order.items.some((item) => isItemReturnable(item))
54
+ }
55
+
56
+ export const enhanceItemsWithReturnStatus = (
57
+ items: HttpTypes.StoreOrderLineItem[]
58
+ ): ItemWithDeliveryStatus[] => {
59
+ return items.map((item) => {
60
+ const anyItem = item as any
61
+ return {
62
+ ...item,
63
+ // Ensure these properties exist
64
+ delivered_quantity: anyItem.delivered_quantity ?? item.quantity, // Optimistic default
65
+ return_requested_quantity: anyItem.return_requested_quantity ?? 0,
66
+ return_received_quantity: anyItem.return_received_quantity ?? 0,
67
+ written_off_quantity: anyItem.written_off_quantity ?? 0,
68
+ returnable_quantity: calculateReturnableQuantity(item),
69
+ }
70
+ })
71
+ }
@@ -0,0 +1,48 @@
1
+ import { HttpTypes } from "@medusajs/types"
2
+ import { SortOptions } from "@modules/store/components/refinement-list/sort-products"
3
+
4
+ interface MinPricedProduct extends HttpTypes.StoreProduct {
5
+ _minPrice?: number
6
+ }
7
+
8
+ /**
9
+ * Helper function to sort products by price until the store API supports sorting by price
10
+ * @param products
11
+ * @param sortBy
12
+ * @returns products sorted by price
13
+ */
14
+ export function sortProducts(
15
+ products: HttpTypes.StoreProduct[],
16
+ sortBy: SortOptions
17
+ ): HttpTypes.StoreProduct[] {
18
+ let sortedProducts = products as MinPricedProduct[]
19
+
20
+ if (["price_asc", "price_desc"].includes(sortBy)) {
21
+ // Precompute the minimum price for each product
22
+ sortedProducts.forEach((product) => {
23
+ if (product.variants && product.variants.length > 0) {
24
+ product._minPrice = Math.min(
25
+ ...product.variants.map(
26
+ (variant) => variant?.calculated_price?.calculated_amount || 0
27
+ )
28
+ )
29
+ } else {
30
+ product._minPrice = Infinity
31
+ }
32
+ })
33
+
34
+ // Sort products based on the precomputed minimum prices
35
+ sortedProducts.sort((a, b) => {
36
+ const diff = a._minPrice! - b._minPrice!
37
+ return sortBy === "price_asc" ? diff : -diff
38
+ })
39
+ }
40
+
41
+ if (sortBy === "created_at") {
42
+ sortedProducts.sort((a, b) => {
43
+ return new Date(b.created_at!).getTime() - new Date(a.created_at!).getTime()
44
+ })
45
+ }
46
+
47
+ return sortedProducts
48
+ }