@colabcommerce/elements 0.0.4 → 0.9.1

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 (99) hide show
  1. package/.pnp.cjs +16484 -0
  2. package/.pnp.loader.mjs +2126 -0
  3. package/.yarn/install-state.gz +0 -0
  4. package/.yarn/releases/yarn-4.12.0.cjs +942 -0
  5. package/.yarnrc.yml +1 -0
  6. package/README.md +60 -41
  7. package/cypress/fixtures/example.json +5 -0
  8. package/cypress/support/commands.js +25 -0
  9. package/cypress/support/component-index.html +15 -0
  10. package/cypress/support/component.js +26 -0
  11. package/cypress.config.js +10 -0
  12. package/eslint.config.js +32 -0
  13. package/index.html +13 -0
  14. package/package.json +91 -67
  15. package/playground/index.html +14 -0
  16. package/playground/main.jsx +36 -0
  17. package/public/vite.svg +1 -0
  18. package/src/App.css +0 -0
  19. package/src/App.jsx +65 -0
  20. package/src/components/CollapsibleStoreHours/index.jsx +269 -0
  21. package/src/components/HoursList/index.jsx +225 -0
  22. package/src/components/LeadForm/index.jsx +241 -0
  23. package/src/components/MessageDialog/index.jsx +169 -0
  24. package/src/components/QuoteForm/index.jsx +82 -0
  25. package/src/components/QuoteFormSearch/index.jsx +276 -0
  26. package/src/components/QuoteFormStoreList/index.jsx +65 -0
  27. package/src/components/QuoteFormStoreListItem/index.jsx +134 -0
  28. package/src/components/QuoteLeadForm/index.jsx +16 -0
  29. package/src/components/QuoteMap/index.jsx +96 -0
  30. package/src/components/QuoteMapMarker/index.jsx +56 -0
  31. package/src/components/StaticMap/index.jsx +24 -0
  32. package/src/components/Store/index.jsx +44 -0
  33. package/src/components/StoreContact/index.jsx +96 -0
  34. package/src/components/StoreInfo/index.jsx +50 -0
  35. package/src/components/StoreList/index.jsx +59 -0
  36. package/src/components/StoreListItem/index.jsx +99 -0
  37. package/src/components/StoreListItem/indexStoreListItem.cy.jsx +30 -0
  38. package/src/components/StoreListNoneFound/index.jsx +16 -0
  39. package/src/components/StoreLocator/index.jsx +43 -0
  40. package/src/components/StoreLocatorMap/index.jsx +93 -0
  41. package/src/components/StoreLocatorMapMarker/index.jsx +55 -0
  42. package/src/components/StoreLocatorMessageDialog/index.jsx +20 -0
  43. package/src/components/StoreLocatorSearch/index.jsx +316 -0
  44. package/src/components/StoreMap/index.jsx +30 -0
  45. package/src/components/StoreMeta/index.jsx +7 -0
  46. package/src/components/StoreProducts/index.jsx +112 -0
  47. package/src/components/ui/Badge/index.jsx +46 -0
  48. package/src/components/ui/Button/index.jsx +56 -0
  49. package/src/components/ui/Button/indexButton.cy.jsx +9 -0
  50. package/src/components/ui/Card/index.jsx +90 -0
  51. package/src/components/ui/Input/index.jsx +19 -0
  52. package/src/components/ui/Input/indexInput.cy.jsx +9 -0
  53. package/src/components/ui/LoadingPuff/index.jsx +10 -0
  54. package/src/components/ui/Panel/index.jsx +23 -0
  55. package/src/components/ui/PhoneNumberInput/index.jsx +17 -0
  56. package/src/contexts/quote-form.jsx +94 -0
  57. package/src/contexts/store-locator.jsx +83 -0
  58. package/src/contexts/store.jsx +59 -0
  59. package/src/contexts/translations.jsx +11 -0
  60. package/src/dist.css +229 -0
  61. package/src/entries/QuoteForm.js +2 -0
  62. package/src/entries/Store.js +2 -0
  63. package/src/entries/StoreLocator.js +2 -0
  64. package/src/entries/StoreLocatorProvider.js +2 -0
  65. package/src/entries/styles.js +2 -0
  66. package/src/entries/useStoreLocator.js +2 -0
  67. package/src/i18n/defaultResources.js +19 -0
  68. package/src/i18n/index.js +44 -0
  69. package/src/i18n/mergeResources.js +22 -0
  70. package/src/index.css +214 -0
  71. package/src/lib/addressComponentsToAddress.js +43 -0
  72. package/src/lib/productSchema.js +6 -0
  73. package/src/lib/useGeolocation.js +266 -0
  74. package/src/lib/useHours.js +205 -0
  75. package/src/lib/usePlacesAutocomplete.js +288 -0
  76. package/src/lib/useProductAvailability.js +38 -0
  77. package/src/lib/useRudderAnalytics.js +50 -0
  78. package/src/lib/useSearchResults.js +102 -0
  79. package/src/lib/useStoreLocatorConfig.js +50 -0
  80. package/src/lib/utils/cn.js +6 -0
  81. package/src/lib/utils/measure.js +31 -0
  82. package/src/locales/en/default.json +58 -0
  83. package/src/locales/es/default.json +58 -0
  84. package/src/locales/fr/default.json +58 -0
  85. package/src/locales/it/default.json +58 -0
  86. package/src/main.jsx +10 -0
  87. package/vite.config.js +60 -53
  88. package/dist/CartForm.js +0 -617
  89. package/dist/Container-CU_WrBOi.js +0 -22
  90. package/dist/Modal-DTBKy_6d.js +0 -863
  91. package/dist/ProductForm.js +0 -343
  92. package/dist/Retailer.js +0 -3637
  93. package/dist/StoreLocator.js +0 -797
  94. package/dist/addressComponentsToAddress-DCL-K8mn.js +0 -1932
  95. package/dist/browser-ponyfill-DcK7_cJB.js +0 -339
  96. package/dist/globals-B8-hYoIU.js +0 -8518
  97. package/dist/index-CqSfhXDd.js +0 -137
  98. package/dist/index-FM02Uq_P.js +0 -100
  99. package/dist/style.css +0 -1
@@ -0,0 +1,241 @@
1
+ import { Dialog } from 'radix-ui'
2
+ import { useFormik } from 'formik'
3
+ import { Button } from '@/components/ui/button'
4
+ import { CircleAlert, User, Mail, Phone, StickyNote } from 'lucide-react'
5
+ import PhoneInput from 'react-phone-number-input'
6
+ import { isValidPhoneNumber, parsePhoneNumber } from 'libphonenumber-js'
7
+ import { PhoneNumberInput } from '@/components/ui/PhoneNumberInput'
8
+ import Input from '@/components/ui/input'
9
+ import * as Yup from 'yup'
10
+ import cn from '@/lib/utils/cn'
11
+ import { useTranslation } from 'react-i18next'
12
+
13
+ const LeadForm = ({
14
+ organizationId,
15
+ selectedLocationId,
16
+ storeName,
17
+ location = {},
18
+ activityType,
19
+ products,
20
+ isOpen,
21
+ onClose,
22
+ onSuccess
23
+ }) => {
24
+ console.log('LeadForm selectedLocation:', selectedLocationId)
25
+
26
+ const { t, lng } = useTranslation()
27
+
28
+ const initialValues = {
29
+ name: '',
30
+ email: '',
31
+ phoneNumber: '',
32
+ message: '',
33
+ consent: false,
34
+ }
35
+
36
+ const validationSchema = Yup.object({
37
+ name: Yup.string().required(t('default:name.required')),
38
+ email: Yup.string().email(t('default:email.invalid')).required(t('default:email.required')),
39
+ phoneNumber: Yup.string().test('is-valid-phone', t('default:phone_number.invalid'), value => isValidPhoneNumber(value || '')).required(t('default:phone_number.required')),
40
+ consent: Yup.boolean().oneOf([true], t('default:consent.required')).required(t('default:consent.required')),
41
+ })
42
+
43
+ const getNationalNumber = (phoneNumber) => {
44
+ try {
45
+ const parsedNumber = parsePhoneNumber(phoneNumber)
46
+ return parsedNumber.nationalNumber
47
+ } catch (error) {
48
+ return phoneNumber
49
+ }
50
+ }
51
+
52
+ const handleSubmit = async (values, { setSubmitting, resetForm }) => {
53
+ const payload = {
54
+ id: organizationId,
55
+ lead: {
56
+ name: values.name,
57
+ emails_attributes: [{ email: values.email }],
58
+ phone_numbers_attributes: [{ phone_number: getNationalNumber(values.phoneNumber), country_code: 'US' }],
59
+ message: values.message,
60
+ opt_in: values.consent,
61
+ locale: lng,
62
+ location_attributes: {
63
+ city: location?.city,
64
+ province: location?.province,
65
+ postal_code: location?.postal,
66
+ country: location?.country,
67
+ latitude: location?.latitude || null,
68
+ longitude: location?.longitude || null
69
+ },
70
+ assignee_id: selectedLocationId || null,
71
+ assignee_type: 'CompanyRetailerLocation',
72
+ }
73
+ }
74
+ if (activityType) {
75
+ payload.lead.lead_activities_attributes = [{
76
+ type: activityType,
77
+ products: products,
78
+ notes: values.message,
79
+ source_type: 'Company',
80
+ source_id: organizationId
81
+ }]
82
+ }
83
+ const req = await fetch(`${import.meta.env.VITE_PUBLIC_API_URL}/widget_api/leads`, {
84
+ method: 'POST',
85
+ headers: {
86
+ 'Content-Type': 'application/json',
87
+ },
88
+ body: JSON.stringify(payload)
89
+ })
90
+ if (!req.ok) {
91
+ if (req.status === 422) {
92
+ const data = await req.json()
93
+ const errors = data.errors || {}
94
+ const formattedErrors = {}
95
+ if (errors['phone_numbers.phone_number']) {
96
+ formattedErrors.phone = t('quote_form.invalid_phone')
97
+ }
98
+ formik.setErrors(formattedErrors)
99
+ }
100
+ setError(true)
101
+ return
102
+ }
103
+ const data = await req.json()
104
+ onSuccess && onSuccess(values, data)
105
+ }
106
+
107
+ const formik = useFormik({
108
+ initialValues,
109
+ validationSchema,
110
+ onSubmit: handleSubmit,
111
+ enableReinitialize: true,
112
+ validateOnBlur: false,
113
+ })
114
+
115
+ return (
116
+ <form onSubmit={formik.handleSubmit} className="space-y-4">
117
+ <div>
118
+ <div className="flex-1 relative">
119
+ <User className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-muted-foreground" />
120
+ <Input
121
+ type="text"
122
+ id="name"
123
+ name="name"
124
+ placeholder={t('default:name.placeholder')}
125
+ aria-invalid={formik.touched.name && formik.errors.name ? 'true' : 'false'}
126
+ value={formik.values.name}
127
+ onChange={formik.handleChange}
128
+ onBlur={formik.handleBlur}
129
+ className="pl-10 h-12"
130
+ />
131
+ {formik.touched.name && formik.errors.name ? (
132
+ <CircleAlert className="absolute text-red-600 right-3 top-1/2 -translate-y-1/2 w-5 h-5" />
133
+ ) : null}
134
+ </div>
135
+ {formik.touched.name && formik.errors.name ? (
136
+ <div className="text-sm text-left text-red-600 mt-1">{formik.errors.name}</div>
137
+ ) : null}
138
+ </div>
139
+ <div>
140
+ <div className="flex-1 relative">
141
+ <Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-muted-foreground" />
142
+ <Input
143
+ type="email"
144
+ id="email"
145
+ name="email"
146
+ placeholder={t('default:email.placeholder')}
147
+ value={formik.values.email}
148
+ aria-invalid={formik.touched.email && formik.errors.email ? 'true' : 'false'}
149
+ onChange={formik.handleChange}
150
+ onBlur={formik.handleBlur}
151
+ className="pl-10 h-12"
152
+ />
153
+ {formik.touched.email && formik.errors.email ? (
154
+ <CircleAlert className="absolute text-red-600 right-3 top-1/2 -translate-y-1/2 w-5 h-5" />
155
+ ) : null}
156
+ </div>
157
+ {formik.touched.email && formik.errors.email ? (
158
+ <div className="text-sm text-left text-red-600 mt-1">{formik.errors.email}</div>
159
+ ) : null}
160
+ </div>
161
+ <div>
162
+ <div className="flex-1 relative">
163
+ <Phone className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-muted-foreground" />
164
+ <PhoneInput
165
+ international
166
+ defaultCountry="US"
167
+ countryCallingCodeEditable={false}
168
+ value={formik.values.phoneNumber}
169
+ onChange={(value) => formik.setFieldValue('phoneNumber', value)}
170
+ onBlur={() => formik.setFieldTouched('phoneNumber', true)}
171
+ inputComponent={PhoneNumberInput}
172
+ inputProps={{
173
+ id: 'phoneNumber',
174
+ name: 'phoneNumber',
175
+ placeholder: t('default:phone_number.placeholder'),
176
+ 'aria-invalid':
177
+ formik.touched.phoneNumber && formik.errors.phoneNumber
178
+ ? 'true'
179
+ : 'false',
180
+ }}
181
+ />
182
+ {formik.touched.phoneNumber && formik.errors.phoneNumber ? (
183
+ <CircleAlert className="absolute text-red-600 right-3 top-1/2 -translate-y-1/2 w-5 h-5" />
184
+ ) : null}
185
+ </div>
186
+ {formik.touched.phoneNumber && formik.errors.phoneNumber ? (
187
+ <div className="text-sm text-left text-red-600 mt-1">{formik.errors.phoneNumber}</div>
188
+ ) : null}
189
+ </div>
190
+ <div>
191
+ <div className="flex-1 relative">
192
+ <StickyNote className="absolute left-3 top-5 -translate-y-1/2 w-5 h-5 text-muted-foreground" />
193
+ <textarea
194
+ id="message"
195
+ name="message"
196
+ placeholder={t('default:quote_message.placeholder')}
197
+ aria-invalid={formik.touched.message && formik.errors.message ? 'true' : 'false'}
198
+ value={formik.values.message}
199
+ onChange={formik.handleChange}
200
+ onBlur={formik.handleBlur}
201
+ className={cn(
202
+ 'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
203
+ 'focus-visible:border-ring focus-visible:ring-ring/20 focus-visible:ring-[3px]',
204
+ 'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
205
+ 'h-36 pt-2.5 pl-10',
206
+ )}
207
+ ></textarea>
208
+ {formik.touched.message && formik.errors.message ? (
209
+ <CircleAlert className="absolute text-red-600 right-3 top-5 -translate-y-1/2 w-5 h-5" />
210
+ ) : null}
211
+ </div>
212
+ {formik.touched.message && formik.errors.message ? (
213
+ <div className="text-sm text-left text-red-600 mt-1">{formik.errors.message}</div>
214
+ ) : null}
215
+ </div>
216
+ <div className="flex items-start">
217
+ <input
218
+ type="checkbox"
219
+ id="consent"
220
+ name="consent"
221
+ checked={formik.values.consent}
222
+ onChange={formik.handleChange}
223
+ onBlur={formik.handleBlur}
224
+ aria-invalid={formik.touched.consent && formik.errors.consent ? 'true' : 'false'}
225
+ className="mt-0.5 h-4 w-4 rounded border-gray-300 text-purple-600 focus:ring-purple-600 cursor-pointer aria-invalid:text-red-600 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive"
226
+ />
227
+ <label htmlFor="consent" className={`ml-2 block text-left text-sm text-gray-700 cursor-pointer ${formik.touched.consent && formik.errors.consent ? 'text-red-600' : ''}`}>
228
+ {t('default:consent.label', { storeName: storeName || t('default:storeName') })}
229
+ </label>
230
+ </div>
231
+ <div className="flex justify-end gap-2">
232
+ <Button variant="outline" onClick={onClose} type="button">{t('default:cancel')}</Button>
233
+ <Button type="submit" disabled={formik.isSubmitting}>
234
+ {formik.isSubmitting ? t('default:sending') : t('default:quote_submit')}
235
+ </Button>
236
+ </div>
237
+ </form>
238
+ )
239
+ }
240
+
241
+ export default LeadForm
@@ -0,0 +1,169 @@
1
+ import { Dialog } from 'radix-ui'
2
+ import { useFormik } from 'formik'
3
+ import { Button } from '@/components/ui/button'
4
+ import { CircleAlert, User, Mail, Phone, StickyNote } from 'lucide-react'
5
+ import Input from '@/components/ui/input'
6
+ import * as Yup from 'yup'
7
+ import cn from '@/lib/utils/cn'
8
+
9
+ // Dialogs are special. They need to be rendered at the root level of the app. So we wrap
10
+ // in a cc div and use a manual z-index to ensure it appears above other elements.
11
+ const MessageDialog = ({ organizationId, storeName, isOpen, onClose, onSuccess }) => {
12
+
13
+ const initialValues = {
14
+ name: '',
15
+ email: '',
16
+ phoneNumber: '',
17
+ message: '',
18
+ consent: false,
19
+ }
20
+
21
+ const phoneNumberRegex = /^\+?[1-9]\d{1,14}$/
22
+
23
+ const validationSchema = Yup.object({
24
+ name: Yup.string().required('Name is required'),
25
+ email: Yup.string().email('Invalid email address').required('Email is required'),
26
+ phoneNumber: Yup.string().matches(phoneNumberRegex, 'Invalid phone number').required('Phone number is required'),
27
+ message: Yup.string().required('Message is required'),
28
+ consent: Yup.boolean().oneOf([true], 'You must agree to the terms').required('You must agree to the terms'),
29
+ })
30
+
31
+ const handleSubmit = async (values, { setSubmitting, resetForm }) => {
32
+ }
33
+
34
+ const formik = useFormik({
35
+ initialValues,
36
+ validationSchema,
37
+ onSubmit: handleSubmit,
38
+ enableReinitialize: true,
39
+ validateOnBlur: false,
40
+ })
41
+
42
+ return (
43
+ <div className="cc" style={{ zIndex: 1000, position: 'relative' }}>
44
+ <Dialog.Root open={isOpen} onOpenChange={onClose}>
45
+ <Dialog.Overlay className="fixed inset-0 bg-black/50" />
46
+ <Dialog.Content className="fixed top-1/2 left-1/2 max-h-[85vh] w-[90vw] max-w-lg -translate-x-1/2 -translate-y-1/2 rounded-lg bg-white p-6 shadow-lg focus:outline-none">
47
+ <Dialog.Title className="text-lg text-left font-medium mb-4">Send a Message to {storeName || 'the store'}</Dialog.Title>
48
+ <form onSubmit={formik.handleSubmit} className="space-y-4">
49
+ <div>
50
+ <div className="flex-1 relative">
51
+ <User className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-muted-foreground" />
52
+ <Input
53
+ type="text"
54
+ id="name"
55
+ name="name"
56
+ placeholder={"Name"}
57
+ aria-invalid={formik.touched.name && formik.errors.name ? 'true' : 'false'}
58
+ value={formik.values.name}
59
+ onChange={formik.handleChange}
60
+ onBlur={formik.handleBlur}
61
+ className="pl-10 h-12"
62
+ />
63
+ {formik.touched.name && formik.errors.name ? (
64
+ <CircleAlert className="absolute text-red-600 right-3 top-1/2 -translate-y-1/2 w-5 h-5" />
65
+ ) : null}
66
+ </div>
67
+ {formik.touched.name && formik.errors.name ? (
68
+ <div className="text-sm text-left text-red-600 mt-1">{formik.errors.name}</div>
69
+ ) : null}
70
+ </div>
71
+ <div>
72
+ <div className="flex-1 relative">
73
+ <Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-muted-foreground" />
74
+ <Input
75
+ type="email"
76
+ id="email"
77
+ name="email"
78
+ placeholder={"Email Address"}
79
+ value={formik.values.email}
80
+ aria-invalid={formik.touched.email && formik.errors.email ? 'true' : 'false'}
81
+ onChange={formik.handleChange}
82
+ onBlur={formik.handleBlur}
83
+ className="pl-10 h-12"
84
+ />
85
+ {formik.touched.email && formik.errors.email ? (
86
+ <CircleAlert className="absolute text-red-600 right-3 top-1/2 -translate-y-1/2 w-5 h-5" />
87
+ ) : null}
88
+ </div>
89
+ {formik.touched.email && formik.errors.email ? (
90
+ <div className="text-sm text-left text-red-600 mt-1">{formik.errors.email}</div>
91
+ ) : null}
92
+ </div>
93
+ <div>
94
+ <div className="flex-1 relative">
95
+ <Phone className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-muted-foreground" />
96
+ <Input
97
+ type="text"
98
+ id="phoneNumber"
99
+ name="phoneNumber"
100
+ placeholder={"Phone Number"}
101
+ value={formik.values.phoneNumber}
102
+ onChange={formik.handleChange}
103
+ onBlur={formik.handleBlur}
104
+ aria-invalid={formik.touched.phoneNumber && formik.errors.phoneNumber ? 'true' : 'false'}
105
+ className="pl-10 h-12"
106
+ />
107
+ {formik.touched.phoneNumber && formik.errors.phoneNumber ? (
108
+ <CircleAlert className="absolute text-red-600 right-3 top-1/2 -translate-y-1/2 w-5 h-5" />
109
+ ) : null}
110
+ </div>
111
+ {formik.touched.phoneNumber && formik.errors.phoneNumber ? (
112
+ <div className="text-sm text-left text-red-600 mt-1">{formik.errors.phoneNumber}</div>
113
+ ) : null}
114
+ </div>
115
+ <div>
116
+ <div className="flex-1 relative">
117
+ <StickyNote className="absolute left-3 top-5 -translate-y-1/2 w-5 h-5 text-muted-foreground" />
118
+ <textarea
119
+ id="message"
120
+ name="message"
121
+ placeholder="Your Message"
122
+ aria-invalid={formik.touched.message && formik.errors.message ? 'true' : 'false'}
123
+ value={formik.values.message}
124
+ onChange={formik.handleChange}
125
+ onBlur={formik.handleBlur}
126
+ className={cn(
127
+ 'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
128
+ 'focus-visible:border-ring focus-visible:ring-ring/20 focus-visible:ring-[3px]',
129
+ 'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
130
+ 'h-36 pt-2.5 pl-10',
131
+ )}
132
+ ></textarea>
133
+ {formik.touched.message && formik.errors.message ? (
134
+ <CircleAlert className="absolute text-red-600 right-3 top-5 -translate-y-1/2 w-5 h-5" />
135
+ ) : null}
136
+ </div>
137
+ {formik.touched.message && formik.errors.message ? (
138
+ <div className="text-sm text-left text-red-600 mt-1">{formik.errors.message}</div>
139
+ ) : null}
140
+ </div>
141
+ <div className="flex items-start">
142
+ <input
143
+ type="checkbox"
144
+ id="consent"
145
+ name="consent"
146
+ checked={formik.values.consent}
147
+ onChange={formik.handleChange}
148
+ onBlur={formik.handleBlur}
149
+ aria-invalid={formik.touched.consent && formik.errors.consent ? 'true' : 'false'}
150
+ className="mt-0.5 h-4 w-4 rounded border-gray-300 text-purple-600 focus:ring-purple-600 cursor-pointer aria-invalid:text-red-600 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive"
151
+ />
152
+ <label htmlFor="consent" className={`ml-2 block text-left text-sm text-gray-700 cursor-pointer ${formik.touched.consent && formik.errors.consent ? 'text-red-600' : ''}`}>
153
+ I would like to receive communications from {storeName || 'the store'}.
154
+ </label>
155
+ </div>
156
+ <div className="flex justify-end gap-2">
157
+ <Button variant="outline" onClick={onClose} type="button">Cancel</Button>
158
+ <Button type="submit" disabled={formik.isSubmitting}>
159
+ {formik.isSubmitting ? 'Sending...' : 'Send Message'}
160
+ </Button>
161
+ </div>
162
+ </form>
163
+ </Dialog.Content>
164
+ </Dialog.Root>
165
+ </div>
166
+ )
167
+ }
168
+
169
+ export default MessageDialog
@@ -0,0 +1,82 @@
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