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.
Files changed (79) hide show
  1. package/package.json +93 -0
  2. package/src/common/components/breadcrumb/index.tsx +43 -0
  3. package/src/common/components/cart-totals/index.tsx +562 -0
  4. package/src/common/components/checkbox/index.tsx +98 -0
  5. package/src/common/components/delete-button/index.tsx +158 -0
  6. package/src/common/components/discount-code/index.tsx +220 -0
  7. package/src/common/components/divider/index.tsx +9 -0
  8. package/src/common/components/error-message/index.tsx +13 -0
  9. package/src/common/components/filter-checkbox-group/index.tsx +134 -0
  10. package/src/common/components/filter-radio-group/index.tsx +62 -0
  11. package/src/common/components/input/index.tsx +79 -0
  12. package/src/common/components/interactive-link/index.tsx +33 -0
  13. package/src/common/components/line-item-options/index.tsx +26 -0
  14. package/src/common/components/line-item-price/index.tsx +64 -0
  15. package/src/common/components/line-item-unit-price/index.tsx +64 -0
  16. package/src/common/components/localized-client-link/index.tsx +32 -0
  17. package/src/common/components/login-popup/index.tsx +78 -0
  18. package/src/common/components/modal/index.tsx +123 -0
  19. package/src/common/components/native-select/index.tsx +75 -0
  20. package/src/common/components/obfuscated-email/index.tsx +30 -0
  21. package/src/common/components/processing-overlay/index.tsx +83 -0
  22. package/src/common/components/radio/index.tsx +27 -0
  23. package/src/common/components/side-panel/index.tsx +65 -0
  24. package/src/common/components/submit-button/index.tsx +32 -0
  25. package/src/common/icons/arrow-left.tsx +36 -0
  26. package/src/common/icons/back.tsx +37 -0
  27. package/src/common/icons/bancontact.tsx +26 -0
  28. package/src/common/icons/chevron-down.tsx +30 -0
  29. package/src/common/icons/delivered.tsx +29 -0
  30. package/src/common/icons/envelope.tsx +27 -0
  31. package/src/common/icons/eye-off.tsx +37 -0
  32. package/src/common/icons/eye.tsx +37 -0
  33. package/src/common/icons/fast-delivery.tsx +65 -0
  34. package/src/common/icons/ideal.tsx +26 -0
  35. package/src/common/icons/lock.tsx +31 -0
  36. package/src/common/icons/map-pin.tsx +37 -0
  37. package/src/common/icons/medusa.tsx +27 -0
  38. package/src/common/icons/menu.tsx +45 -0
  39. package/src/common/icons/nextjs.tsx +27 -0
  40. package/src/common/icons/package.tsx +44 -0
  41. package/src/common/icons/paypal.tsx +30 -0
  42. package/src/common/icons/phone.tsx +30 -0
  43. package/src/common/icons/placeholder-image.tsx +44 -0
  44. package/src/common/icons/refresh.tsx +51 -0
  45. package/src/common/icons/spinner.tsx +37 -0
  46. package/src/common/icons/trash.tsx +51 -0
  47. package/src/common/icons/user.tsx +37 -0
  48. package/src/common/icons/x.tsx +37 -0
  49. package/src/constants/payments.tsx +31 -0
  50. package/src/context/modal-context.tsx +37 -0
  51. package/src/context/wishlist-context.tsx +83 -0
  52. package/src/index.ts +16 -0
  53. package/src/skeletons/components/skeleton-button/index.tsx +5 -0
  54. package/src/skeletons/components/skeleton-card-details/index.tsx +10 -0
  55. package/src/skeletons/components/skeleton-cart-item/index.tsx +35 -0
  56. package/src/skeletons/components/skeleton-cart-totals/index.tsx +30 -0
  57. package/src/skeletons/components/skeleton-code-form/index.tsx +13 -0
  58. package/src/skeletons/components/skeleton-line-item/index.tsx +35 -0
  59. package/src/skeletons/components/skeleton-order-confirmed-header/index.tsx +14 -0
  60. package/src/skeletons/components/skeleton-order-information/index.tsx +36 -0
  61. package/src/skeletons/components/skeleton-order-items/index.tsx +43 -0
  62. package/src/skeletons/components/skeleton-order-summary/index.tsx +15 -0
  63. package/src/skeletons/components/skeleton-product-preview/index.tsx +15 -0
  64. package/src/skeletons/templates/skeleton-cart-page/index.tsx +65 -0
  65. package/src/skeletons/templates/skeleton-order-confirmed/index.tsx +21 -0
  66. package/src/skeletons/templates/skeleton-product-grid/index.tsx +23 -0
  67. package/src/skeletons/templates/skeleton-related-products/index.tsx +25 -0
  68. package/src/types/global.ts +24 -0
  69. package/src/types/icon.ts +6 -0
  70. package/src/util/checkout-dom.ts +65 -0
  71. package/src/util/compare-addresses.ts +28 -0
  72. package/src/util/env.ts +3 -0
  73. package/src/util/get-percentage-diff.ts +6 -0
  74. package/src/util/get-product-price.ts +79 -0
  75. package/src/util/isEmpty.ts +11 -0
  76. package/src/util/money.ts +26 -0
  77. package/src/util/product.ts +86 -0
  78. package/src/util/repeat.ts +5 -0
  79. 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