@colabcommerce/elements 0.9.1 → 0.9.3

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 (97) hide show
  1. package/dist/QuoteForm.js +24 -0
  2. package/dist/Store.js +37 -0
  3. package/dist/StoreLocator.js +34 -0
  4. package/dist/StoreLocatorProvider.js +1 -0
  5. package/dist/elements.css +1 -0
  6. package/dist/index-DvX0QvFh.js +31 -0
  7. package/dist/navigation-DpGLbcKb.js +1 -0
  8. package/dist/store-locator-Bto20jHS.js +1 -0
  9. package/dist/styles.js +1 -0
  10. package/dist/translations-6mspyPRw.js +1 -0
  11. package/dist/useStoreLocator.js +1 -0
  12. package/dist/useStoreLocatorConfig-r4Y_2t-M.js +1 -0
  13. package/package.json +5 -1
  14. package/.pnp.cjs +0 -16484
  15. package/.pnp.loader.mjs +0 -2126
  16. package/.yarn/install-state.gz +0 -0
  17. package/.yarn/releases/yarn-4.12.0.cjs +0 -942
  18. package/.yarnrc.yml +0 -1
  19. package/cypress/fixtures/example.json +0 -5
  20. package/cypress/support/commands.js +0 -25
  21. package/cypress/support/component-index.html +0 -15
  22. package/cypress/support/component.js +0 -26
  23. package/eslint.config.js +0 -32
  24. package/index.html +0 -13
  25. package/playground/index.html +0 -14
  26. package/playground/main.jsx +0 -36
  27. package/src/App.css +0 -0
  28. package/src/App.jsx +0 -65
  29. package/src/components/CollapsibleStoreHours/index.jsx +0 -269
  30. package/src/components/HoursList/index.jsx +0 -225
  31. package/src/components/LeadForm/index.jsx +0 -241
  32. package/src/components/MessageDialog/index.jsx +0 -169
  33. package/src/components/QuoteForm/index.jsx +0 -82
  34. package/src/components/QuoteFormSearch/index.jsx +0 -276
  35. package/src/components/QuoteFormStoreList/index.jsx +0 -65
  36. package/src/components/QuoteFormStoreListItem/index.jsx +0 -134
  37. package/src/components/QuoteLeadForm/index.jsx +0 -16
  38. package/src/components/QuoteMap/index.jsx +0 -96
  39. package/src/components/QuoteMapMarker/index.jsx +0 -56
  40. package/src/components/StaticMap/index.jsx +0 -24
  41. package/src/components/Store/index.jsx +0 -44
  42. package/src/components/StoreContact/index.jsx +0 -96
  43. package/src/components/StoreInfo/index.jsx +0 -50
  44. package/src/components/StoreList/index.jsx +0 -59
  45. package/src/components/StoreListItem/index.jsx +0 -99
  46. package/src/components/StoreListItem/indexStoreListItem.cy.jsx +0 -30
  47. package/src/components/StoreListNoneFound/index.jsx +0 -16
  48. package/src/components/StoreLocator/index.jsx +0 -43
  49. package/src/components/StoreLocatorMap/index.jsx +0 -93
  50. package/src/components/StoreLocatorMapMarker/index.jsx +0 -55
  51. package/src/components/StoreLocatorMessageDialog/index.jsx +0 -20
  52. package/src/components/StoreLocatorSearch/index.jsx +0 -316
  53. package/src/components/StoreMap/index.jsx +0 -30
  54. package/src/components/StoreMeta/index.jsx +0 -7
  55. package/src/components/StoreProducts/index.jsx +0 -112
  56. package/src/components/ui/Badge/index.jsx +0 -46
  57. package/src/components/ui/Button/index.jsx +0 -56
  58. package/src/components/ui/Button/indexButton.cy.jsx +0 -9
  59. package/src/components/ui/Card/index.jsx +0 -90
  60. package/src/components/ui/Input/index.jsx +0 -19
  61. package/src/components/ui/Input/indexInput.cy.jsx +0 -9
  62. package/src/components/ui/LoadingPuff/index.jsx +0 -10
  63. package/src/components/ui/Panel/index.jsx +0 -23
  64. package/src/components/ui/PhoneNumberInput/index.jsx +0 -17
  65. package/src/contexts/quote-form.jsx +0 -94
  66. package/src/contexts/store-locator.jsx +0 -83
  67. package/src/contexts/store.jsx +0 -59
  68. package/src/contexts/translations.jsx +0 -11
  69. package/src/dist.css +0 -229
  70. package/src/entries/QuoteForm.js +0 -2
  71. package/src/entries/Store.js +0 -2
  72. package/src/entries/StoreLocator.js +0 -2
  73. package/src/entries/StoreLocatorProvider.js +0 -2
  74. package/src/entries/styles.js +0 -2
  75. package/src/entries/useStoreLocator.js +0 -2
  76. package/src/i18n/defaultResources.js +0 -19
  77. package/src/i18n/index.js +0 -44
  78. package/src/i18n/mergeResources.js +0 -22
  79. package/src/index.css +0 -214
  80. package/src/lib/addressComponentsToAddress.js +0 -43
  81. package/src/lib/productSchema.js +0 -6
  82. package/src/lib/useGeolocation.js +0 -266
  83. package/src/lib/useHours.js +0 -205
  84. package/src/lib/usePlacesAutocomplete.js +0 -288
  85. package/src/lib/useProductAvailability.js +0 -38
  86. package/src/lib/useRudderAnalytics.js +0 -50
  87. package/src/lib/useSearchResults.js +0 -102
  88. package/src/lib/useStoreLocatorConfig.js +0 -50
  89. package/src/lib/utils/cn.js +0 -6
  90. package/src/lib/utils/measure.js +0 -31
  91. package/src/locales/en/default.json +0 -58
  92. package/src/locales/es/default.json +0 -58
  93. package/src/locales/fr/default.json +0 -58
  94. package/src/locales/it/default.json +0 -58
  95. package/src/main.jsx +0 -10
  96. package/vite.config.js +0 -67
  97. /package/{public → dist}/vite.svg +0 -0
@@ -1,55 +0,0 @@
1
- import { useState, useCallback } from 'react'
2
- import { MapPin, Navigation } from 'lucide-react'
3
- import { AdvancedMarker, InfoWindow, useAdvancedMarkerRef } from '@vis.gl/react-google-maps'
4
- import { Button } from "@/components/ui/button"
5
- import { info } from 'autoprefixer'
6
-
7
- const StoreLocatorMapMarker = ({ store, isSelected, onClick }) => {
8
-
9
- const [infoWindowShown, setInfoWindowShown] = useState(false)
10
- const [markerRef, marker] = useAdvancedMarkerRef()
11
-
12
- const handleMarkerClick = useCallback(() => {
13
- onClick && onClick(store.id)
14
- }, [store.id, onClick])
15
-
16
- const handleClose = useCallback(() => setInfoWindowShown(false), [])
17
-
18
- const showInfoWindow = isSelected
19
-
20
- return (
21
- <AdvancedMarker
22
- position={{
23
- lat: store.address?.latitude * 1,
24
- lng: store.address?.longitude * 1,
25
- }}
26
- ref={markerRef}
27
- title={store.name}
28
- onClick={handleMarkerClick}
29
- >
30
- <MapPin className={`w-8 h-8 hover:cursor-pointer ${isSelected ? 'text-primary fill-white z-10' : 'z-1 fill-white text-muted-foreground'}`} size={32} />
31
- {showInfoWindow && (
32
- <InfoWindow anchor={marker} headerContent={<h3 className="font-bold text-sm align-center text-left mb-2">{store.retailer_name}</h3>} onClose={handleClose}>
33
- <p className="text-sm text-left text-muted-foreground">
34
- {store.address?.street_line_one}
35
- {store.address?.street_line_two ? `, ${store.address?.street_line_two}` : ''}
36
- </p>
37
- <p className="text-sm text-left text-muted-foreground mb-4">
38
- {store.address?.city}, {store.address?.province} {store.address?.postal_code}
39
- </p>
40
- <div className="flex flex-row gap-2 mb-1">
41
- <Button className="flex-1 md:flex-row" size="sm">
42
- <Navigation className="w-4 h-4" />
43
- Directions
44
- </Button>
45
- <Button variant="outline" className="flex-1 md:flex-none bg-transparent" size="sm">
46
- View Details
47
- </Button>
48
- </div>
49
- </InfoWindow>
50
- )}
51
- </AdvancedMarker>
52
- )
53
- }
54
-
55
- export default StoreLocatorMapMarker
@@ -1,20 +0,0 @@
1
- import MessageDialog from "@/components/MessageDialog"
2
- import { useStoreLocator } from "@/contexts/store-locator"
3
-
4
- const StoreLocatorMessageDialog = () => {
5
-
6
- const { messageDialogOpen, setMessageDialogOpen, messageStoreId, setMessageStoreId, stores } = useStoreLocator()
7
-
8
- const store = stores.find(store => store.id === messageStoreId)
9
- const storeName = store ? store.retailer_name : null
10
-
11
- const handleClose = () => {
12
- setMessageDialogOpen(false)
13
- setMessageStoreId(null)
14
- }
15
- return (
16
- <MessageDialog isOpen={messageDialogOpen} onClose={handleClose} storeName={storeName} />
17
- )
18
- }
19
-
20
- export default StoreLocatorMessageDialog
@@ -1,316 +0,0 @@
1
- "use client"
2
-
3
- import { useEffect, useId, useMemo, useRef, useState } from "react"
4
- import { Search, MapPin, Sliders } from "lucide-react"
5
- import * as Popover from "@radix-ui/react-popover"
6
- import * as ScrollArea from "@radix-ui/react-scroll-area"
7
-
8
- import { Button } from "@/components/ui/Button"
9
- import { useStoreLocator } from "@/contexts/store-locator"
10
- import { usePlacesAutocomplete } from "@/lib/usePlacesAutocomplete"
11
- import Input from "@/components/ui/Input"
12
- import { useTranslation } from "react-i18next"
13
-
14
- function getItemLabel(item) {
15
- // Support a few common shapes:
16
- // - Google Places autocomplete style: { placePrediction: { text: { text } } }
17
- // - Simple: { label }, { description }, { text }
18
- return (
19
- item?.label ??
20
- item?.description ??
21
- item?.text ??
22
- item?.placePrediction?.text?.text ??
23
- item?.placePrediction?.structuredFormat?.mainText?.text ??
24
- ""
25
- )
26
- }
27
-
28
- function getItemSecondary(item) {
29
- return (
30
- item?.secondary ??
31
- item?.secondary_text ??
32
- item?.placePrediction?.structuredFormat?.secondaryText?.text ??
33
- item?.placePrediction?.structuredFormat?.secondaryText ??
34
- ""
35
- )
36
- }
37
-
38
- function getItemId(item, fallback) {
39
- return (
40
- item?.id ??
41
- item?.place_id ??
42
- item?.placePrediction?.placeId ??
43
- fallback
44
- )
45
- }
46
-
47
- export function StoreLocatorSearch() {
48
-
49
- const { t } = useTranslation()
50
- const [searchQuery, setSearchQuery] = useState("")
51
- const [showFilters, setShowFilters] = useState(false)
52
-
53
- // Radix dropdown state
54
- const [open, setOpen] = useState(false)
55
- const [activeIndex, setActiveIndex] = useState(-1)
56
-
57
- const { location, searchRadius, setSearchRadius, setSearchLocation, setUseLocationType, geoLocation } = useStoreLocator()
58
-
59
- const { results, isLoading, error } = usePlacesAutocomplete({
60
- query: searchQuery,
61
- apiKey: "AIzaSyAnJmWEU1r63DiRWHkjczxzHyIEq3dhj4M",
62
- center: { lat: 37.7937, lng: -122.3965 },
63
- radiusMeters: 10000,
64
- types: ["locality", "postal_code"],
65
- })
66
-
67
- const items = useMemo(() => Array.isArray(results) ? results : [], [results])
68
-
69
- const inputRef = useRef(null)
70
- const listId = useId()
71
- const activeId = activeIndex >= 0 ? `${listId}-opt-${activeIndex}` : undefined
72
-
73
- // When results change while open, reset the highlighted row
74
- useEffect(() => {
75
- if (!open) return
76
- if (isLoading) return
77
- setActiveIndex(items.length ? 0 : -1)
78
- }, [open, isLoading, items.length])
79
-
80
- const commitSelection = (item) => {
81
- const label = getItemLabel(item)
82
- if (!label) return
83
-
84
- setSearchQuery(label)
85
- setOpen(false)
86
-
87
- // Keep focus like Google
88
- requestAnimationFrame(() => inputRef.current?.focus())
89
-
90
- const locationObject = {
91
- "latitude": item.location?.lat ?? null,
92
- "longitude": item.location?.lng ?? null,
93
- "accuracy": null,
94
- "altitude": 0,
95
- "altitudeAccuracy": null,
96
- "heading": null,
97
- "speed": null,
98
- "city": item.city ?? null,
99
- "region": item.region ?? null,
100
- "country": item.country ?? null,
101
- "postal": item.postalCode ?? null,
102
- "timezone": null,
103
- "ip": null,
104
- "type": "search",
105
- "raw": item
106
- }
107
-
108
- setSearchLocation(locationObject)
109
- setUseLocationType('search')
110
-
111
- // If your store locator has a method to actually run the search / set coords,
112
- // this is where you’d call it, e.g.:
113
- // setSelectedPlace(item)
114
- // runSearch({ placeId: ..., radius: searchRadius })
115
- }
116
-
117
- const onKeyDown = (e) => {
118
- // open dropdown with arrows
119
- if (!open && (e.key === "ArrowDown" || e.key === "ArrowUp")) {
120
- setOpen(true)
121
- return
122
- }
123
- if (!open) return
124
-
125
- switch (e.key) {
126
- case "ArrowDown": {
127
- e.preventDefault()
128
- setActiveIndex((i) => Math.min(i + 1, items.length - 1))
129
- break
130
- }
131
- case "ArrowUp": {
132
- e.preventDefault()
133
- setActiveIndex((i) => Math.max(i - 1, 0))
134
- break
135
- }
136
- case "Enter": {
137
- if (activeIndex >= 0 && items[activeIndex]) {
138
- e.preventDefault()
139
- commitSelection(items[activeIndex])
140
- }
141
- break
142
- }
143
- case "Escape": {
144
- e.preventDefault()
145
- setOpen(false)
146
- break
147
- }
148
- default:
149
- break
150
- }
151
- }
152
-
153
- const handleUseCurrentLocation = () => {
154
- setUseLocationType('geolocation')
155
- setOpen(false)
156
- setSearchQuery("")
157
- }
158
-
159
- const showEmpty =
160
- open &&
161
- !isLoading &&
162
- !error &&
163
- searchQuery.trim().length > 0 &&
164
- items.length === 0
165
-
166
- return (
167
- <div className="max-w-3xl mx-auto">
168
- <div className="bg-card rounded-xl shadow-lg p-6 border">
169
- <div className="flex flex-row gap-4">
170
- <Popover.Root open={open} onOpenChange={setOpen}>
171
- <Popover.Anchor asChild>
172
- <div className="flex-1 relative">
173
- <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-muted-foreground" />
174
- <Input
175
- ref={inputRef}
176
- type="text"
177
- placeholder={location?.city ? t('default:location_search_placeholder_w_location', { location: location.city }) : t('default:location_search_placeholder')}
178
- value={searchQuery}
179
- onChange={(e) => {
180
- setSearchQuery(e.target.value)
181
- setOpen(true)
182
- }}
183
- onFocus={() => setOpen(true)}
184
- onKeyDown={onKeyDown}
185
- className="pl-10 h-12"
186
- role="combobox"
187
- aria-autocomplete="list"
188
- aria-expanded={open}
189
- aria-controls={listId}
190
- aria-activedescendant={activeId}
191
- />
192
- </div>
193
- </Popover.Anchor>
194
-
195
- <Popover.Content
196
- sideOffset={6}
197
- align="start"
198
- onOpenAutoFocus={(e) => e.preventDefault()} // keep focus in input
199
- className="w-[var(--radix-popover-trigger-width)] max-w-[560px] rounded-xl border rounded bg-background shadow-lg overflow-hidden"
200
- >
201
- <ScrollArea.Root className="max-h-80">
202
- <ScrollArea.Viewport>
203
- <div id={listId} role="listbox" className="p-2">
204
- {error ? (
205
- <div className="px-3 py-2 text-sm rounded-md border border-destructive/30 bg-destructive/10 text-destructive">
206
- {String(error)}
207
- </div>
208
- ) : null}
209
-
210
- {isLoading ? (
211
- <div className="px-3 py-2 text-sm text-muted-foreground">
212
- {t('default:loading')}
213
- </div>
214
- ) : null}
215
-
216
- {showEmpty ? (
217
- <div className="px-3 py-2 text-sm text-muted-foreground">
218
- {t('default:no_results')}
219
- </div>
220
- ) : null}
221
-
222
- {!isLoading && !error && items.map((item, idx) => {
223
- const active = idx === activeIndex
224
- const label = getItemLabel(item)
225
- const secondary = getItemSecondary(item)
226
- const id = getItemId(item, `${idx}`)
227
-
228
- if (!label) return null
229
-
230
- return (
231
- <div
232
- key={id}
233
- id={`${listId}-opt-${idx}`}
234
- role="option"
235
- aria-selected={active}
236
- onMouseEnter={() => setActiveIndex(idx)}
237
- onMouseDown={(e) => e.preventDefault()} // keep focus
238
- onClick={() => commitSelection(item)}
239
- className={[
240
- "cursor-pointer rounded-lg px-3 py-2",
241
- active ? "bg-muted" : "hover:bg-muted/60",
242
- ].join(" ")}
243
- >
244
- <div className="text-sm text-left font-medium leading-tight">
245
- {label}
246
- </div>
247
- {secondary ? (
248
- <div className="text-xs text-left text-muted-foreground mt-1 leading-tight">
249
- {secondary}
250
- </div>
251
- ) : null}
252
- </div>
253
- )
254
- })}
255
- </div>
256
- </ScrollArea.Viewport>
257
-
258
- <ScrollArea.Scrollbar orientation="vertical" className="flex select-none touch-none p-0.5 bg-transparent">
259
- <ScrollArea.Thumb className="flex-1 bg-border rounded-full" />
260
- </ScrollArea.Scrollbar>
261
- </ScrollArea.Root>
262
- </Popover.Content>
263
- </Popover.Root>
264
-
265
- <Button
266
- variant="outline"
267
- size="lg"
268
- onClick={() => setShowFilters(!showFilters)}
269
- className="w-auto h-12"
270
- type="button"
271
- >
272
- <Sliders className="w-5 h-5" />
273
- </Button>
274
- </div>
275
-
276
- {showFilters && (
277
- <div className="mt-6 pt-6 border-t grid md:grid-cols-2 gap-4">
278
- <div>
279
- <label className="text-sm font-medium mb-2 block">Distance</label>
280
- <select
281
- value={searchRadius}
282
- onChange={(e) => setSearchRadius(e.target.value)}
283
- className="w-full h-10 rounded-md border border-input bg-background px-3 text-sm"
284
- >
285
- <option value="5mi">Within 5 miles</option>
286
- <option value="10mi">Within 10 miles</option>
287
- <option value="25mi">Within 25 miles</option>
288
- <option value="50mi">Within 50 miles</option>
289
- <option value="100mi">Within 100 miles</option>
290
- </select>
291
- </div>
292
- <div>
293
- <label className="text-sm font-medium mb-2 block">Retailer Type</label>
294
- <select className="w-full h-10 rounded-md border border-input bg-background px-3 text-sm">
295
- <option>All Types</option>
296
- <option>Specialty Stores</option>
297
- <option>Big Box Retailers</option>
298
- <option>Independent Dealers</option>
299
- <option>Authorized Distributors</option>
300
- </select>
301
- </div>
302
- </div>
303
- )}
304
- </div>
305
-
306
- <div className="mt-6 flex items-center justify-center gap-2 text-sm text-muted-foreground">
307
- <MapPin className="w-4 h-4" />
308
- <button className="text-primary hover:underline font-medium" type="button" onClick={handleUseCurrentLocation}>
309
- {t('default:use_my_location')}
310
- </button>
311
- </div>
312
- </div>
313
- )
314
- }
315
-
316
- export default StoreLocatorSearch
@@ -1,30 +0,0 @@
1
- import { MapPin, Navigation } from "lucide-react"
2
- import { Button } from "@/components/ui/button"
3
- import { Card } from "@/components/ui/card"
4
- import StaticMap from "@/components/StaticMap"
5
- import { useStore } from "@/contexts/store"
6
-
7
- export function StoreMap() {
8
- const { store } = useStore()
9
- return (
10
- <Card className="p-0 overflow-hidden sticky top-24">
11
- <div className="relative w-full h-[500px] bg-gradient-to-br from-primary/10 to-primary/5">
12
- <StaticMap
13
- center={`${store?.address?.latitude},${store?.address?.longitude}`}
14
- zoom={15}
15
- markers={[{ lat: store?.address?.latitude, lng: store?.address?.longitude }]}
16
- />
17
- <div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 flex items-center justify-center">
18
- <div className="text-center">
19
- <Button variant="outline" size="sm">
20
- <Navigation className="w-4 h-4 mr-2" />
21
- View on Google Maps
22
- </Button>
23
- </div>
24
- </div>
25
- </div>
26
- </Card>
27
- )
28
- }
29
-
30
- export default StoreMap
@@ -1,7 +0,0 @@
1
- const StoreMeta = () => {
2
- return (
3
- <div>StoreMeta</div>
4
- )
5
- }
6
-
7
- export default StoreMeta
@@ -1,112 +0,0 @@
1
- "use client"
2
-
3
- import { useState } from "react"
4
- import { Search, SlidersHorizontal, Heart, Eye } from "lucide-react"
5
- import Input from "@/components/ui/input"
6
- import { Button } from "@/components/ui/button"
7
- import { Badge } from "@/components/ui/badge"
8
- import { Card } from "@/components/ui/card"
9
- import { useStore } from "@/contexts/store"
10
-
11
- export function StoreProducts() {
12
- const [searchQuery, setSearchQuery] = useState("")
13
- const [selectedCategory, setSelectedCategory] = useState("all")
14
-
15
- const { store } = useStore()
16
-
17
- const products = store?.retailer_location_products || []
18
- const categories = [{ id: "all", label: "All Products" }, ...Array.from(new Set(products.map(p => p.category))).map(cat => ({ id: cat, label: cat.charAt(0).toUpperCase() + cat.slice(1) }))]
19
-
20
- const filteredProducts = products.filter((product) => {
21
- const matchesSearch = product.name.toLowerCase().includes(searchQuery.toLowerCase())
22
- const matchesCategory = selectedCategory === "all" || product.category === selectedCategory
23
- return matchesSearch && matchesCategory
24
- })
25
-
26
- return (
27
- <div>
28
- <div className="mb-8">
29
- <h2 className="text-3xl font-bold mb-4">Available Products</h2>
30
- <p className="text-muted-foreground mb-6">
31
- Browse our current inventory of quality furniture pieces. All items shown are available for immediate viewing
32
- at our showroom.
33
- </p>
34
-
35
- {/* Search and Filter */}
36
- <div className="flex flex-col sm:flex-row gap-4 mb-6">
37
- <div className="relative flex-1">
38
- <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-muted-foreground" />
39
- <Input
40
- type="text"
41
- placeholder="Search products..."
42
- value={searchQuery}
43
- onChange={(e) => setSearchQuery(e.target.value)}
44
- className="pl-10"
45
- />
46
- </div>
47
- </div>
48
-
49
- {/* Category Filters */}
50
- <div className="flex flex-wrap gap-2">
51
- {categories.map((category) => (
52
- <Button
53
- key={category.id}
54
- variant={selectedCategory === category.id ? "default" : "outline"}
55
- size="sm"
56
- onClick={() => setSelectedCategory(category.id)}
57
- >
58
- {category.label}
59
- </Button>
60
- ))}
61
- </div>
62
- </div>
63
-
64
- {/* Products Grid */}
65
- <div className="grid sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
66
- {filteredProducts.map((product) => (
67
- <Card key={product.id} className="group overflow-hidden">
68
- <div className="relative bg-muted overflow-hidden">
69
- <img
70
- src={product.images?.[0] || "/placeholder.svg"}
71
- alt={product.name}
72
- fill
73
- className="object-cover group-hover:scale-105 transition-transform duration-300"
74
- />
75
- {product.available && (
76
- <Badge className="absolute top-3 left-3 bg-primary text-primary-foreground">Special Order</Badge>
77
- )}
78
- {!product.stocked && <Badge className="absolute top-3 left-3 bg-red-600 text-white">Out of Stock</Badge>}
79
- <div className="absolute top-3 right-3 flex flex-col gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
80
- <Button size="icon" variant="secondary" className="h-9 w-9">
81
- <Heart className="w-4 h-4" />
82
- </Button>
83
- <Button size="icon" variant="secondary" className="h-9 w-9">
84
- <Eye className="w-4 h-4" />
85
- </Button>
86
- </div>
87
- </div>
88
- <div className="p-4">
89
- <h3 className="font-semibold mb-1 group-hover:text-primary transition-colors">{product.name}</h3>
90
- <p className="text-sm text-muted-foreground mb-3 line-clamp-2">{product.description}</p>
91
- <div className="flex items-center justify-between">
92
- <span className="text-2xl font-bold text-primary">{product?.price && product.price.toLocaleString()}</span>
93
-
94
- <Button size="sm" disabled={!product.stocked}>
95
- {product.stocked ? "View Details" : "Unavailable"}
96
- </Button>
97
- </div>
98
- </div>
99
- </Card>
100
- ))}
101
- </div>
102
-
103
- {filteredProducts.length === 0 && (
104
- <div className="text-center py-12">
105
- <p className="text-lg text-muted-foreground">No products found matching your criteria.</p>
106
- </div>
107
- )}
108
-
109
- </div>
110
- )
111
- }
112
- export default StoreProducts
@@ -1,46 +0,0 @@
1
- import { Slot } from '@radix-ui/react-slot'
2
- import { cva } from 'class-variance-authority'
3
-
4
- import cn from '@/lib/utils/cn'
5
-
6
- const badgeVariants = cva(
7
- 'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden',
8
- {
9
- variants: {
10
- variant: {
11
- default:
12
- 'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
13
- secondary:
14
- 'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
15
- destructive:
16
- 'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
17
- outline:
18
- 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
19
- success:
20
- 'border-transparent bg-success text-success-foreground [a&]:hover:bg-success/90',
21
- },
22
- },
23
- defaultVariants: {
24
- variant: 'default',
25
- },
26
- },
27
- )
28
-
29
- function Badge({
30
- className,
31
- variant,
32
- asChild = false,
33
- ...props
34
- }) {
35
- const Comp = asChild ? Slot : 'span'
36
-
37
- return (
38
- <Comp
39
- data-slot="badge"
40
- className={cn(badgeVariants({ variant }), className)}
41
- {...props}
42
- />
43
- )
44
- }
45
-
46
- export { Badge, badgeVariants }
@@ -1,56 +0,0 @@
1
- import { Slot } from '@radix-ui/react-slot'
2
- import { cva } from 'class-variance-authority'
3
-
4
- import cn from '@/lib/utils/cn'
5
-
6
- const buttonVariants = cva(
7
- "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
8
- {
9
- variants: {
10
- variant: {
11
- default: 'bg-primary text-primary-foreground hover:bg-primary/90',
12
- destructive:
13
- 'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
14
- outline:
15
- 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
16
- secondary:
17
- 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
18
- ghost:
19
- 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
20
- link: 'text-primary underline-offset-4 hover:underline',
21
- },
22
- size: {
23
- default: 'h-9 px-4 py-2 has-[>svg]:px-3',
24
- sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
25
- lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
26
- icon: 'size-9',
27
- 'icon-sm': 'size-8',
28
- 'icon-lg': 'size-10',
29
- },
30
- },
31
- defaultVariants: {
32
- variant: 'default',
33
- size: 'default',
34
- },
35
- },
36
- )
37
-
38
- const Button = ({
39
- className,
40
- variant,
41
- size,
42
- asChild = false,
43
- ...props
44
- }) => {
45
- const Comp = asChild ? Slot : 'button'
46
-
47
- return (
48
- <Comp
49
- data-slot="button"
50
- className={cn(buttonVariants({ variant, size, className }))}
51
- {...props}
52
- />
53
- )
54
- }
55
-
56
- export { Button, buttonVariants }
@@ -1,9 +0,0 @@
1
- import React from 'react'
2
- import { Button } from './index'
3
-
4
- describe('<Button />', () => {
5
- it('renders', () => {
6
- // see: https://on.cypress.io/mounting-react
7
- cy.mount(<Button>Hello World</Button>)
8
- })
9
- })