@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.
- package/.pnp.cjs +16484 -0
- package/.pnp.loader.mjs +2126 -0
- package/.yarn/install-state.gz +0 -0
- package/.yarn/releases/yarn-4.12.0.cjs +942 -0
- package/.yarnrc.yml +1 -0
- package/README.md +60 -41
- package/cypress/fixtures/example.json +5 -0
- package/cypress/support/commands.js +25 -0
- package/cypress/support/component-index.html +15 -0
- package/cypress/support/component.js +26 -0
- package/cypress.config.js +10 -0
- package/eslint.config.js +32 -0
- package/index.html +13 -0
- package/package.json +91 -67
- package/playground/index.html +14 -0
- package/playground/main.jsx +36 -0
- package/public/vite.svg +1 -0
- package/src/App.css +0 -0
- package/src/App.jsx +65 -0
- package/src/components/CollapsibleStoreHours/index.jsx +269 -0
- package/src/components/HoursList/index.jsx +225 -0
- package/src/components/LeadForm/index.jsx +241 -0
- package/src/components/MessageDialog/index.jsx +169 -0
- package/src/components/QuoteForm/index.jsx +82 -0
- package/src/components/QuoteFormSearch/index.jsx +276 -0
- package/src/components/QuoteFormStoreList/index.jsx +65 -0
- package/src/components/QuoteFormStoreListItem/index.jsx +134 -0
- package/src/components/QuoteLeadForm/index.jsx +16 -0
- package/src/components/QuoteMap/index.jsx +96 -0
- package/src/components/QuoteMapMarker/index.jsx +56 -0
- package/src/components/StaticMap/index.jsx +24 -0
- package/src/components/Store/index.jsx +44 -0
- package/src/components/StoreContact/index.jsx +96 -0
- package/src/components/StoreInfo/index.jsx +50 -0
- package/src/components/StoreList/index.jsx +59 -0
- package/src/components/StoreListItem/index.jsx +99 -0
- package/src/components/StoreListItem/indexStoreListItem.cy.jsx +30 -0
- package/src/components/StoreListNoneFound/index.jsx +16 -0
- package/src/components/StoreLocator/index.jsx +43 -0
- package/src/components/StoreLocatorMap/index.jsx +93 -0
- package/src/components/StoreLocatorMapMarker/index.jsx +55 -0
- package/src/components/StoreLocatorMessageDialog/index.jsx +20 -0
- package/src/components/StoreLocatorSearch/index.jsx +316 -0
- package/src/components/StoreMap/index.jsx +30 -0
- package/src/components/StoreMeta/index.jsx +7 -0
- package/src/components/StoreProducts/index.jsx +112 -0
- package/src/components/ui/Badge/index.jsx +46 -0
- package/src/components/ui/Button/index.jsx +56 -0
- package/src/components/ui/Button/indexButton.cy.jsx +9 -0
- package/src/components/ui/Card/index.jsx +90 -0
- package/src/components/ui/Input/index.jsx +19 -0
- package/src/components/ui/Input/indexInput.cy.jsx +9 -0
- package/src/components/ui/LoadingPuff/index.jsx +10 -0
- package/src/components/ui/Panel/index.jsx +23 -0
- package/src/components/ui/PhoneNumberInput/index.jsx +17 -0
- package/src/contexts/quote-form.jsx +94 -0
- package/src/contexts/store-locator.jsx +83 -0
- package/src/contexts/store.jsx +59 -0
- package/src/contexts/translations.jsx +11 -0
- package/src/dist.css +229 -0
- package/src/entries/QuoteForm.js +2 -0
- package/src/entries/Store.js +2 -0
- package/src/entries/StoreLocator.js +2 -0
- package/src/entries/StoreLocatorProvider.js +2 -0
- package/src/entries/styles.js +2 -0
- package/src/entries/useStoreLocator.js +2 -0
- package/src/i18n/defaultResources.js +19 -0
- package/src/i18n/index.js +44 -0
- package/src/i18n/mergeResources.js +22 -0
- package/src/index.css +214 -0
- package/src/lib/addressComponentsToAddress.js +43 -0
- package/src/lib/productSchema.js +6 -0
- package/src/lib/useGeolocation.js +266 -0
- package/src/lib/useHours.js +205 -0
- package/src/lib/usePlacesAutocomplete.js +288 -0
- package/src/lib/useProductAvailability.js +38 -0
- package/src/lib/useRudderAnalytics.js +50 -0
- package/src/lib/useSearchResults.js +102 -0
- package/src/lib/useStoreLocatorConfig.js +50 -0
- package/src/lib/utils/cn.js +6 -0
- package/src/lib/utils/measure.js +31 -0
- package/src/locales/en/default.json +58 -0
- package/src/locales/es/default.json +58 -0
- package/src/locales/fr/default.json +58 -0
- package/src/locales/it/default.json +58 -0
- package/src/main.jsx +10 -0
- package/vite.config.js +60 -53
- package/dist/CartForm.js +0 -617
- package/dist/Container-CU_WrBOi.js +0 -22
- package/dist/Modal-DTBKy_6d.js +0 -863
- package/dist/ProductForm.js +0 -343
- package/dist/Retailer.js +0 -3637
- package/dist/StoreLocator.js +0 -797
- package/dist/addressComponentsToAddress-DCL-K8mn.js +0 -1932
- package/dist/browser-ponyfill-DcK7_cJB.js +0 -339
- package/dist/globals-B8-hYoIU.js +0 -8518
- package/dist/index-CqSfhXDd.js +0 -137
- package/dist/index-FM02Uq_P.js +0 -100
- 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
|