@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,82 +0,0 @@
|
|
|
1
|
-
import { useState } from "react"
|
|
2
|
-
import { QuoteFormProvider, useQuoteForm } from "@/contexts/quote-form"
|
|
3
|
-
import QuoteFormSearch from "@/components/QuoteFormSearch"
|
|
4
|
-
import QuoteMap from "@/components/QuoteMap"
|
|
5
|
-
import QuoteFormStoreList from "@/components/QuoteFormStoreList"
|
|
6
|
-
import TranslationsProvider from "@/contexts/translations"
|
|
7
|
-
import QuoteLeadForm from "@/components/QuoteLeadForm"
|
|
8
|
-
import { APIProvider } from "@vis.gl/react-google-maps"
|
|
9
|
-
import Panel from "@/components/ui/Panel"
|
|
10
|
-
import { Button } from "@/components/ui/Button"
|
|
11
|
-
import cn from "@/lib/utils/cn"
|
|
12
|
-
|
|
13
|
-
const QuoteFormPanels = ({ organizationId, products, locale, onClose, onSuccess }) => {
|
|
14
|
-
|
|
15
|
-
const { stepIndex: panelIndex, setStepIndex } = useQuoteForm()
|
|
16
|
-
|
|
17
|
-
const handleClose = () => {
|
|
18
|
-
onClose && onClose()
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const handleSuccess = (emittedValues) => {
|
|
22
|
-
setStepIndex(2)
|
|
23
|
-
onSuccess && onSuccess(emittedValues)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
return (
|
|
27
|
-
<div className="relative overflow-hidden h-[100%] py-1">
|
|
28
|
-
<div className="h-[100%] relative">
|
|
29
|
-
<Panel active={panelIndex === 0} className={cn(
|
|
30
|
-
"data-[state=open]:opacity-100 data-[state=open]:translate-x-0",
|
|
31
|
-
"data-[state=closed]:opacity-0 data-[state=closed]:-translate-x-6"
|
|
32
|
-
)}>
|
|
33
|
-
<div className="relative px-1 h-screen overflow-y-scroll">
|
|
34
|
-
<QuoteFormSearch />
|
|
35
|
-
<div className="pb-8">
|
|
36
|
-
<APIProvider apiKey={'AIzaSyAnJmWEU1r63DiRWHkjczxzHyIEq3dhj4M'}>
|
|
37
|
-
<QuoteMap />
|
|
38
|
-
</APIProvider>
|
|
39
|
-
</div>
|
|
40
|
-
<QuoteFormStoreList />
|
|
41
|
-
</div>
|
|
42
|
-
</Panel>
|
|
43
|
-
<Panel active={panelIndex === 1} className={cn(
|
|
44
|
-
"data-[state=open]:opacity-100 data-[state=open]:translate-x-0",
|
|
45
|
-
"data-[state=closed]:opacity-0 data-[state=closed]:-translate-x-6",
|
|
46
|
-
"flex items-center"
|
|
47
|
-
)}>
|
|
48
|
-
<div className="px-1">
|
|
49
|
-
<QuoteLeadForm onClose={() => setStepIndex(0)} onSuccess={handleSuccess} />
|
|
50
|
-
</div>
|
|
51
|
-
</Panel>
|
|
52
|
-
<Panel active={panelIndex === 2} className={cn(
|
|
53
|
-
"data-[state=open]:opacity-100 data-[state=open]:translate-x-0",
|
|
54
|
-
"data-[state=closed]:opacity-0 data-[state=closed]:-translate-x-6",
|
|
55
|
-
"flex items-center"
|
|
56
|
-
)}>
|
|
57
|
-
<div className="px-1">
|
|
58
|
-
<div className="text-center py-20 px-4">
|
|
59
|
-
<h2 className="text-2xl font-semibold mb-4">Thank you!</h2>
|
|
60
|
-
<p className="mb-8">Your quote request has been submitted successfully. We will get back to you shortly.</p>
|
|
61
|
-
<Button onClick={handleClose}>Close</Button>
|
|
62
|
-
</div>
|
|
63
|
-
</div>
|
|
64
|
-
</Panel>
|
|
65
|
-
</div>
|
|
66
|
-
</div>
|
|
67
|
-
)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const QuoteForm = ({ organizationId, products, locale, onClose, onSuccess }) => {
|
|
71
|
-
return (
|
|
72
|
-
<TranslationsProvider options={{ lng: locale }}>
|
|
73
|
-
<QuoteFormProvider organizationId={organizationId} products={products} locale={locale}>
|
|
74
|
-
<div className="cc" style={{ height: '100%' }}>
|
|
75
|
-
<QuoteFormPanels organizationId={organizationId} products={products} locale={locale} onClose={onClose} onSuccess={onSuccess} />
|
|
76
|
-
</div>
|
|
77
|
-
</QuoteFormProvider>
|
|
78
|
-
</TranslationsProvider>
|
|
79
|
-
)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export default QuoteForm
|
|
@@ -1,276 +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 { useQuoteForm } from "@/contexts/quote-form"
|
|
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 QuoteFormSearch() {
|
|
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 } = useQuoteForm()
|
|
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="rounded-xl p-6">
|
|
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
|
-
</div>
|
|
265
|
-
<div className="mt-2 flex items-center justify-center gap-2 text-sm text-muted-foreground">
|
|
266
|
-
<MapPin className="w-4 h-4" />
|
|
267
|
-
<button className="text-primary hover:underline font-medium" type="button" onClick={handleUseCurrentLocation}>
|
|
268
|
-
{t('default:use_my_location')}
|
|
269
|
-
</button>
|
|
270
|
-
</div>
|
|
271
|
-
</div>
|
|
272
|
-
</div>
|
|
273
|
-
)
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
export default QuoteFormSearch
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { useState } from "react"
|
|
2
|
-
import { MapPin, Phone, Clock, Navigation, Star } from "lucide-react"
|
|
3
|
-
import { Button } from "@/components/ui/button"
|
|
4
|
-
import StoreListItem from '@/components/QuoteFormStoreListItem'
|
|
5
|
-
import StoreListNoneFound from "@/components/StoreListNoneFound"
|
|
6
|
-
import LoadingPuff from "@/components/ui/LoadingPuff"
|
|
7
|
-
import { distanceBetweenPoints } from '@/lib/utils/measure'
|
|
8
|
-
import { useQuoteForm } from '@/contexts/quote-form'
|
|
9
|
-
|
|
10
|
-
export function QuoteFormStoreList() {
|
|
11
|
-
|
|
12
|
-
const { stores,
|
|
13
|
-
location,
|
|
14
|
-
products,
|
|
15
|
-
setStepIndex,
|
|
16
|
-
selectedStoreId, setSelectedStoreId, setMessageDialogOpen, setMessageStoreId, searchIsLoading, searchError, baseUrl, focusedStoreId, setFocusedStoreId } = useQuoteForm()
|
|
17
|
-
|
|
18
|
-
const handleSelect = (storeId) => {
|
|
19
|
-
setSelectedStoreId(storeId)
|
|
20
|
-
setStepIndex(1)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
return (
|
|
24
|
-
<div className="space-y-4">
|
|
25
|
-
{!searchIsLoading && !searchError && stores.length === 0 && (
|
|
26
|
-
<StoreListNoneFound />
|
|
27
|
-
)}
|
|
28
|
-
{searchIsLoading && (
|
|
29
|
-
<div className="flex items-center justify-center h-48">
|
|
30
|
-
<LoadingPuff />
|
|
31
|
-
</div>
|
|
32
|
-
)}
|
|
33
|
-
{!searchIsLoading && !searchError && stores.map((store) => (
|
|
34
|
-
<StoreListItem
|
|
35
|
-
key={store.id}
|
|
36
|
-
id={store.id}
|
|
37
|
-
name={store.retailer_name}
|
|
38
|
-
placeId={store.place_id}
|
|
39
|
-
addressLineOne={store.address?.street_line_one}
|
|
40
|
-
addressLineTwo={store.address?.street_line_two}
|
|
41
|
-
city={store.address?.city}
|
|
42
|
-
province={store.address?.province}
|
|
43
|
-
postalCode={store.address?.postal_code}
|
|
44
|
-
country={store.address?.country}
|
|
45
|
-
distance={distanceBetweenPoints(location.latitude, location.longitude, store.address?.latitude, store.address?.longitude)}
|
|
46
|
-
phone={store.phone_number}
|
|
47
|
-
hours={store.retailer_location_hours}
|
|
48
|
-
services={[store.store_type]}
|
|
49
|
-
isFocused={focusedStoreId === store.id}
|
|
50
|
-
handleFocus={setFocusedStoreId}
|
|
51
|
-
href={`${baseUrl}/${store.id}`}
|
|
52
|
-
handleSelect={handleSelect}
|
|
53
|
-
locationProducts={store.retailer_location_products}
|
|
54
|
-
requestedProducts={products}
|
|
55
|
-
/>
|
|
56
|
-
))}
|
|
57
|
-
{!searchIsLoading && searchError && (
|
|
58
|
-
<div className="text-red-600 text-center">
|
|
59
|
-
An error occurred while searching for stores. Please try again.
|
|
60
|
-
</div>
|
|
61
|
-
)}
|
|
62
|
-
</div>
|
|
63
|
-
)
|
|
64
|
-
}
|
|
65
|
-
export default QuoteFormStoreList
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import { MapPin, Phone, Clock, Check, CheckCheck, CircleAlert, ChevronDown, Info } from "lucide-react"
|
|
2
|
-
import { Button } from "@/components/ui/button"
|
|
3
|
-
import { Badge } from "@/components/ui/badge"
|
|
4
|
-
import * as Collapsible from "@radix-ui/react-collapsible";
|
|
5
|
-
import { metersToMiles } from "@/lib/utils/measure"
|
|
6
|
-
import CollapsibleStoreHours from "@/components/CollapsibleStoreHours"
|
|
7
|
-
import { useTranslation } from "react-i18next"
|
|
8
|
-
import useProductAvailability from "@/lib/useProductAvailability";
|
|
9
|
-
|
|
10
|
-
const StoreListItem = ({
|
|
11
|
-
id,
|
|
12
|
-
name,
|
|
13
|
-
placeId,
|
|
14
|
-
addressLineOne,
|
|
15
|
-
addressLineTwo,
|
|
16
|
-
city,
|
|
17
|
-
province,
|
|
18
|
-
postalCode,
|
|
19
|
-
country,
|
|
20
|
-
phone,
|
|
21
|
-
hours,
|
|
22
|
-
distance,
|
|
23
|
-
services = [],
|
|
24
|
-
isFocused,
|
|
25
|
-
handleFocus,
|
|
26
|
-
handleSelect,
|
|
27
|
-
requestedProducts = [],
|
|
28
|
-
locationProducts = [],
|
|
29
|
-
}) => {
|
|
30
|
-
const { t } = useTranslation()
|
|
31
|
-
|
|
32
|
-
const directionsUrl = `https://www.google.com/maps/dir/?api=1&destination=${name}&destination_place_id=${placeId}`
|
|
33
|
-
|
|
34
|
-
const { availability, allAvailable, allStocked, noneStocked, noneAvailable, availabilitySummary } = useProductAvailability(locationProducts, requestedProducts)
|
|
35
|
-
|
|
36
|
-
const handleStoreSelected = (e) => {
|
|
37
|
-
e.stopPropagation()
|
|
38
|
-
handleSelect(id)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return (
|
|
42
|
-
<div
|
|
43
|
-
className={`bg-card border rounded-xl p-6 transition-all hover:shadow-lg cursor-pointer ${isFocused ? "ring-2 ring-primary shadow-lg" : ""
|
|
44
|
-
}`}
|
|
45
|
-
onClick={() => handleFocus(id)}
|
|
46
|
-
>
|
|
47
|
-
<div className="flex flex-col md:flex-row md:items-start md:justify-between gap-4">
|
|
48
|
-
<div className="flex-1">
|
|
49
|
-
<div className="flex items-start justify-between gap-4 mb-3">
|
|
50
|
-
<div>
|
|
51
|
-
<h3 className="font-bold text-lg text-left mb-1">{name}</h3>
|
|
52
|
-
</div>
|
|
53
|
-
</div>
|
|
54
|
-
|
|
55
|
-
<div className="space-y-2 text-sm">
|
|
56
|
-
<div className="flex items-start gap-2">
|
|
57
|
-
<MapPin className="w-4 h-4 text-muted-foreground mt-0.5 shrink-0" />
|
|
58
|
-
<div>
|
|
59
|
-
<p className="font-medium text-left">{addressLineOne}</p>
|
|
60
|
-
<p className="text-muted-foreground text-left">{city}, {province} {postalCode}</p>
|
|
61
|
-
<p className="text-primary font-medium mt-1 text-left">{t('default:distance_away', { distance: metersToMiles(distance).toFixed(1), distanceUnit: 'miles' })}</p>
|
|
62
|
-
</div>
|
|
63
|
-
</div>
|
|
64
|
-
|
|
65
|
-
<div className="flex items-top gap-2">
|
|
66
|
-
<div className="mt-2.5">
|
|
67
|
-
<Clock className="w-4 h-4 text-muted-foreground" />
|
|
68
|
-
</div>
|
|
69
|
-
<span className="text-muted-foreground"><CollapsibleStoreHours hours={hours} /></span>
|
|
70
|
-
</div>
|
|
71
|
-
</div>
|
|
72
|
-
|
|
73
|
-
{availabilitySummary === 'allStocked' &&
|
|
74
|
-
<div className="flex items-center shrink mt-3 px-3 py-1 bg-green-50 text-green-600 text-xs font-medium rounded-full">
|
|
75
|
-
<CheckCheck className="w-4 h-4 text-green-600 me-1" />
|
|
76
|
-
All Items Stocked
|
|
77
|
-
</div>
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
{availabilitySummary === 'allAvailable' &&
|
|
81
|
-
<div
|
|
82
|
-
className="flex items-center shrink mt-3 px-3 py-1 bg-orange-50 text-orange-600 text-xs font-medium rounded-full"
|
|
83
|
-
>
|
|
84
|
-
<Info className="w-4 me-1 h-4 text-orange-600" />
|
|
85
|
-
Available by Special Order
|
|
86
|
-
</div>
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
{availabilitySummary === 'mixed' &&
|
|
90
|
-
<div >
|
|
91
|
-
<div className="flex items-center gap-2 mt-4 text-sm">
|
|
92
|
-
<span className="font-bold w-28 text-wrap">Braxton</span>
|
|
93
|
-
<span
|
|
94
|
-
className="flex items-center px-3 py-1 bg-orange-50 text-orange-600 text-xs font-medium rounded-full"
|
|
95
|
-
>
|
|
96
|
-
<Info className="w-4 me-1 h-4 text-orange-600" />
|
|
97
|
-
Special Order
|
|
98
|
-
</span>
|
|
99
|
-
</div>
|
|
100
|
-
<div className="flex items-center gap-2 mt-4 text-sm">
|
|
101
|
-
<span className="font-bold w-28 text-wrap">Juno</span>
|
|
102
|
-
<span
|
|
103
|
-
className="flex items-center px-3 py-1 bg-green-50 text-green-600 text-xs font-medium rounded-full"
|
|
104
|
-
>
|
|
105
|
-
<CheckCheck className="w-4 me-1 h-4 text-green-600" />
|
|
106
|
-
Stocked
|
|
107
|
-
</span>
|
|
108
|
-
</div>
|
|
109
|
-
</div>
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
<div className="flex flex-wrap gap-2 mt-4">
|
|
113
|
-
{services.map((service) => (
|
|
114
|
-
<span
|
|
115
|
-
key={service}
|
|
116
|
-
className="px-3 py-1 bg-primary/10 text-primary text-xs font-medium rounded-full"
|
|
117
|
-
>
|
|
118
|
-
{service}
|
|
119
|
-
</span>
|
|
120
|
-
))}
|
|
121
|
-
</div>
|
|
122
|
-
</div>
|
|
123
|
-
|
|
124
|
-
<div className="flex md:flex-col gap-2">
|
|
125
|
-
<Button onClick={handleStoreSelected} variant="default" className="whitespace-nowrap">
|
|
126
|
-
{t('default:select_store')}
|
|
127
|
-
</Button>
|
|
128
|
-
</div>
|
|
129
|
-
</div>
|
|
130
|
-
</div>
|
|
131
|
-
)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export default StoreListItem
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import LeadForm from '@/components/LeadForm'
|
|
2
|
-
import { useQuoteForm } from '@/contexts/quote-form'
|
|
3
|
-
|
|
4
|
-
const QuoteLeadForm = ({ onClose, onSuccess }) => {
|
|
5
|
-
|
|
6
|
-
const { organizationId, location, selectedStoreId, products } = useQuoteForm()
|
|
7
|
-
|
|
8
|
-
const handleSuccess = (formValues, lead) => {
|
|
9
|
-
onSuccess && onSuccess({ formValues, lead })
|
|
10
|
-
}
|
|
11
|
-
return (
|
|
12
|
-
<LeadForm onClose={onClose} products={products} activityType="LeadActivity::QuoteRequest" onSuccess={handleSuccess} location={location} organizationId={organizationId} selectedLocationId={selectedStoreId} />
|
|
13
|
-
)
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export default QuoteLeadForm
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
// The import part of the map is that you need to let the map managed its own center
|
|
2
|
-
// and zoom state internally. Using external state management (like React state) can lead to
|
|
3
|
-
// performance issues and a suboptimal user experience.
|
|
4
|
-
import { useEffect, useState, useRef } from 'react'
|
|
5
|
-
import { Map, useMap } from '@vis.gl/react-google-maps'
|
|
6
|
-
import QuoteMapMarker from '@/components/QuoteMapMarker'
|
|
7
|
-
import { useQuoteForm } from '@/contexts/quote-form'
|
|
8
|
-
|
|
9
|
-
const QuoteMap = () => {
|
|
10
|
-
|
|
11
|
-
const { location, stores, focusedStoreId, setFocusedStoreId, setSelectedStoreId, setStepIndex } = useQuoteForm()
|
|
12
|
-
const map = useMap()
|
|
13
|
-
|
|
14
|
-
const zoomIn = () => {
|
|
15
|
-
if (!map) return;
|
|
16
|
-
const currentZoom = map.getZoom();
|
|
17
|
-
map.setZoom(currentZoom + 1);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const zoomOut = () => {
|
|
21
|
-
if (!map) return;
|
|
22
|
-
const currentZoom = map.getZoom();
|
|
23
|
-
map.setZoom(currentZoom - 1);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
useEffect(() => {
|
|
27
|
-
if (!map) return;
|
|
28
|
-
if (location?.latitude && location?.longitude) {
|
|
29
|
-
map.panTo({ lat: location.latitude, lng: location.longitude })
|
|
30
|
-
map.setZoom(9)
|
|
31
|
-
}
|
|
32
|
-
}, [location, map]);
|
|
33
|
-
|
|
34
|
-
useEffect(() => {
|
|
35
|
-
if (!map || !focusedStoreId) return;
|
|
36
|
-
const focusedStore = stores.find(store => store.id === focusedStoreId)
|
|
37
|
-
if (focusedStore && focusedStore.address) {
|
|
38
|
-
map.panTo({ lat: focusedStore.address.latitude * 1, lng: focusedStore.address.longitude * 1 })
|
|
39
|
-
}
|
|
40
|
-
}, [focusedStoreId, stores, map]);
|
|
41
|
-
|
|
42
|
-
const handleMarkerClick = (storeId) => {
|
|
43
|
-
setFocusedStoreId(storeId)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const handleMarkerSelect = (storeId) => {
|
|
47
|
-
setSelectedStoreId(storeId)
|
|
48
|
-
setStepIndex(1)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return (
|
|
52
|
-
<div className="relative">
|
|
53
|
-
<div className="bg-muted rounded-xl overflow-hidden border shadow-md h-[300px]">
|
|
54
|
-
{/* Map placeholder - Replace with actual map integration (Google Maps, Mapbox, etc.) */}
|
|
55
|
-
<div className="w-full h-full flex items-center justify-center bg-gradient-to-br from-primary/5 to-muted">
|
|
56
|
-
<div className="text-center w-full">
|
|
57
|
-
|
|
58
|
-
<Map
|
|
59
|
-
style={{ width: '100%', height: '300px' }}
|
|
60
|
-
defaultCenter={{ lat: 22.54992, lng: 0 }}
|
|
61
|
-
defaultZoom={3}
|
|
62
|
-
gestureHandling='greedy'
|
|
63
|
-
disableDefaultUI
|
|
64
|
-
mapId="b1e4f5f5f8c7e2d9"
|
|
65
|
-
>
|
|
66
|
-
{stores.map((store) => (
|
|
67
|
-
<QuoteMapMarker key={store.id} store={store} isSelected={focusedStoreId === store.id} onClick={handleMarkerClick} onSelect={handleMarkerSelect} />
|
|
68
|
-
))}
|
|
69
|
-
</Map>
|
|
70
|
-
</div>
|
|
71
|
-
</div>
|
|
72
|
-
|
|
73
|
-
{/* Map controls overlay */}
|
|
74
|
-
<div className="absolute top-4 right-4 flex flex-col gap-2">
|
|
75
|
-
<button className="bg-card border shadow-md rounded-lg p-2 hover:bg-accent transition-colors" onClick={zoomIn}>
|
|
76
|
-
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
77
|
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
|
78
|
-
</svg>
|
|
79
|
-
</button>
|
|
80
|
-
<button className="bg-card border shadow-md rounded-lg p-2 hover:bg-accent transition-colors" onClick={zoomOut}>
|
|
81
|
-
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
82
|
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 12H4" />
|
|
83
|
-
</svg>
|
|
84
|
-
</button>
|
|
85
|
-
</div>
|
|
86
|
-
|
|
87
|
-
{/* Results count badge */}
|
|
88
|
-
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 bg-card border shadow-lg rounded-full px-4 py-2 text-sm font-medium">
|
|
89
|
-
<span className="text-primary">{stores.length}</span> stores found
|
|
90
|
-
</div>
|
|
91
|
-
</div>
|
|
92
|
-
</div>
|
|
93
|
-
)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export default QuoteMap
|