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,158 @@
1
+ "use client"
2
+
3
+ import { deleteLineItem } from "medusa-storefront-data/cart"
4
+ import { addToWishlist } from "medusa-wishlist-logic/server"
5
+ import { Spinner, Trash } from "@medusajs/icons"
6
+ import { clx, Button, Heading, Text } from "@medusajs/ui"
7
+ import Modal from "medusa-ui-common/common/components/modal"
8
+ import { useState } from "react"
9
+ import { useWishlist } from "medusa-ui-common/context/wishlist-context"
10
+ import { trackAddToWishlist } from "medusa-storefront-analytics"
11
+
12
+ import Image from "next/image"
13
+ import X from "medusa-ui-common/common/icons/x"
14
+
15
+ const DeleteButton = ({
16
+ id,
17
+ productId,
18
+ thumbnail,
19
+ children,
20
+ className,
21
+ onAfterDelete,
22
+ "data-testid": dataTestId,
23
+ }: {
24
+ id: string
25
+ productId?: string
26
+ thumbnail?: string | null
27
+ children?: React.ReactNode
28
+ className?: string
29
+ onAfterDelete?: () => void
30
+ "data-testid"?: string
31
+ }) => {
32
+ const [isDeleting, setIsDeleting] = useState(false)
33
+ const [showModal, setShowModal] = useState(false)
34
+ const [wishlistLoading, setWishlistLoading] = useState(false)
35
+ const [error, setError] = useState<string | null>(null)
36
+ const { refreshWishlist } = useWishlist()
37
+
38
+ const handleDelete = async () => {
39
+ setIsDeleting(true)
40
+ setShowModal(false)
41
+ try {
42
+ await deleteLineItem(id)
43
+ onAfterDelete?.()
44
+ } catch {
45
+ // keep isDeleting false on error
46
+ } finally {
47
+ setIsDeleting(false)
48
+ }
49
+ }
50
+
51
+ const handleMoveToWishlist = async () => {
52
+ if (!productId) {
53
+ handleDelete()
54
+ return
55
+ }
56
+
57
+ setWishlistLoading(true)
58
+ setError(null)
59
+
60
+ const result = await addToWishlist(productId)
61
+
62
+ if (result.success) {
63
+ trackAddToWishlist({ content_ids: [productId] })
64
+ await refreshWishlist()
65
+ await handleDelete()
66
+ } else {
67
+ setError(result.error || "Please login to move items to wishlist.")
68
+ setWishlistLoading(false)
69
+ }
70
+ }
71
+
72
+ return (
73
+ <>
74
+ <div
75
+ className={clx(
76
+ "flex items-center justify-between text-small-regular",
77
+ className
78
+ )}
79
+ >
80
+ <button
81
+ className="flex gap-x-1 text-ui-fg-subtle hover:text-ui-fg-base cursor-pointer items-center"
82
+ onClick={() => setShowModal(true)}
83
+ data-testid={dataTestId}
84
+ >
85
+ {isDeleting ? <Spinner className="animate-spin" /> : <Trash />}
86
+ <span>{children}</span>
87
+ </button>
88
+ </div>
89
+
90
+ <Modal isOpen={showModal} close={() => setShowModal(false)} size="medium">
91
+ <div className="flex flex-col bg-white overflow-hidden rounded-sm relative">
92
+ {/* Close Icon */}
93
+ <button
94
+ onClick={() => setShowModal(false)}
95
+ className="absolute top-4 right-4 text-black hover:opacity-70 transition-opacity z-10"
96
+ >
97
+ <X size={24} />
98
+ </button>
99
+
100
+ {/* Body Content */}
101
+ <div className="p-6 flex gap-6 pr-12">
102
+ {/* Product Thumbnail */}
103
+ <div className="w-24 h-24 sm:w-28 sm:h-28 flex-shrink-0 bg-gray-50 rounded-sm overflow-hidden border border-gray-100 shadow-sm">
104
+ {thumbnail ? (
105
+ <Image
106
+ src={thumbnail}
107
+ alt="Product"
108
+ width={120}
109
+ height={120}
110
+ className="w-full h-full object-cover"
111
+ />
112
+ ) : (
113
+ <div className="w-full h-full flex items-center justify-center text-gray-300 bg-gray-50 uppercase text-[10px] font-bold">No Image</div>
114
+ )}
115
+ </div>
116
+
117
+ {/* Title and Description */}
118
+ <div className="flex flex-col justify-center">
119
+ <h3 className="text-[17px] font-bold text-gray-800 mb-1.5 leading-tight">Move from Bag</h3>
120
+ <p className="text-[15px] text-gray-500 leading-snug font-medium max-w-[280px]">
121
+ Are you sure you want to move this item from bag?
122
+ </p>
123
+
124
+ {error && (
125
+ <p className="text-red-500 text-[11px] font-bold mt-2 uppercase tracking-wide">{error}</p>
126
+ )}
127
+ </div>
128
+ </div>
129
+
130
+ {/* Footer with side-by-side buttons and separator */}
131
+ <div className="flex border-t border-gray-100 h-[52px]">
132
+ <button
133
+ onClick={handleDelete}
134
+ disabled={isDeleting}
135
+ className="flex-1 h-full flex items-center justify-center text-[13px] font-bold text-gray-500 tracking-wider hover:bg-gray-50 transition-colors uppercase"
136
+ >
137
+ {isDeleting ? <Spinner className="animate-spin" /> : "REMOVE"}
138
+ </button>
139
+
140
+ {/* Vertical Separator */}
141
+ <div className="w-px bg-gray-100 h-full" />
142
+
143
+ <button
144
+ onClick={handleMoveToWishlist}
145
+ disabled={wishlistLoading}
146
+ className="flex-1 h-full flex items-center justify-center text-[13px] font-bold tracking-wider hover:bg-gray-50 transition-colors uppercase"
147
+ style={{ color: '#D25C78' }} // Pinkish color from reference
148
+ >
149
+ {wishlistLoading ? <Spinner className="animate-spin" /> : "MOVE TO WISHLIST"}
150
+ </button>
151
+ </div>
152
+ </div>
153
+ </Modal>
154
+ </>
155
+ )
156
+ }
157
+
158
+ export default DeleteButton
@@ -0,0 +1,220 @@
1
+ "use client"
2
+
3
+ import { Badge, Heading, Input, Label, Text } from "@medusajs/ui"
4
+ import React from "react"
5
+
6
+ import { applyPromotions } from "medusa-storefront-data/cart"
7
+ import { convertToLocale } from "medusa-ui-common/util/money"
8
+ import { HttpTypes } from "@medusajs/types"
9
+ import Trash from "medusa-ui-common/common/icons/trash"
10
+ import ErrorMessage from "medusa-ui-common/common/components/error-message"
11
+ import { SubmitButton } from "medusa-ui-common/common/components/submit-button"
12
+
13
+ type DiscountCodeProps = {
14
+ cart: HttpTypes.StoreCart & {
15
+ promotions: HttpTypes.StorePromotion[]
16
+ }
17
+ }
18
+
19
+ const DiscountCode: React.FC<DiscountCodeProps> = ({ cart }) => {
20
+ const [isOpen, setIsOpen] = React.useState(false)
21
+ const [errorMessage, setErrorMessage] = React.useState("")
22
+ const [successMessage, setSuccessMessage] = React.useState("")
23
+
24
+ const { promotions = [] } = cart
25
+ const removePromotionCode = async (code: string) => {
26
+ const validPromotions = promotions.filter(
27
+ (promotion) => promotion.code !== code
28
+ )
29
+
30
+ await applyPromotions(
31
+ validPromotions.filter((p) => p.code !== undefined).map((p) => p.code!)
32
+ )
33
+ setSuccessMessage("")
34
+ }
35
+
36
+ const addPromotionCode = async (formData: FormData) => {
37
+ setErrorMessage("")
38
+ setSuccessMessage("")
39
+
40
+ const code = formData.get("code")
41
+ if (!code) {
42
+ return
43
+ }
44
+ const input = document.getElementById("promotion-input") as HTMLInputElement
45
+ const codes = promotions
46
+ .filter((p) => p.code !== undefined)
47
+ .map((p) => p.code!)
48
+ codes.push(code.toString())
49
+
50
+ try {
51
+ const updatedCart = await applyPromotions(codes)
52
+ const hasDiscount = (updatedCart?.discount_total || 0) > 0
53
+
54
+ if (hasDiscount) {
55
+ setSuccessMessage("Coupon applied successfully!")
56
+ } else {
57
+ setErrorMessage("Coupon added, but it doesn't apply to the items in your bag.")
58
+ }
59
+ } catch (e: any) {
60
+ setErrorMessage("Invalid coupon code. Please check the code and try again.")
61
+ }
62
+
63
+ if (input) {
64
+ input.value = ""
65
+ }
66
+ }
67
+
68
+ return (
69
+ <div
70
+ className="border border-gray-200 rounded-lg p-6 flex flex-col"
71
+ style={{ width: '480px', maxWidth: '100%' }}
72
+ >
73
+ {/* COUPONS Heading */}
74
+ <Heading className="text-lg font-bold text-gray-900 mb-4 uppercase">
75
+ COUPONS
76
+ </Heading>
77
+
78
+ {/* Apply Coupons Button */}
79
+ <button
80
+ onClick={() => setIsOpen(!isOpen)}
81
+ type="button"
82
+ className="w-full rounded-lg p-4 flex items-center justify-between transition-colors border border-purple-200"
83
+ style={{ backgroundColor: '#D195FF33' }}
84
+ data-testid="add-discount-button"
85
+ >
86
+ <div className="flex items-center gap-3">
87
+ {/* Percentage Icon - White circle with black % symbol */}
88
+ <div className="w-10 h-10 rounded-full bg-white flex items-center justify-center flex-shrink-0 shadow-sm">
89
+ <span className="text-gray-900 font-bold text-xl">%</span>
90
+ </div>
91
+
92
+ {/* Text Content */}
93
+ <div className="flex flex-col items-start">
94
+ <span className="font-bold text-base text-gray-900">
95
+ Apply Coupons
96
+ </span>
97
+ <span className="text-sm text-gray-500">
98
+ View all offers
99
+ </span>
100
+ </div>
101
+ </div>
102
+
103
+ {/* Chevron Icon */}
104
+ <svg
105
+ className="w-5 h-5 text-gray-900"
106
+ fill="none"
107
+ stroke="currentColor"
108
+ viewBox="0 0 24 24"
109
+ >
110
+ <path
111
+ strokeLinecap="round"
112
+ strokeLinejoin="round"
113
+ strokeWidth={2}
114
+ d="M9 5l7 7-7 7"
115
+ />
116
+ </svg>
117
+ </button>
118
+
119
+ {/* Promotion Code Input Form */}
120
+ {isOpen && (
121
+ <form action={(a) => addPromotionCode(a)} className="w-full mt-4">
122
+ <div className="flex w-full gap-x-2">
123
+ <Input
124
+ className="size-full"
125
+ id="promotion-input"
126
+ name="code"
127
+ type="text"
128
+ autoFocus={false}
129
+ placeholder="Enter coupon code"
130
+ data-testid="discount-input"
131
+ />
132
+ <SubmitButton
133
+ variant="secondary"
134
+ className="!bg-[#8B5AB1] hover:!bg-[#7a4a9f] text-white border-0"
135
+ data-testid="discount-apply-button"
136
+ >
137
+ Apply
138
+ </SubmitButton>
139
+ </div>
140
+
141
+ <ErrorMessage
142
+ error={errorMessage}
143
+ data-testid="discount-error-message"
144
+ />
145
+
146
+ {successMessage && (
147
+ <div
148
+ className="mt-2 text-sm text-green-500"
149
+ data-testid="discount-success-message"
150
+ >
151
+ {successMessage}
152
+ </div>
153
+ )}
154
+ </form>
155
+ )}
156
+
157
+ {/* Applied Promotions - Clean Card Redesign */}
158
+ {promotions.length > 0 && (
159
+ <div className="w-full flex flex-col mt-6 pt-4 border-t border-gray-100">
160
+ <Heading className="text-sm font-bold mb-3 text-gray-900 uppercase tracking-tight">
161
+ Promotion(s) applied:
162
+ </Heading>
163
+
164
+ <div className="flex flex-col gap-y-2">
165
+ {promotions.map((promotion) => {
166
+ return (
167
+ <div
168
+ key={promotion.id}
169
+ className="flex items-center justify-between w-full p-3 rounded-xl border border-purple-100 bg-purple-50/50 transition-all hover:bg-purple-50 group"
170
+ data-testid="discount-row"
171
+ >
172
+ <div className="flex items-center gap-x-3 overflow-hidden">
173
+ {/* Tag Icon */}
174
+ <div className="flex-shrink-0 text-[#8B5AB1]">
175
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
176
+ <path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path>
177
+ <line x1="7" y1="7" x2="7.01" y2="7"></line>
178
+ </svg>
179
+ </div>
180
+
181
+ <div className="flex flex-col overflow-hidden">
182
+ <span className="font-bold truncate text-sm uppercase tracking-wider" style={{ color: '#8B5AB1' }} data-testid="discount-code">
183
+ {promotion.code}
184
+ </span>
185
+ {promotion.application_method?.value !== undefined && (
186
+ <span className="text-xs font-bold" style={{ color: '#FE5FB7' }}>
187
+ {promotion.application_method.type === "percentage"
188
+ ? `${promotion.application_method.value}% OFF`
189
+ : `${convertToLocale({
190
+ amount: +promotion.application_method.value,
191
+ currency_code: promotion.application_method.currency_code || "INR",
192
+ })} OFF`}
193
+ </span>
194
+ )}
195
+ </div>
196
+ </div>
197
+
198
+ {!promotion.is_automatic && (
199
+ <button
200
+ className="p-2 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-full transition-all"
201
+ onClick={() => {
202
+ if (!promotion.code) return
203
+ removePromotionCode(promotion.code)
204
+ }}
205
+ data-testid="remove-discount-button"
206
+ >
207
+ <Trash size={16} />
208
+ </button>
209
+ )}
210
+ </div>
211
+ )
212
+ })}
213
+ </div>
214
+ </div>
215
+ )}
216
+ </div>
217
+ )
218
+ }
219
+
220
+ export default DiscountCode
@@ -0,0 +1,9 @@
1
+ import { clx } from "@medusajs/ui"
2
+
3
+ const Divider = ({ className }: { className?: string }) => (
4
+ <div
5
+ className={clx("h-px w-full border-b border-gray-200 mt-1", className)}
6
+ />
7
+ )
8
+
9
+ export default Divider
@@ -0,0 +1,13 @@
1
+ const ErrorMessage = ({ error, 'data-testid': dataTestid }: { error?: string | null, 'data-testid'?: string }) => {
2
+ if (!error) {
3
+ return null
4
+ }
5
+
6
+ return (
7
+ <div className="pt-2 text-rose-500 text-small-regular" data-testid={dataTestid}>
8
+ <span>{error}</span>
9
+ </div>
10
+ )
11
+ }
12
+
13
+ export default ErrorMessage
@@ -0,0 +1,134 @@
1
+ import { Heading } from "@medusajs/ui"
2
+ import CheckboxWithLabel from "medusa-ui-common/common/components/checkbox"
3
+ import React from "react"
4
+
5
+ type FilterCheckboxGroupProps = {
6
+ title: string
7
+ items: { value: string; label: string }[]
8
+ values: string[]
9
+ handleChange: (value: string) => void
10
+ isColor?: boolean
11
+ 'data-testid'?: string
12
+ }
13
+
14
+ const FilterCheckboxGroup = ({
15
+ title,
16
+ items,
17
+ values,
18
+ handleChange,
19
+ isColor,
20
+ 'data-testid': dataTestId,
21
+ }: FilterCheckboxGroupProps) => {
22
+ const [isOpen, setIsOpen] = React.useState(true)
23
+ const [showAll, setShowAll] = React.useState(false)
24
+
25
+ // Optimistic local state — reflects clicks instantly without waiting for server
26
+ const [optimisticValues, setOptimisticValues] = React.useState<string[]>(values)
27
+
28
+ // Sync back when server props update (after navigation completes)
29
+ React.useEffect(() => {
30
+ setOptimisticValues(values)
31
+ }, [values])
32
+
33
+ const initialItemsCount = 6
34
+ const hasMore = items?.length > initialItemsCount
35
+
36
+ // Auto-expand if a selected item is hidden or if any item is selected
37
+ React.useEffect(() => {
38
+ if (values && values.length > 0) {
39
+ setIsOpen(true)
40
+ if (hasMore && items) {
41
+ const hiddenItems = items.slice(initialItemsCount)
42
+ const isAnyHiddenItemSelected = hiddenItems.some(item => values.includes(item.value))
43
+ if (isAnyHiddenItemSelected) {
44
+ setShowAll(true)
45
+ }
46
+ }
47
+ }
48
+ }, [values, items, hasMore])
49
+
50
+ const handleOptimisticChange = (value: string) => {
51
+ // Immediately toggle locally — no flicker
52
+ setOptimisticValues((prev) =>
53
+ prev.includes(value) ? prev.filter((v) => v !== value) : [...prev, value]
54
+ )
55
+ // Trigger actual URL navigation in background
56
+ handleChange(value)
57
+ }
58
+
59
+ const displayedItems = showAll ? items : items?.slice(0, initialItemsCount)
60
+
61
+ return (
62
+ <div className="flex flex-col border-b border-gray-100 pb-4 last:border-0 last:pb-0">
63
+ <button
64
+ onClick={() => setIsOpen(!isOpen)}
65
+ className="flex items-center justify-between w-full group transition-all py-1"
66
+ >
67
+ <Heading
68
+ level="h3"
69
+ className="text-sm font-bold text-gray-900 uppercase tracking-wider group-hover:text-[#8B5AB1] transition-colors"
70
+ >
71
+ {title}
72
+ </Heading>
73
+ <span
74
+ className={`transform transition-transform duration-300 text-gray-400 group-hover:text-[#8B5AB1] ${isOpen ? "" : "-rotate-180"}`}
75
+ >
76
+ <svg
77
+ width="18"
78
+ height="18"
79
+ viewBox="0 0 24 24"
80
+ fill="none"
81
+ stroke="currentColor"
82
+ strokeWidth="2.5"
83
+ strokeLinecap="round"
84
+ strokeLinejoin="round"
85
+ >
86
+ <polyline points="18 15 12 9 6 15"></polyline>
87
+ </svg>
88
+ </span>
89
+ </button>
90
+
91
+ <div
92
+ className={`flex flex-col gap-y-1 transition-all duration-300 ease-in-out ${isOpen ? "mt-3 opacity-100 px-0.5" : "max-h-0 opacity-0 invisible overflow-hidden"}`}
93
+ style={isOpen ? { maxHeight: showAll ? '1000px' : '300px' } : {}}
94
+ >
95
+ {displayedItems?.map((i) => (
96
+ <CheckboxWithLabel
97
+ key={i.value}
98
+ label={i.label}
99
+ color={isColor ? i.value : undefined}
100
+ checked={optimisticValues.includes(i.value)}
101
+ onChange={() => handleOptimisticChange(i.value)}
102
+ data-testid={dataTestId}
103
+ data-ga-event="filter_checkbox_click"
104
+ data-ga-label={`${title} - ${i.label}`}
105
+ />
106
+ ))}
107
+
108
+ {hasMore && isOpen && (
109
+ <button
110
+ onClick={() => setShowAll(!showAll)}
111
+ className="text-xs font-bold text-[#8B5AB1] hover:underline mt-2 flex items-center gap-1 uppercase tracking-tight"
112
+ >
113
+ {showAll ? "View Less" : `View More (${items.length - initialItemsCount})`}
114
+ <svg
115
+ width="12"
116
+ height="12"
117
+ viewBox="0 0 24 24"
118
+ fill="none"
119
+ stroke="currentColor"
120
+ strokeWidth="3"
121
+ strokeLinecap="round"
122
+ strokeLinejoin="round"
123
+ className={`transform transition-transform ${showAll ? "rotate-180" : ""}`}
124
+ >
125
+ <polyline points="6 9 12 15 18 9"></polyline>
126
+ </svg>
127
+ </button>
128
+ )}
129
+ </div>
130
+ </div>
131
+ )
132
+ }
133
+
134
+ export default FilterCheckboxGroup
@@ -0,0 +1,62 @@
1
+ import { EllipseMiniSolid } from "@medusajs/icons"
2
+ import { Label, RadioGroup, Text, clx } from "@medusajs/ui"
3
+
4
+ type FilterRadioGroupProps = {
5
+ title: string
6
+ items: {
7
+ value: string
8
+ label: string
9
+ }[]
10
+ value: any
11
+ handleChange: (...args: any[]) => void
12
+ "data-testid"?: string
13
+ }
14
+
15
+ const FilterRadioGroup = ({
16
+ title,
17
+ items,
18
+ value,
19
+ handleChange,
20
+ "data-testid": dataTestId,
21
+ }: FilterRadioGroupProps) => {
22
+ return (
23
+ <div className="flex gap-x-3 flex-col gap-y-3">
24
+ <Text className="txt-compact-small-plus text-ui-fg-muted">{title}</Text>
25
+ <RadioGroup data-testid={dataTestId} onValueChange={handleChange}>
26
+ {items?.map((i) => (
27
+ <div
28
+ key={i.value}
29
+ className={clx("flex gap-x-2 items-center", {
30
+ "ml-[-23px]": i.value === value,
31
+ })}
32
+ >
33
+ {i.value === value && <EllipseMiniSolid />}
34
+ <RadioGroup.Item
35
+ checked={i.value === value}
36
+ className="hidden peer"
37
+ id={i.value}
38
+ value={i.value}
39
+ />
40
+ <Label
41
+ htmlFor={i.value}
42
+ className={clx(
43
+ "!txt-compact-small !transform-none text-ui-fg-subtle hover:cursor-pointer",
44
+ {
45
+ "text-ui-fg-base": i.value === value,
46
+ }
47
+ )}
48
+ data-testid="radio-label"
49
+ data-active={i.value === value}
50
+ data-ga-event="filter_sort_click"
51
+ data-ga-label={i.label}
52
+ >
53
+ {i.label}
54
+ </Label>
55
+ </div>
56
+ ))}
57
+ </RadioGroup>
58
+ </div>
59
+ )
60
+ }
61
+
62
+ export default FilterRadioGroup
@@ -0,0 +1,79 @@
1
+ import { Label } from "@medusajs/ui"
2
+ import React, { useEffect, useImperativeHandle, useState } from "react"
3
+
4
+ import Eye from "medusa-ui-common/common/icons/eye"
5
+ import EyeOff from "medusa-ui-common/common/icons/eye-off"
6
+
7
+ type InputProps = Omit<
8
+ Omit<React.InputHTMLAttributes<HTMLInputElement>, "size">,
9
+ "placeholder"
10
+ > & {
11
+ label: string
12
+ errors?: Record<string, unknown>
13
+ touched?: Record<string, unknown>
14
+ name: string
15
+ topLabel?: string
16
+ }
17
+
18
+ const Input = React.forwardRef<HTMLInputElement, InputProps>(
19
+ ({ type, name, label, touched, required, topLabel, ...props }, ref) => {
20
+ const inputRef = React.useRef<HTMLInputElement>(null)
21
+ const [showPassword, setShowPassword] = useState(false)
22
+ const [inputType, setInputType] = useState(type)
23
+
24
+ useEffect(() => {
25
+ if (type === "password" && showPassword) {
26
+ setInputType("text")
27
+ }
28
+
29
+ if (type === "password" && !showPassword) {
30
+ setInputType("password")
31
+ }
32
+ }, [type, showPassword])
33
+
34
+ useImperativeHandle(ref, () => inputRef.current!)
35
+
36
+ return (
37
+ <div className="flex flex-col w-full">
38
+ {topLabel && (
39
+ <Label className="mb-2 txt-compact-medium-plus">{topLabel}</Label>
40
+ )}
41
+ <div className="flex relative z-0 w-full txt-compact-medium overflow-visible">
42
+ <input
43
+ type={inputType}
44
+ name={name}
45
+ placeholder=" "
46
+ required={required}
47
+ className="peer pt-6 pb-2 block w-full h-12 px-4 mt-0 bg-white border rounded-md appearance-none focus:outline-none focus:border-[#8B5AB1] focus:ring-0 hover:bg-gray-50 transition-colors duration-200 antialiased"
48
+ style={{ borderColor: '#C0C0C0' }}
49
+ {...props}
50
+ ref={inputRef}
51
+ />
52
+ {label && (
53
+ <label
54
+ htmlFor={name}
55
+ onClick={() => inputRef.current?.focus()}
56
+ className="flex items-center justify-center absolute left-4 px-1 transition-all duration-300 top-1/2 -translate-y-1/2 origin-0 text-ui-fg-subtle pointer-events-none peer-focus:top-2 peer-focus:translate-y-0 peer-focus:text-[11px] peer-focus:text-[#8B5AB1] peer-focus:z-10 peer-[:not(:placeholder-shown)]:top-2 peer-[:not(:placeholder-shown)]:translate-y-0 peer-[:not(:placeholder-shown)]:text-[11px] peer-[:not(:placeholder-shown)]:z-10"
57
+ >
58
+ {label}
59
+ {required && <span className="text-rose-500">*</span>}
60
+ </label>
61
+ )}
62
+ {type === "password" && (
63
+ <button
64
+ type="button"
65
+ onClick={() => setShowPassword(!showPassword)}
66
+ className="text-ui-fg-subtle px-4 focus:outline-none transition-all duration-150 outline-none focus:text-ui-fg-base absolute right-0 top-3"
67
+ >
68
+ {showPassword ? <Eye /> : <EyeOff />}
69
+ </button>
70
+ )}
71
+ </div>
72
+ </div>
73
+ )
74
+ }
75
+ )
76
+
77
+ Input.displayName = "Input"
78
+
79
+ export default Input