@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.
- package/dist/QuoteForm.js +24 -0
- package/dist/Store.js +37 -0
- package/dist/StoreLocator.js +34 -0
- package/dist/StoreLocatorProvider.js +1 -0
- package/dist/elements.css +1 -0
- package/dist/index-DvX0QvFh.js +31 -0
- package/dist/navigation-DpGLbcKb.js +1 -0
- package/dist/store-locator-Bto20jHS.js +1 -0
- package/dist/styles.js +1 -0
- package/dist/translations-6mspyPRw.js +1 -0
- package/dist/useStoreLocator.js +1 -0
- package/dist/useStoreLocatorConfig-r4Y_2t-M.js +1 -0
- package/package.json +5 -1
- package/.pnp.cjs +0 -16484
- package/.pnp.loader.mjs +0 -2126
- package/.yarn/install-state.gz +0 -0
- package/.yarn/releases/yarn-4.12.0.cjs +0 -942
- package/.yarnrc.yml +0 -1
- package/cypress/fixtures/example.json +0 -5
- package/cypress/support/commands.js +0 -25
- package/cypress/support/component-index.html +0 -15
- package/cypress/support/component.js +0 -26
- package/eslint.config.js +0 -32
- package/index.html +0 -13
- package/playground/index.html +0 -14
- package/playground/main.jsx +0 -36
- package/src/App.css +0 -0
- package/src/App.jsx +0 -65
- package/src/components/CollapsibleStoreHours/index.jsx +0 -269
- package/src/components/HoursList/index.jsx +0 -225
- package/src/components/LeadForm/index.jsx +0 -241
- package/src/components/MessageDialog/index.jsx +0 -169
- package/src/components/QuoteForm/index.jsx +0 -82
- package/src/components/QuoteFormSearch/index.jsx +0 -276
- package/src/components/QuoteFormStoreList/index.jsx +0 -65
- package/src/components/QuoteFormStoreListItem/index.jsx +0 -134
- package/src/components/QuoteLeadForm/index.jsx +0 -16
- package/src/components/QuoteMap/index.jsx +0 -96
- package/src/components/QuoteMapMarker/index.jsx +0 -56
- package/src/components/StaticMap/index.jsx +0 -24
- package/src/components/Store/index.jsx +0 -44
- package/src/components/StoreContact/index.jsx +0 -96
- package/src/components/StoreInfo/index.jsx +0 -50
- package/src/components/StoreList/index.jsx +0 -59
- package/src/components/StoreListItem/index.jsx +0 -99
- package/src/components/StoreListItem/indexStoreListItem.cy.jsx +0 -30
- package/src/components/StoreListNoneFound/index.jsx +0 -16
- package/src/components/StoreLocator/index.jsx +0 -43
- package/src/components/StoreLocatorMap/index.jsx +0 -93
- package/src/components/StoreLocatorMapMarker/index.jsx +0 -55
- package/src/components/StoreLocatorMessageDialog/index.jsx +0 -20
- package/src/components/StoreLocatorSearch/index.jsx +0 -316
- package/src/components/StoreMap/index.jsx +0 -30
- package/src/components/StoreMeta/index.jsx +0 -7
- package/src/components/StoreProducts/index.jsx +0 -112
- package/src/components/ui/Badge/index.jsx +0 -46
- package/src/components/ui/Button/index.jsx +0 -56
- package/src/components/ui/Button/indexButton.cy.jsx +0 -9
- package/src/components/ui/Card/index.jsx +0 -90
- package/src/components/ui/Input/index.jsx +0 -19
- package/src/components/ui/Input/indexInput.cy.jsx +0 -9
- package/src/components/ui/LoadingPuff/index.jsx +0 -10
- package/src/components/ui/Panel/index.jsx +0 -23
- package/src/components/ui/PhoneNumberInput/index.jsx +0 -17
- package/src/contexts/quote-form.jsx +0 -94
- package/src/contexts/store-locator.jsx +0 -83
- package/src/contexts/store.jsx +0 -59
- package/src/contexts/translations.jsx +0 -11
- package/src/dist.css +0 -229
- package/src/entries/QuoteForm.js +0 -2
- package/src/entries/Store.js +0 -2
- package/src/entries/StoreLocator.js +0 -2
- package/src/entries/StoreLocatorProvider.js +0 -2
- package/src/entries/styles.js +0 -2
- package/src/entries/useStoreLocator.js +0 -2
- package/src/i18n/defaultResources.js +0 -19
- package/src/i18n/index.js +0 -44
- package/src/i18n/mergeResources.js +0 -22
- package/src/index.css +0 -214
- package/src/lib/addressComponentsToAddress.js +0 -43
- package/src/lib/productSchema.js +0 -6
- package/src/lib/useGeolocation.js +0 -266
- package/src/lib/useHours.js +0 -205
- package/src/lib/usePlacesAutocomplete.js +0 -288
- package/src/lib/useProductAvailability.js +0 -38
- package/src/lib/useRudderAnalytics.js +0 -50
- package/src/lib/useSearchResults.js +0 -102
- package/src/lib/useStoreLocatorConfig.js +0 -50
- package/src/lib/utils/cn.js +0 -6
- package/src/lib/utils/measure.js +0 -31
- package/src/locales/en/default.json +0 -58
- package/src/locales/es/default.json +0 -58
- package/src/locales/fr/default.json +0 -58
- package/src/locales/it/default.json +0 -58
- package/src/main.jsx +0 -10
- package/vite.config.js +0 -67
- /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,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 }
|