medusa-ui-common 2.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.
- package/package.json +93 -0
- package/src/common/components/breadcrumb/index.tsx +43 -0
- package/src/common/components/cart-totals/index.tsx +562 -0
- package/src/common/components/checkbox/index.tsx +98 -0
- package/src/common/components/delete-button/index.tsx +158 -0
- package/src/common/components/discount-code/index.tsx +220 -0
- package/src/common/components/divider/index.tsx +9 -0
- package/src/common/components/error-message/index.tsx +13 -0
- package/src/common/components/filter-checkbox-group/index.tsx +134 -0
- package/src/common/components/filter-radio-group/index.tsx +62 -0
- package/src/common/components/input/index.tsx +79 -0
- package/src/common/components/interactive-link/index.tsx +33 -0
- package/src/common/components/line-item-options/index.tsx +26 -0
- package/src/common/components/line-item-price/index.tsx +64 -0
- package/src/common/components/line-item-unit-price/index.tsx +64 -0
- package/src/common/components/localized-client-link/index.tsx +32 -0
- package/src/common/components/login-popup/index.tsx +78 -0
- package/src/common/components/modal/index.tsx +123 -0
- package/src/common/components/native-select/index.tsx +75 -0
- package/src/common/components/obfuscated-email/index.tsx +30 -0
- package/src/common/components/processing-overlay/index.tsx +83 -0
- package/src/common/components/radio/index.tsx +27 -0
- package/src/common/components/side-panel/index.tsx +65 -0
- package/src/common/components/submit-button/index.tsx +32 -0
- package/src/common/icons/arrow-left.tsx +36 -0
- package/src/common/icons/back.tsx +37 -0
- package/src/common/icons/bancontact.tsx +26 -0
- package/src/common/icons/chevron-down.tsx +30 -0
- package/src/common/icons/delivered.tsx +29 -0
- package/src/common/icons/envelope.tsx +27 -0
- package/src/common/icons/eye-off.tsx +37 -0
- package/src/common/icons/eye.tsx +37 -0
- package/src/common/icons/fast-delivery.tsx +65 -0
- package/src/common/icons/ideal.tsx +26 -0
- package/src/common/icons/lock.tsx +31 -0
- package/src/common/icons/map-pin.tsx +37 -0
- package/src/common/icons/medusa.tsx +27 -0
- package/src/common/icons/menu.tsx +45 -0
- package/src/common/icons/nextjs.tsx +27 -0
- package/src/common/icons/package.tsx +44 -0
- package/src/common/icons/paypal.tsx +30 -0
- package/src/common/icons/phone.tsx +30 -0
- package/src/common/icons/placeholder-image.tsx +44 -0
- package/src/common/icons/refresh.tsx +51 -0
- package/src/common/icons/spinner.tsx +37 -0
- package/src/common/icons/trash.tsx +51 -0
- package/src/common/icons/user.tsx +37 -0
- package/src/common/icons/x.tsx +37 -0
- package/src/constants/payments.tsx +31 -0
- package/src/context/modal-context.tsx +37 -0
- package/src/context/wishlist-context.tsx +83 -0
- package/src/index.ts +16 -0
- package/src/skeletons/components/skeleton-button/index.tsx +5 -0
- package/src/skeletons/components/skeleton-card-details/index.tsx +10 -0
- package/src/skeletons/components/skeleton-cart-item/index.tsx +35 -0
- package/src/skeletons/components/skeleton-cart-totals/index.tsx +30 -0
- package/src/skeletons/components/skeleton-code-form/index.tsx +13 -0
- package/src/skeletons/components/skeleton-line-item/index.tsx +35 -0
- package/src/skeletons/components/skeleton-order-confirmed-header/index.tsx +14 -0
- package/src/skeletons/components/skeleton-order-information/index.tsx +36 -0
- package/src/skeletons/components/skeleton-order-items/index.tsx +43 -0
- package/src/skeletons/components/skeleton-order-summary/index.tsx +15 -0
- package/src/skeletons/components/skeleton-product-preview/index.tsx +15 -0
- package/src/skeletons/templates/skeleton-cart-page/index.tsx +65 -0
- package/src/skeletons/templates/skeleton-order-confirmed/index.tsx +21 -0
- package/src/skeletons/templates/skeleton-product-grid/index.tsx +23 -0
- package/src/skeletons/templates/skeleton-related-products/index.tsx +25 -0
- package/src/types/global.ts +24 -0
- package/src/types/icon.ts +6 -0
- package/src/util/checkout-dom.ts +65 -0
- package/src/util/compare-addresses.ts +28 -0
- package/src/util/env.ts +3 -0
- package/src/util/get-percentage-diff.ts +6 -0
- package/src/util/get-product-price.ts +79 -0
- package/src/util/isEmpty.ts +11 -0
- package/src/util/money.ts +26 -0
- package/src/util/product.ts +86 -0
- package/src/util/repeat.ts +5 -0
- package/src/util/returns.ts +72 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const SkeletonOrderConfirmedHeader = () => {
|
|
2
|
+
return (
|
|
3
|
+
<div className="flex flex-col gap-y-2 pb-10 animate-pulse">
|
|
4
|
+
<div className="w-2/5 h-4 bg-gray-100"></div>
|
|
5
|
+
<div className="w-3/6 h-6 bg-gray-100"></div>
|
|
6
|
+
<div className="flex gap-x-4">
|
|
7
|
+
<div className="w-16 h-4 bg-gray-100"></div>
|
|
8
|
+
<div className="w-12 h-4 bg-gray-100"></div>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default SkeletonOrderConfirmedHeader
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import SkeletonCartTotals from "medusa-ui-common/skeletons/components/skeleton-cart-totals"
|
|
2
|
+
|
|
3
|
+
const SkeletonOrderInformation = () => {
|
|
4
|
+
return (
|
|
5
|
+
<div>
|
|
6
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 py-10 border-b border-gray-200">
|
|
7
|
+
<div className="flex flex-col">
|
|
8
|
+
<div className="w-32 h-4 bg-gray-100 mb-4"></div>
|
|
9
|
+
<div className="w-2/6 h-3 bg-gray-100"></div>
|
|
10
|
+
<div className="w-3/6 h-3 bg-gray-100 my-2"></div>
|
|
11
|
+
<div className="w-1/6 h-3 bg-gray-100"></div>
|
|
12
|
+
</div>
|
|
13
|
+
<div className="flex flex-col">
|
|
14
|
+
<div className="w-32 h-4 bg-gray-100 mb-4"></div>
|
|
15
|
+
<div className="w-2/6 h-3 bg-gray-100"></div>
|
|
16
|
+
<div className="w-3/6 h-3 bg-gray-100 my-2"></div>
|
|
17
|
+
<div className="w-2/6 h-3 bg-gray-100"></div>
|
|
18
|
+
<div className="w-1/6 h-3 bg-gray-100 mt-2"></div>
|
|
19
|
+
<div className="w-32 h-4 bg-gray-100 my-4"></div>
|
|
20
|
+
<div className="w-1/6 h-3 bg-gray-100"></div>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 py-10">
|
|
24
|
+
<div className="flex flex-col">
|
|
25
|
+
<div className="w-32 h-4 bg-gray-100 mb-4"></div>
|
|
26
|
+
<div className="w-2/6 h-3 bg-gray-100"></div>
|
|
27
|
+
<div className="w-3/6 h-3 bg-gray-100 my-4"></div>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<SkeletonCartTotals />
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default SkeletonOrderInformation
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const SkeletonOrderItems = () => {
|
|
2
|
+
return (
|
|
3
|
+
<div className="flex flex-col gap-y-4 py-10 border-y border-gray-200">
|
|
4
|
+
<div className="grid grid-cols-[122px_1fr] gap-x-4">
|
|
5
|
+
<div className="w-full aspect-[29/34] bg-gray-100"></div>
|
|
6
|
+
<div className="flex items-start justify-between">
|
|
7
|
+
<div className="flex flex-col gap-y-2">
|
|
8
|
+
<div className="w-48 h-6 bg-gray-100"></div>
|
|
9
|
+
<div className="w-24 h-4 bg-gray-100"></div>
|
|
10
|
+
<div className="w-32 h-4 bg-gray-100"></div>
|
|
11
|
+
</div>
|
|
12
|
+
<div className="w-32 h-6 bg-gray-100"></div>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<div className="grid grid-cols-[122px_1fr] gap-x-4">
|
|
17
|
+
<div className="w-full aspect-[29/34] bg-gray-100"></div>
|
|
18
|
+
<div className="flex items-start justify-between">
|
|
19
|
+
<div className="flex flex-col gap-y-2">
|
|
20
|
+
<div className="w-48 h-6 bg-gray-100"></div>
|
|
21
|
+
<div className="w-24 h-4 bg-gray-100"></div>
|
|
22
|
+
<div className="w-32 h-4 bg-gray-100"></div>
|
|
23
|
+
</div>
|
|
24
|
+
<div className="w-32 h-6 bg-gray-100"></div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div className="grid grid-cols-[122px_1fr] gap-x-4">
|
|
29
|
+
<div className="w-full aspect-[29/34] bg-gray-100"></div>
|
|
30
|
+
<div className="flex items-start justify-between">
|
|
31
|
+
<div className="flex flex-col gap-y-2">
|
|
32
|
+
<div className="w-48 h-6 bg-gray-100"></div>
|
|
33
|
+
<div className="w-24 h-4 bg-gray-100"></div>
|
|
34
|
+
<div className="w-32 h-4 bg-gray-100"></div>
|
|
35
|
+
</div>
|
|
36
|
+
<div className="w-32 h-6 bg-gray-100"></div>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default SkeletonOrderItems
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import SkeletonButton from "medusa-ui-common/skeletons/components/skeleton-button"
|
|
2
|
+
import SkeletonCartTotals from "medusa-ui-common/skeletons/components/skeleton-cart-totals"
|
|
3
|
+
|
|
4
|
+
const SkeletonOrderSummary = () => {
|
|
5
|
+
return (
|
|
6
|
+
<div className="grid-cols-1">
|
|
7
|
+
<SkeletonCartTotals header={false} />
|
|
8
|
+
<div className="mt-4">
|
|
9
|
+
<SkeletonButton />
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default SkeletonOrderSummary
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Container } from "@medusajs/ui"
|
|
2
|
+
|
|
3
|
+
const SkeletonProductPreview = () => {
|
|
4
|
+
return (
|
|
5
|
+
<div className="animate-pulse">
|
|
6
|
+
<Container className="aspect-[9/16] w-full bg-gray-100 bg-ui-bg-subtle" />
|
|
7
|
+
<div className="flex justify-between text-base-regular mt-2">
|
|
8
|
+
<div className="w-2/5 h-6 bg-gray-100"></div>
|
|
9
|
+
<div className="w-1/5 h-6 bg-gray-100"></div>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default SkeletonProductPreview
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Table } from "@medusajs/ui"
|
|
2
|
+
|
|
3
|
+
import repeat from "medusa-ui-common/util/repeat"
|
|
4
|
+
import SkeletonCartItem from "medusa-ui-common/skeletons/components/skeleton-cart-item"
|
|
5
|
+
import SkeletonCodeForm from "medusa-ui-common/skeletons/components/skeleton-code-form"
|
|
6
|
+
import SkeletonOrderSummary from "medusa-ui-common/skeletons/components/skeleton-order-summary"
|
|
7
|
+
|
|
8
|
+
const SkeletonCartPage = () => {
|
|
9
|
+
return (
|
|
10
|
+
<div className="py-12">
|
|
11
|
+
<div className="content-container">
|
|
12
|
+
<div className="grid grid-cols-1 small:grid-cols-[1fr_360px] gap-x-40">
|
|
13
|
+
<div className="flex flex-col bg-white p-6 gap-y-6">
|
|
14
|
+
<div className="bg-white flex items-start justify-between">
|
|
15
|
+
<div className="flex flex-col gap-y-2">
|
|
16
|
+
<div className="w-60 h-8 bg-gray-200 animate-pulse" />
|
|
17
|
+
<div className="w-48 h-6 bg-gray-200 animate-pulse" />
|
|
18
|
+
</div>
|
|
19
|
+
<div>
|
|
20
|
+
<div className="w-14 h-8 bg-gray-200 animate-pulse" />
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
<div>
|
|
24
|
+
<div className="pb-3 flex items-center">
|
|
25
|
+
<div className="w-20 h-12 bg-gray-200 animate-pulse" />
|
|
26
|
+
</div>
|
|
27
|
+
<Table>
|
|
28
|
+
<Table.Header className="border-t-0">
|
|
29
|
+
<Table.Row>
|
|
30
|
+
<Table.HeaderCell className="!pl-0">
|
|
31
|
+
<div className="w-10 h-6 bg-gray-200 animate-pulse" />
|
|
32
|
+
</Table.HeaderCell>
|
|
33
|
+
<Table.HeaderCell></Table.HeaderCell>
|
|
34
|
+
<Table.HeaderCell>
|
|
35
|
+
<div className="w-16 h-6 bg-gray-200 animate-pulse" />
|
|
36
|
+
</Table.HeaderCell>
|
|
37
|
+
<Table.HeaderCell>
|
|
38
|
+
<div className="w-12 h-6 bg-gray-200 animate-pulse" />
|
|
39
|
+
</Table.HeaderCell>
|
|
40
|
+
<Table.HeaderCell className="!pr-0">
|
|
41
|
+
<div className="flex justify-end">
|
|
42
|
+
<div className="w-12 h-6 bg-gray-200 animate-pulse" />
|
|
43
|
+
</div>
|
|
44
|
+
</Table.HeaderCell>
|
|
45
|
+
</Table.Row>
|
|
46
|
+
</Table.Header>
|
|
47
|
+
<Table.Body>
|
|
48
|
+
{repeat(4).map((index) => (
|
|
49
|
+
<SkeletonCartItem key={index} />
|
|
50
|
+
))}
|
|
51
|
+
</Table.Body>
|
|
52
|
+
</Table>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
<div className="flex flex-col gap-y-8">
|
|
56
|
+
<SkeletonOrderSummary />
|
|
57
|
+
<SkeletonCodeForm />
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export default SkeletonCartPage
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import SkeletonOrderConfirmedHeader from "medusa-ui-common/skeletons/components/skeleton-order-confirmed-header"
|
|
2
|
+
import SkeletonOrderInformation from "medusa-ui-common/skeletons/components/skeleton-order-information"
|
|
3
|
+
import SkeletonOrderItems from "medusa-ui-common/skeletons/components/skeleton-order-items"
|
|
4
|
+
|
|
5
|
+
const SkeletonOrderConfirmed = () => {
|
|
6
|
+
return (
|
|
7
|
+
<div className="bg-gray-50 py-6 min-h-[calc(100vh-64px)] animate-pulse">
|
|
8
|
+
<div className="content-container flex justify-center">
|
|
9
|
+
<div className="max-w-4xl h-full bg-white w-full p-10">
|
|
10
|
+
<SkeletonOrderConfirmedHeader />
|
|
11
|
+
|
|
12
|
+
<SkeletonOrderItems />
|
|
13
|
+
|
|
14
|
+
<SkeletonOrderInformation />
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default SkeletonOrderConfirmed
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import repeat from "medusa-ui-common/util/repeat"
|
|
2
|
+
import SkeletonProductPreview from "medusa-ui-common/skeletons/components/skeleton-product-preview"
|
|
3
|
+
|
|
4
|
+
const SkeletonProductGrid = ({
|
|
5
|
+
numberOfProducts = 8,
|
|
6
|
+
}: {
|
|
7
|
+
numberOfProducts?: number
|
|
8
|
+
}) => {
|
|
9
|
+
return (
|
|
10
|
+
<ul
|
|
11
|
+
className="grid grid-cols-2 small:grid-cols-3 medium:grid-cols-4 gap-x-6 gap-y-8 flex-1"
|
|
12
|
+
data-testid="products-list-loader"
|
|
13
|
+
>
|
|
14
|
+
{repeat(numberOfProducts).map((index) => (
|
|
15
|
+
<li key={index}>
|
|
16
|
+
<SkeletonProductPreview />
|
|
17
|
+
</li>
|
|
18
|
+
))}
|
|
19
|
+
</ul>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default SkeletonProductGrid
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import repeat from "medusa-ui-common/util/repeat"
|
|
2
|
+
import SkeletonProductPreview from "medusa-ui-common/skeletons/components/skeleton-product-preview"
|
|
3
|
+
|
|
4
|
+
const SkeletonRelatedProducts = () => {
|
|
5
|
+
return (
|
|
6
|
+
<div className="product-page-constraint">
|
|
7
|
+
<div className="flex flex-col gap-8 items-center text-center mb-8">
|
|
8
|
+
<div className="w-20 h-6 animate-pulse bg-gray-100"></div>
|
|
9
|
+
<div className="flex flex-col gap-4 items-center text-center mb-16">
|
|
10
|
+
<div className="w-96 h-10 animate-pulse bg-gray-100"></div>
|
|
11
|
+
<div className="w-48 h-10 animate-pulse bg-gray-100"></div>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
<ul className="grid grid-cols-2 small:grid-cols-3 medium:grid-cols-4 gap-x-6 gap-y-8 flex-1">
|
|
15
|
+
{repeat(3).map((index) => (
|
|
16
|
+
<li key={index}>
|
|
17
|
+
<SkeletonProductPreview />
|
|
18
|
+
</li>
|
|
19
|
+
))}
|
|
20
|
+
</ul>
|
|
21
|
+
</div>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default SkeletonRelatedProducts
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { StorePrice } from "@medusajs/types"
|
|
2
|
+
|
|
3
|
+
export type FeaturedProduct = {
|
|
4
|
+
id: string
|
|
5
|
+
title: string
|
|
6
|
+
handle: string
|
|
7
|
+
thumbnail?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type VariantPrice = {
|
|
11
|
+
calculated_price_number: number
|
|
12
|
+
calculated_price: string
|
|
13
|
+
original_price_number: number
|
|
14
|
+
original_price: string
|
|
15
|
+
currency_code: string
|
|
16
|
+
price_type: string
|
|
17
|
+
percentage_diff: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type StoreFreeShippingPrice = StorePrice & {
|
|
21
|
+
target_reached: boolean
|
|
22
|
+
target_remaining: number
|
|
23
|
+
remaining_percentage: number
|
|
24
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
2
|
+
|
|
3
|
+
export function normalizeCheckoutEmail(email?: string | null): string {
|
|
4
|
+
return (email ?? "").trim().toLowerCase()
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function isValidCheckoutEmail(email?: string | null): boolean {
|
|
8
|
+
const normalized = normalizeCheckoutEmail(email)
|
|
9
|
+
return normalized.length > 0 && EMAIL_PATTERN.test(normalized)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function readCheckoutAddressFromDom(fallback?: {
|
|
13
|
+
email?: string | null
|
|
14
|
+
shipping_address?: Record<string, unknown> | null
|
|
15
|
+
}) {
|
|
16
|
+
const fullNameInput = document.getElementById("shipping_full_name_field") as HTMLInputElement | null
|
|
17
|
+
const address1Input = document.getElementById("shipping_address_1_field") as HTMLInputElement | null
|
|
18
|
+
const companyInput = document.getElementById("shipping_company_field") as HTMLInputElement | null
|
|
19
|
+
const cityInput = document.getElementById("shipping_city_field") as HTMLInputElement | null
|
|
20
|
+
const provinceInput = document.getElementById("shipping_province_field") as HTMLInputElement | null
|
|
21
|
+
const postalInput = document.getElementById("shipping_postal_code_field") as HTMLInputElement | null
|
|
22
|
+
const phoneInput = document.getElementById("shipping_phone_field_internal") as HTMLInputElement | null
|
|
23
|
+
const emailInput = document.getElementById("shipping_email_field") as HTMLInputElement | null
|
|
24
|
+
|
|
25
|
+
const fullName = fullNameInput?.value?.trim() || ""
|
|
26
|
+
const [firstName, ...lastNameParts] = fullName.split(/\s+/).filter(Boolean)
|
|
27
|
+
const lastName = lastNameParts.join(" ") || "."
|
|
28
|
+
|
|
29
|
+
const shipping = fallback?.shipping_address ?? {}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
email: normalizeCheckoutEmail(emailInput?.value || (fallback?.email as string) || ""),
|
|
33
|
+
shipping_address: {
|
|
34
|
+
first_name: firstName || (shipping.first_name as string) || "Visitor",
|
|
35
|
+
last_name: lastName || (shipping.last_name as string) || "Customer",
|
|
36
|
+
address_1: address1Input?.value?.trim() || (shipping.address_1 as string) || "",
|
|
37
|
+
company: companyInput?.value?.trim() || (shipping.company as string) || "",
|
|
38
|
+
city: cityInput?.value?.trim() || (shipping.city as string) || "",
|
|
39
|
+
province: provinceInput?.value?.trim() || (shipping.province as string) || "",
|
|
40
|
+
postal_code: postalInput?.value?.trim() || (shipping.postal_code as string) || "",
|
|
41
|
+
phone: phoneInput?.value?.trim() || (shipping.phone as string) || "",
|
|
42
|
+
country_code: (shipping.country_code as string) || "in",
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function syncCheckoutAddressFromDom(fallback?: {
|
|
48
|
+
email?: string | null
|
|
49
|
+
shipping_address?: Record<string, unknown> | null
|
|
50
|
+
}) {
|
|
51
|
+
const payload = readCheckoutAddressFromDom(fallback)
|
|
52
|
+
|
|
53
|
+
if (!isValidCheckoutEmail(payload.email)) {
|
|
54
|
+
throw new Error("Please enter a valid email address before continuing.")
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const { updateAddressSilently } = await import("medusa-storefront-data/cart")
|
|
58
|
+
const result = await updateAddressSilently(payload)
|
|
59
|
+
|
|
60
|
+
if (result && "success" in result && result.success === false) {
|
|
61
|
+
throw new Error("Could not save your delivery details. Please check your email and try again.")
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return payload
|
|
65
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { isEqual, pick } from "lodash"
|
|
2
|
+
|
|
3
|
+
export default function compareAddresses(address1: any, address2: any) {
|
|
4
|
+
return isEqual(
|
|
5
|
+
pick(address1, [
|
|
6
|
+
"first_name",
|
|
7
|
+
"last_name",
|
|
8
|
+
"address_1",
|
|
9
|
+
"company",
|
|
10
|
+
"postal_code",
|
|
11
|
+
"city",
|
|
12
|
+
"country_code",
|
|
13
|
+
"province",
|
|
14
|
+
"phone",
|
|
15
|
+
]),
|
|
16
|
+
pick(address2, [
|
|
17
|
+
"first_name",
|
|
18
|
+
"last_name",
|
|
19
|
+
"address_1",
|
|
20
|
+
"company",
|
|
21
|
+
"postal_code",
|
|
22
|
+
"city",
|
|
23
|
+
"country_code",
|
|
24
|
+
"province",
|
|
25
|
+
"phone",
|
|
26
|
+
])
|
|
27
|
+
)
|
|
28
|
+
}
|
package/src/util/env.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { HttpTypes } from "@medusajs/types"
|
|
2
|
+
import { getPercentageDiff } from "./get-percentage-diff"
|
|
3
|
+
import { convertToLocale } from "./money"
|
|
4
|
+
|
|
5
|
+
export const getPricesForVariant = (variant: any) => {
|
|
6
|
+
if (!variant?.calculated_price?.calculated_amount) {
|
|
7
|
+
return null
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
calculated_price_number: variant.calculated_price.calculated_amount,
|
|
12
|
+
calculated_price: convertToLocale({
|
|
13
|
+
amount: variant.calculated_price.calculated_amount,
|
|
14
|
+
currency_code: variant.calculated_price.currency_code,
|
|
15
|
+
}),
|
|
16
|
+
original_price_number: variant.calculated_price.original_amount,
|
|
17
|
+
original_price: convertToLocale({
|
|
18
|
+
amount: variant.calculated_price.original_amount,
|
|
19
|
+
currency_code: variant.calculated_price.currency_code,
|
|
20
|
+
}),
|
|
21
|
+
currency_code: variant.calculated_price.currency_code,
|
|
22
|
+
price_type: variant.calculated_price.calculated_price.price_list_type,
|
|
23
|
+
percentage_diff: getPercentageDiff(
|
|
24
|
+
variant.calculated_price.original_amount,
|
|
25
|
+
variant.calculated_price.calculated_amount
|
|
26
|
+
),
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getProductPrice({
|
|
31
|
+
product,
|
|
32
|
+
variantId,
|
|
33
|
+
}: {
|
|
34
|
+
product: HttpTypes.StoreProduct
|
|
35
|
+
variantId?: string
|
|
36
|
+
}) {
|
|
37
|
+
if (!product || !product.id) {
|
|
38
|
+
throw new Error("No product provided")
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const cheapestPrice = () => {
|
|
42
|
+
if (!product || !product.variants?.length) {
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const cheapestVariant: any = product.variants
|
|
47
|
+
.filter((v: any) => !!v.calculated_price)
|
|
48
|
+
.sort((a: any, b: any) => {
|
|
49
|
+
return (
|
|
50
|
+
a.calculated_price.calculated_amount -
|
|
51
|
+
b.calculated_price.calculated_amount
|
|
52
|
+
)
|
|
53
|
+
})[0]
|
|
54
|
+
|
|
55
|
+
return getPricesForVariant(cheapestVariant)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const variantPrice = () => {
|
|
59
|
+
if (!product || !variantId) {
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const variant: any = product.variants?.find(
|
|
64
|
+
(v) => v.id === variantId || v.sku === variantId
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if (!variant) {
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return getPricesForVariant(variant)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
product,
|
|
76
|
+
cheapestPrice: cheapestPrice(),
|
|
77
|
+
variantPrice: variantPrice(),
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const isObject = (input: any) => input instanceof Object
|
|
2
|
+
export const isArray = (input: any) => Array.isArray(input)
|
|
3
|
+
export const isEmpty = (input: any) => {
|
|
4
|
+
return (
|
|
5
|
+
input === null ||
|
|
6
|
+
input === undefined ||
|
|
7
|
+
(isObject(input) && Object.keys(input).length === 0) ||
|
|
8
|
+
(isArray(input) && (input as any[]).length === 0) ||
|
|
9
|
+
(typeof input === "string" && input.trim().length === 0)
|
|
10
|
+
)
|
|
11
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { isEmpty } from "./isEmpty"
|
|
2
|
+
|
|
3
|
+
type ConvertToLocaleParams = {
|
|
4
|
+
amount: number
|
|
5
|
+
currency_code: string
|
|
6
|
+
minimumFractionDigits?: number
|
|
7
|
+
maximumFractionDigits?: number
|
|
8
|
+
locale?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const convertToLocale = ({
|
|
12
|
+
amount,
|
|
13
|
+
currency_code,
|
|
14
|
+
minimumFractionDigits,
|
|
15
|
+
maximumFractionDigits,
|
|
16
|
+
locale = "en-US",
|
|
17
|
+
}: ConvertToLocaleParams) => {
|
|
18
|
+
return currency_code && !isEmpty(currency_code)
|
|
19
|
+
? new Intl.NumberFormat(locale, {
|
|
20
|
+
style: "currency",
|
|
21
|
+
currency: currency_code,
|
|
22
|
+
minimumFractionDigits,
|
|
23
|
+
maximumFractionDigits,
|
|
24
|
+
}).format(amount)
|
|
25
|
+
: amount.toString()
|
|
26
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
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
|
+
/**
|
|
8
|
+
* Consistently determines which images to show for a product or a specific variant.
|
|
9
|
+
* Used on both server and client to prevent flickering during hydration.
|
|
10
|
+
*/
|
|
11
|
+
export function getImagesForVariant(
|
|
12
|
+
product: HttpTypes.StoreProduct,
|
|
13
|
+
selectedVariantId?: string
|
|
14
|
+
): HttpTypes.StoreProductImage[] {
|
|
15
|
+
// 1. If no variant is selected, show general product images
|
|
16
|
+
if (!selectedVariantId || !product.variants) {
|
|
17
|
+
return product.images || []
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// 2. Find the selected variant
|
|
21
|
+
const variant = product.variants.find((v) => v.id === selectedVariantId)
|
|
22
|
+
|
|
23
|
+
// 3. If variant has specific images assigned in the Media section
|
|
24
|
+
if (variant?.images && variant.images.length > 0) {
|
|
25
|
+
const variantImages = [...variant.images] as HttpTypes.StoreProductImage[]
|
|
26
|
+
|
|
27
|
+
// We want to avoid showing the main product thumbnail in the variant gallery
|
|
28
|
+
// if it's already included (Medusa often duplicates it).
|
|
29
|
+
const productImageUrls = new Set<string>()
|
|
30
|
+
const productImageIds = new Set<string>()
|
|
31
|
+
|
|
32
|
+
if (product.thumbnail) {
|
|
33
|
+
const cleanThumbnailUrl = product.thumbnail.split('?')[0].toLowerCase().trim()
|
|
34
|
+
const thumbnailPath = cleanThumbnailUrl.split('/').pop() || ''
|
|
35
|
+
productImageUrls.add(cleanThumbnailUrl)
|
|
36
|
+
if (thumbnailPath) {
|
|
37
|
+
productImageUrls.add(thumbnailPath.toLowerCase())
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (product.images && product.images.length > 0) {
|
|
42
|
+
product.images.forEach(img => {
|
|
43
|
+
if (img.url) {
|
|
44
|
+
const cleanUrl = img.url.split('?')[0].toLowerCase().trim()
|
|
45
|
+
const urlPath = cleanUrl.split('/').pop() || ''
|
|
46
|
+
productImageUrls.add(cleanUrl)
|
|
47
|
+
if (urlPath) {
|
|
48
|
+
productImageUrls.add(urlPath.toLowerCase())
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (img.id) {
|
|
52
|
+
productImageIds.add(img.id)
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const filteredVariantImages = variantImages.filter(img => {
|
|
58
|
+
if (!img.url) return false
|
|
59
|
+
|
|
60
|
+
const cleanImgUrl = img.url.split('?')[0].toLowerCase().trim()
|
|
61
|
+
const imgPath = cleanImgUrl.split('/').pop() || ''
|
|
62
|
+
const imgId = img.id
|
|
63
|
+
|
|
64
|
+
const urlMatches = productImageUrls.has(cleanImgUrl) || (imgPath && productImageUrls.has(imgPath.toLowerCase()))
|
|
65
|
+
const idMatches = imgId && productImageIds.has(imgId)
|
|
66
|
+
|
|
67
|
+
return !urlMatches && !idMatches
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
// Return variant-specific images, or fallback to all variant images if filtering cleared everything
|
|
71
|
+
return filteredVariantImages.length > 0 ? filteredVariantImages : variantImages
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 4. Default Fallback: If a specific variant is selected but has NO specific media images,
|
|
75
|
+
// show the product thumbnail as a single image.
|
|
76
|
+
if (product.thumbnail) {
|
|
77
|
+
return [
|
|
78
|
+
{
|
|
79
|
+
id: "default-thumb",
|
|
80
|
+
url: product.thumbnail,
|
|
81
|
+
} as HttpTypes.StoreProductImage,
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return product.images || []
|
|
86
|
+
}
|