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,33 @@
|
|
|
1
|
+
import { ArrowUpRightMini } from "@medusajs/icons"
|
|
2
|
+
import { Text } from "@medusajs/ui"
|
|
3
|
+
import LocalizedClientLink from "../localized-client-link"
|
|
4
|
+
|
|
5
|
+
type InteractiveLinkProps = {
|
|
6
|
+
href: string
|
|
7
|
+
children?: React.ReactNode
|
|
8
|
+
onClick?: () => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const InteractiveLink = ({
|
|
12
|
+
href,
|
|
13
|
+
children,
|
|
14
|
+
onClick,
|
|
15
|
+
...props
|
|
16
|
+
}: InteractiveLinkProps) => {
|
|
17
|
+
return (
|
|
18
|
+
<LocalizedClientLink
|
|
19
|
+
className="flex gap-x-1 items-center group"
|
|
20
|
+
href={href}
|
|
21
|
+
onClick={onClick}
|
|
22
|
+
{...props}
|
|
23
|
+
>
|
|
24
|
+
<Text className="text-ui-fg-interactive">{children}</Text>
|
|
25
|
+
<ArrowUpRightMini
|
|
26
|
+
className="group-hover:rotate-45 ease-in-out duration-150"
|
|
27
|
+
color="var(--fg-interactive)"
|
|
28
|
+
/>
|
|
29
|
+
</LocalizedClientLink>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default InteractiveLink
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { HttpTypes } from "@medusajs/types"
|
|
2
|
+
import { Text } from "@medusajs/ui"
|
|
3
|
+
|
|
4
|
+
type LineItemOptionsProps = {
|
|
5
|
+
variant: HttpTypes.StoreProductVariant | undefined
|
|
6
|
+
"data-testid"?: string
|
|
7
|
+
"data-value"?: HttpTypes.StoreProductVariant
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const LineItemOptions = ({
|
|
11
|
+
variant,
|
|
12
|
+
"data-testid": dataTestid,
|
|
13
|
+
"data-value": dataValue,
|
|
14
|
+
}: LineItemOptionsProps) => {
|
|
15
|
+
return (
|
|
16
|
+
<Text
|
|
17
|
+
data-testid={dataTestid}
|
|
18
|
+
data-value={dataValue}
|
|
19
|
+
className="inline-block txt-medium text-ui-fg-subtle w-full overflow-hidden text-ellipsis"
|
|
20
|
+
>
|
|
21
|
+
Variant: {variant?.title}
|
|
22
|
+
</Text>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default LineItemOptions
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { getPercentageDiff } from "medusa-ui-common/util/get-percentage-diff"
|
|
2
|
+
import { convertToLocale } from "medusa-ui-common/util/money"
|
|
3
|
+
import { HttpTypes } from "@medusajs/types"
|
|
4
|
+
import { clx } from "@medusajs/ui"
|
|
5
|
+
|
|
6
|
+
type LineItemPriceProps = {
|
|
7
|
+
item: HttpTypes.StoreCartLineItem | HttpTypes.StoreOrderLineItem
|
|
8
|
+
style?: "default" | "tight"
|
|
9
|
+
currencyCode: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const LineItemPrice = ({
|
|
13
|
+
item,
|
|
14
|
+
style = "default",
|
|
15
|
+
currencyCode,
|
|
16
|
+
}: LineItemPriceProps) => {
|
|
17
|
+
const { total, original_total } = item
|
|
18
|
+
const originalPrice = original_total ?? 0
|
|
19
|
+
const currentPrice = total ?? 0
|
|
20
|
+
const hasReducedPrice = currentPrice < originalPrice
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="flex flex-col gap-x-2 text-ui-fg-subtle items-end">
|
|
24
|
+
<div className="text-left">
|
|
25
|
+
{hasReducedPrice && (
|
|
26
|
+
<>
|
|
27
|
+
<p>
|
|
28
|
+
{style === "default" && (
|
|
29
|
+
<span className="text-ui-fg-subtle">Original: </span>
|
|
30
|
+
)}
|
|
31
|
+
<span
|
|
32
|
+
className="line-through text-ui-fg-muted"
|
|
33
|
+
data-testid="product-original-price"
|
|
34
|
+
>
|
|
35
|
+
{convertToLocale({
|
|
36
|
+
amount: originalPrice,
|
|
37
|
+
currency_code: currencyCode,
|
|
38
|
+
})}
|
|
39
|
+
</span>
|
|
40
|
+
</p>
|
|
41
|
+
{style === "default" && (
|
|
42
|
+
<span className="text-ui-fg-interactive">
|
|
43
|
+
-{getPercentageDiff(originalPrice, currentPrice || 0)}%
|
|
44
|
+
</span>
|
|
45
|
+
)}
|
|
46
|
+
</>
|
|
47
|
+
)}
|
|
48
|
+
<span
|
|
49
|
+
className={clx("text-base-regular", {
|
|
50
|
+
"text-ui-fg-interactive": hasReducedPrice,
|
|
51
|
+
})}
|
|
52
|
+
data-testid="product-price"
|
|
53
|
+
>
|
|
54
|
+
{convertToLocale({
|
|
55
|
+
amount: currentPrice,
|
|
56
|
+
currency_code: currencyCode,
|
|
57
|
+
})}
|
|
58
|
+
</span>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export default LineItemPrice
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { convertToLocale } from "medusa-ui-common/util/money"
|
|
2
|
+
import { HttpTypes } from "@medusajs/types"
|
|
3
|
+
import { clx } from "@medusajs/ui"
|
|
4
|
+
|
|
5
|
+
type LineItemUnitPriceProps = {
|
|
6
|
+
item: HttpTypes.StoreCartLineItem | HttpTypes.StoreOrderLineItem
|
|
7
|
+
style?: "default" | "tight"
|
|
8
|
+
currencyCode: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const LineItemUnitPrice = ({
|
|
12
|
+
item,
|
|
13
|
+
style = "default",
|
|
14
|
+
currencyCode,
|
|
15
|
+
}: LineItemUnitPriceProps) => {
|
|
16
|
+
const { total, original_total } = item
|
|
17
|
+
const lineTotal = total ?? 0
|
|
18
|
+
const lineOriginal = original_total ?? lineTotal
|
|
19
|
+
const hasReducedPrice = lineTotal < lineOriginal
|
|
20
|
+
|
|
21
|
+
const percentage_diff =
|
|
22
|
+
lineOriginal > 0
|
|
23
|
+
? Math.round(((lineOriginal - lineTotal) / lineOriginal) * 100)
|
|
24
|
+
: 0
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="flex flex-col text-ui-fg-muted justify-center h-full">
|
|
28
|
+
{hasReducedPrice && (
|
|
29
|
+
<>
|
|
30
|
+
<p>
|
|
31
|
+
{style === "default" && (
|
|
32
|
+
<span className="text-ui-fg-muted">Original: </span>
|
|
33
|
+
)}
|
|
34
|
+
<span
|
|
35
|
+
className="line-through"
|
|
36
|
+
data-testid="product-unit-original-price"
|
|
37
|
+
>
|
|
38
|
+
{convertToLocale({
|
|
39
|
+
amount: lineOriginal / item.quantity,
|
|
40
|
+
currency_code: currencyCode,
|
|
41
|
+
})}
|
|
42
|
+
</span>
|
|
43
|
+
</p>
|
|
44
|
+
{style === "default" && (
|
|
45
|
+
<span className="text-ui-fg-interactive">-{percentage_diff}%</span>
|
|
46
|
+
)}
|
|
47
|
+
</>
|
|
48
|
+
)}
|
|
49
|
+
<span
|
|
50
|
+
className={clx("text-base-regular", {
|
|
51
|
+
"text-ui-fg-interactive": hasReducedPrice,
|
|
52
|
+
})}
|
|
53
|
+
data-testid="product-unit-price"
|
|
54
|
+
>
|
|
55
|
+
{convertToLocale({
|
|
56
|
+
amount: lineTotal / item.quantity,
|
|
57
|
+
currency_code: currencyCode,
|
|
58
|
+
})}
|
|
59
|
+
</span>
|
|
60
|
+
</div>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export default LineItemUnitPrice
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import Link from "next/link"
|
|
4
|
+
import { useParams } from "next/navigation"
|
|
5
|
+
import React from "react"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Use this component to create a Next.js `<Link />` that persists the current country code in the url,
|
|
9
|
+
* without having to explicitly pass it as a prop.
|
|
10
|
+
*/
|
|
11
|
+
const LocalizedClientLink = ({
|
|
12
|
+
children,
|
|
13
|
+
href,
|
|
14
|
+
...props
|
|
15
|
+
}: {
|
|
16
|
+
children?: React.ReactNode
|
|
17
|
+
href: string
|
|
18
|
+
className?: string
|
|
19
|
+
onClick?: () => void
|
|
20
|
+
passHref?: true
|
|
21
|
+
[x: string]: any
|
|
22
|
+
}) => {
|
|
23
|
+
const { countryCode } = useParams()
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<Link href={`/${countryCode}${href}`} {...props}>
|
|
27
|
+
{children}
|
|
28
|
+
</Link>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default LocalizedClientLink
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useRouter, usePathname } from "next/navigation"
|
|
4
|
+
|
|
5
|
+
interface LoginPopupProps {
|
|
6
|
+
isOpen: boolean
|
|
7
|
+
onClose: () => void
|
|
8
|
+
countryCode?: string
|
|
9
|
+
message?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function LoginPopup({ isOpen, onClose, countryCode = "us", message = "Please login to add products to your wishlist." }: LoginPopupProps) {
|
|
13
|
+
const router = useRouter()
|
|
14
|
+
const pathname = usePathname()
|
|
15
|
+
|
|
16
|
+
const handleLoginClick = () => {
|
|
17
|
+
// Store current URL in localStorage for redirect after login
|
|
18
|
+
if (typeof window !== "undefined") {
|
|
19
|
+
const currentPath = pathname || window.location.pathname
|
|
20
|
+
// Add a special marker so the login component knows to keep the user on this page
|
|
21
|
+
const redirectWithContext = currentPath.includes('?')
|
|
22
|
+
? `${currentPath}&login_context=keep`
|
|
23
|
+
: `${currentPath}?login_context=keep`
|
|
24
|
+
|
|
25
|
+
localStorage.setItem("loginRedirectUrl", redirectWithContext)
|
|
26
|
+
}
|
|
27
|
+
router.push(`/${countryCode}/account`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!isOpen) {
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-white/10 backdrop-blur-md p-4 animate-in fade-in duration-200">
|
|
36
|
+
<div
|
|
37
|
+
className="bg-[#FFFAFE] rounded-2xl shadow-2xl p-8 max-w-sm w-full mx-auto border border-gray-100 transform transition-all scale-100 animate-in zoom-in-95 duration-200"
|
|
38
|
+
onClick={(e) => e.stopPropagation()}
|
|
39
|
+
>
|
|
40
|
+
<div className="text-center">
|
|
41
|
+
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-purple-100 mb-4">
|
|
42
|
+
<svg className="h-6 w-6 text-[#8B5AB1]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
43
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
|
44
|
+
</svg>
|
|
45
|
+
</div>
|
|
46
|
+
<h2 className="text-xl font-bold text-gray-900 mb-2">
|
|
47
|
+
Login Required
|
|
48
|
+
</h2>
|
|
49
|
+
<p className="text-gray-500 mb-8 text-sm leading-relaxed">
|
|
50
|
+
{message}
|
|
51
|
+
</p>
|
|
52
|
+
|
|
53
|
+
<div className="grid grid-cols-2 gap-3">
|
|
54
|
+
<button
|
|
55
|
+
onClick={onClose}
|
|
56
|
+
className="px-4 py-2.5 border border-gray-200 rounded-xl font-semibold text-gray-600 hover:bg-gray-50 hover:border-gray-300 active:scale-95 transition-all duration-200"
|
|
57
|
+
>
|
|
58
|
+
Cancel
|
|
59
|
+
</button>
|
|
60
|
+
<button
|
|
61
|
+
onClick={handleLoginClick}
|
|
62
|
+
className="px-4 py-2.5 rounded-xl font-semibold text-white transition-all duration-200 hover:shadow-lg hover:shadow-purple-100 active:scale-95"
|
|
63
|
+
style={{ backgroundColor: '#8B5AB1' }}
|
|
64
|
+
onMouseEnter={(e) => {
|
|
65
|
+
e.currentTarget.style.backgroundColor = '#7a4a9a'
|
|
66
|
+
}}
|
|
67
|
+
onMouseLeave={(e) => {
|
|
68
|
+
e.currentTarget.style.backgroundColor = '#8B5AB1'
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
Login
|
|
72
|
+
</button>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { Dialog, Transition } from "@headlessui/react"
|
|
2
|
+
import { clx } from "@medusajs/ui"
|
|
3
|
+
import React, { Fragment } from "react"
|
|
4
|
+
|
|
5
|
+
import { ModalProvider, useModal } from "medusa-ui-common/context/modal-context"
|
|
6
|
+
import X from "medusa-ui-common/common/icons/x"
|
|
7
|
+
|
|
8
|
+
type ModalProps = {
|
|
9
|
+
isOpen: boolean
|
|
10
|
+
close: () => void
|
|
11
|
+
size?: "tiny" | "xsmall" | "small" | "medium" | "large"
|
|
12
|
+
search?: boolean
|
|
13
|
+
children: React.ReactNode
|
|
14
|
+
'data-testid'?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const Modal = ({
|
|
18
|
+
isOpen,
|
|
19
|
+
close,
|
|
20
|
+
size = "medium",
|
|
21
|
+
search = false,
|
|
22
|
+
children,
|
|
23
|
+
'data-testid': dataTestId
|
|
24
|
+
}: ModalProps) => {
|
|
25
|
+
return (
|
|
26
|
+
<Transition appear show={isOpen} as={Fragment}>
|
|
27
|
+
<Dialog as="div" className="relative z-[75]" onClose={close}>
|
|
28
|
+
<Transition.Child
|
|
29
|
+
as={Fragment}
|
|
30
|
+
enter="ease-out duration-300"
|
|
31
|
+
enterFrom="opacity-0"
|
|
32
|
+
enterTo="opacity-100"
|
|
33
|
+
leave="ease-in duration-200"
|
|
34
|
+
leaveFrom="opacity-100"
|
|
35
|
+
leaveTo="opacity-0"
|
|
36
|
+
>
|
|
37
|
+
<div className="fixed inset-0 bg-opacity-75 backdrop-blur-md h-screen" />
|
|
38
|
+
</Transition.Child>
|
|
39
|
+
|
|
40
|
+
<div className="fixed inset-0 overflow-y-hidden">
|
|
41
|
+
<div
|
|
42
|
+
className={clx(
|
|
43
|
+
"flex min-h-full h-full justify-center p-4 text-center",
|
|
44
|
+
{
|
|
45
|
+
"items-center": !search,
|
|
46
|
+
"items-start": search,
|
|
47
|
+
}
|
|
48
|
+
)}
|
|
49
|
+
>
|
|
50
|
+
<Transition.Child
|
|
51
|
+
as={Fragment}
|
|
52
|
+
enter="ease-out duration-300"
|
|
53
|
+
enterFrom="opacity-0 scale-95"
|
|
54
|
+
enterTo="opacity-100 scale-100"
|
|
55
|
+
leave="ease-in duration-200"
|
|
56
|
+
leaveFrom="opacity-100 scale-100"
|
|
57
|
+
leaveTo="opacity-0 scale-95"
|
|
58
|
+
>
|
|
59
|
+
<Dialog.Panel
|
|
60
|
+
data-testid={dataTestId}
|
|
61
|
+
className={clx(
|
|
62
|
+
"flex flex-col justify-start w-full transform p-0 text-left align-middle transition-all max-h-[90vh] overflow-hidden bg-white shadow-xl border rounded-[32px]",
|
|
63
|
+
{
|
|
64
|
+
"max-w-[320px] mx-auto": size === "tiny",
|
|
65
|
+
"max-w-[320px] min-[400px]:max-w-[400px] min-[640px]:max-w-[450px] mx-auto": size === "xsmall",
|
|
66
|
+
"max-w-md": size === "small",
|
|
67
|
+
"max-w-xl": size === "medium",
|
|
68
|
+
"max-w-3xl": size === "large",
|
|
69
|
+
"bg-transparent shadow-none": search,
|
|
70
|
+
}
|
|
71
|
+
)}
|
|
72
|
+
>
|
|
73
|
+
<ModalProvider close={close}>
|
|
74
|
+
<div className="flex flex-col flex-1 h-full overflow-hidden">
|
|
75
|
+
{children}
|
|
76
|
+
</div>
|
|
77
|
+
</ModalProvider>
|
|
78
|
+
</Dialog.Panel>
|
|
79
|
+
</Transition.Child>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</Dialog>
|
|
83
|
+
</Transition>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const Title: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
88
|
+
const { close } = useModal()
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<Dialog.Title className="flex items-center justify-between px-6 small:px-8 py-5 border-b border-gray-100 flex-shrink-0">
|
|
92
|
+
<div className="text-xl small:text-2xl font-bold text-gray-900">{children}</div>
|
|
93
|
+
<div>
|
|
94
|
+
<button onClick={close} data-testid="close-modal-button" className="p-2 hover:bg-gray-100 rounded-full transition-colors text-gray-500">
|
|
95
|
+
<X size={24} />
|
|
96
|
+
</button>
|
|
97
|
+
</div>
|
|
98
|
+
</Dialog.Title>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const Description: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
103
|
+
return (
|
|
104
|
+
<Dialog.Description className="flex text-small-regular text-ui-fg-base items-center justify-center pt-2 pb-4 h-full">
|
|
105
|
+
{children}
|
|
106
|
+
</Dialog.Description>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const Body: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
111
|
+
return <div className="p-6 small:p-8 overflow-y-auto [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none] flex-1">{children}</div>
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const Footer: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
115
|
+
return <div className="flex items-center justify-end gap-x-4 px-6 small:px-8 py-5 border-t border-gray-100 flex-shrink-0">{children}</div>
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
Modal.Title = Title
|
|
119
|
+
Modal.Description = Description
|
|
120
|
+
Modal.Body = Body
|
|
121
|
+
Modal.Footer = Footer
|
|
122
|
+
|
|
123
|
+
export default Modal
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { ChevronUpDown } from "@medusajs/icons"
|
|
2
|
+
import { clx } from "@medusajs/ui"
|
|
3
|
+
import {
|
|
4
|
+
SelectHTMLAttributes,
|
|
5
|
+
forwardRef,
|
|
6
|
+
useEffect,
|
|
7
|
+
useImperativeHandle,
|
|
8
|
+
useRef,
|
|
9
|
+
useState,
|
|
10
|
+
} from "react"
|
|
11
|
+
|
|
12
|
+
export type NativeSelectProps = {
|
|
13
|
+
placeholder?: string
|
|
14
|
+
errors?: Record<string, unknown>
|
|
15
|
+
touched?: Record<string, unknown>
|
|
16
|
+
} & SelectHTMLAttributes<HTMLSelectElement>
|
|
17
|
+
|
|
18
|
+
const NativeSelect = forwardRef<HTMLSelectElement, NativeSelectProps>(
|
|
19
|
+
(
|
|
20
|
+
{ placeholder = "Select...", defaultValue, className, children, ...props },
|
|
21
|
+
ref
|
|
22
|
+
) => {
|
|
23
|
+
const innerRef = useRef<HTMLSelectElement>(null)
|
|
24
|
+
const [isPlaceholder, setIsPlaceholder] = useState(false)
|
|
25
|
+
|
|
26
|
+
useImperativeHandle<HTMLSelectElement | null, HTMLSelectElement | null>(
|
|
27
|
+
ref,
|
|
28
|
+
() => innerRef.current
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (innerRef.current && innerRef.current.value === "") {
|
|
33
|
+
setIsPlaceholder(true)
|
|
34
|
+
} else {
|
|
35
|
+
setIsPlaceholder(false)
|
|
36
|
+
}
|
|
37
|
+
}, [innerRef.current?.value])
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div>
|
|
41
|
+
<div
|
|
42
|
+
onFocus={() => innerRef.current?.focus()}
|
|
43
|
+
onBlur={() => innerRef.current?.blur()}
|
|
44
|
+
className={clx(
|
|
45
|
+
"relative flex items-center text-base-regular border bg-white rounded-lg hover:bg-gray-50",
|
|
46
|
+
className,
|
|
47
|
+
{
|
|
48
|
+
"text-ui-fg-muted": isPlaceholder,
|
|
49
|
+
}
|
|
50
|
+
)}
|
|
51
|
+
style={{ borderColor: '#C0C0C0' }}
|
|
52
|
+
>
|
|
53
|
+
<select
|
|
54
|
+
ref={innerRef}
|
|
55
|
+
defaultValue={defaultValue}
|
|
56
|
+
{...props}
|
|
57
|
+
className="appearance-none flex-1 bg-transparent border-none px-4 py-2.5 transition-colors duration-150 outline-none "
|
|
58
|
+
>
|
|
59
|
+
<option disabled value="">
|
|
60
|
+
{placeholder}
|
|
61
|
+
</option>
|
|
62
|
+
{children}
|
|
63
|
+
</select>
|
|
64
|
+
<span className="absolute right-4 inset-y-0 flex items-center pointer-events-none ">
|
|
65
|
+
<ChevronUpDown />
|
|
66
|
+
</span>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
NativeSelect.displayName = "NativeSelect"
|
|
74
|
+
|
|
75
|
+
export default NativeSelect
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState } from "react"
|
|
4
|
+
|
|
5
|
+
interface ObfuscatedEmailProps {
|
|
6
|
+
email: string
|
|
7
|
+
className?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const ObfuscatedEmail: React.FC<ObfuscatedEmailProps> = ({ email, className }) => {
|
|
11
|
+
const [displayEmail, setDisplayEmail] = useState("")
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
// Reveal the email only on the client side
|
|
15
|
+
setDisplayEmail(email)
|
|
16
|
+
}, [email])
|
|
17
|
+
|
|
18
|
+
if (!displayEmail) {
|
|
19
|
+
// Hide or show placeholder while loading
|
|
20
|
+
return <span className={className}>...</span>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<a href={`mailto:${displayEmail}`} className={className}>
|
|
25
|
+
{displayEmail}
|
|
26
|
+
</a>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default ObfuscatedEmail
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect } from "react"
|
|
4
|
+
|
|
5
|
+
const ProcessingOverlay = ({
|
|
6
|
+
isOpen,
|
|
7
|
+
title,
|
|
8
|
+
subtitle = "Please do not refresh or close this window",
|
|
9
|
+
variant = "payment"
|
|
10
|
+
}: {
|
|
11
|
+
isOpen: boolean
|
|
12
|
+
title?: string
|
|
13
|
+
subtitle?: string
|
|
14
|
+
variant?: "payment" | "order"
|
|
15
|
+
}) => {
|
|
16
|
+
const [messageIndex, setMessageIndex] = useState(0)
|
|
17
|
+
|
|
18
|
+
const paymentMessages = [
|
|
19
|
+
"Securing your transaction...",
|
|
20
|
+
"Verifying with payment provider...",
|
|
21
|
+
"Finalizing your order...",
|
|
22
|
+
"Almost there...",
|
|
23
|
+
"Finishing up..."
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
const orderMessages = [
|
|
27
|
+
"Placing your order...",
|
|
28
|
+
"Preparing your confirmation...",
|
|
29
|
+
"Almost there...",
|
|
30
|
+
"Finalizing details...",
|
|
31
|
+
"Finishing up..."
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
const messages = variant === "payment" ? paymentMessages : orderMessages
|
|
35
|
+
const displayTitle = title || (variant === "payment" ? "Processing Payment" : "Placing Order")
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!isOpen) {
|
|
39
|
+
setMessageIndex(0)
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const interval = setInterval(() => {
|
|
44
|
+
setMessageIndex((prev) => (prev + 1) % messages.length)
|
|
45
|
+
}, 3000)
|
|
46
|
+
|
|
47
|
+
return () => clearInterval(interval)
|
|
48
|
+
}, [isOpen, messages.length])
|
|
49
|
+
|
|
50
|
+
if (!isOpen) return null
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-white/70 backdrop-blur-md animate-in fade-in duration-300">
|
|
54
|
+
<div className="bg-white p-8 rounded-2xl shadow-2xl border border-purple-100 flex flex-col items-center max-w-xs w-full mx-4 transform animate-in zoom-in-95 duration-300">
|
|
55
|
+
<div className="relative mb-2">
|
|
56
|
+
<img
|
|
57
|
+
src="/uploads/profile-images/t-shirt.gif"
|
|
58
|
+
alt="Placing order..."
|
|
59
|
+
className="w-32 h-32 object-contain mx-auto"
|
|
60
|
+
/>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<h3 className="text-xl font-bold text-gray-900 mb-1 text-center">{displayTitle}</h3>
|
|
64
|
+
|
|
65
|
+
<div className="h-6 flex items-center justify-center mb-2 text-center px-4">
|
|
66
|
+
<p key={messageIndex} className="text-[#8B5AB1] font-semibold text-sm animate-in slide-in-from-bottom-2 fade-in duration-500">
|
|
67
|
+
{messages[messageIndex]}
|
|
68
|
+
</p>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div className="w-full bg-gray-100 h-1.5 rounded-full overflow-hidden mb-4">
|
|
72
|
+
<div className="h-full bg-[#8B5AB1] rounded-full animate-progress transition-all duration-500 ease-out"></div>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<p className="text-xs text-gray-400 text-center italic leading-relaxed">
|
|
76
|
+
{subtitle}
|
|
77
|
+
</p>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export default ProcessingOverlay
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const Radio = ({ checked, 'data-testid': dataTestId }: { checked: boolean, 'data-testid'?: string }) => {
|
|
2
|
+
return (
|
|
3
|
+
<>
|
|
4
|
+
<button
|
|
5
|
+
type="button"
|
|
6
|
+
role="radio"
|
|
7
|
+
aria-checked="true"
|
|
8
|
+
data-state={checked ? "checked" : "unchecked"}
|
|
9
|
+
className="group relative flex h-5 w-5 items-center justify-center outline-none"
|
|
10
|
+
data-testid={dataTestId || 'radio-button'}
|
|
11
|
+
>
|
|
12
|
+
<div className="shadow-borders-base group-hover:shadow-borders-strong-with-shadow bg-ui-bg-base group-data-[state=checked]:bg-ui-bg-interactive group-data-[state=checked]:shadow-borders-interactive group-focus:!shadow-borders-interactive-with-focus group-disabled:!bg-ui-bg-disabled group-disabled:!shadow-borders-base flex h-[14px] w-[14px] items-center justify-center rounded-full transition-all">
|
|
13
|
+
{checked && (
|
|
14
|
+
<span
|
|
15
|
+
data-state={checked ? "checked" : "unchecked"}
|
|
16
|
+
className="group flex items-center justify-center"
|
|
17
|
+
>
|
|
18
|
+
<div className="bg-ui-bg-base shadow-details-contrast-on-bg-interactive group-disabled:bg-ui-fg-disabled rounded-full group-disabled:shadow-none h-1.5 w-1.5"></div>
|
|
19
|
+
</span>
|
|
20
|
+
)}
|
|
21
|
+
</div>
|
|
22
|
+
</button>
|
|
23
|
+
</>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default Radio
|