@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,225 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import * as Collapsible from "@radix-ui/react-collapsible";
|
|
3
|
-
import { ChevronDown } from "lucide-react";
|
|
4
|
-
import { useTranslation } from "react-i18next";
|
|
5
|
-
|
|
6
|
-
const DAY_LABELS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
7
|
-
|
|
8
|
-
export default function HoursList({
|
|
9
|
-
hours,
|
|
10
|
-
openingSoonMinutes = 60,
|
|
11
|
-
closingSoonMinutes = 60,
|
|
12
|
-
className = "",
|
|
13
|
-
}) {
|
|
14
|
-
const { t } = useTranslation();
|
|
15
|
-
const [open, setOpen] = React.useState(false);
|
|
16
|
-
|
|
17
|
-
const computed = React.useMemo(() => {
|
|
18
|
-
const safeHours = Array.isArray(hours) ? hours : [];
|
|
19
|
-
const storeTz =
|
|
20
|
-
safeHours.find((h) => h?.timezone)?.timezone ||
|
|
21
|
-
Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
22
|
-
|
|
23
|
-
const now = new Date();
|
|
24
|
-
const storeWeekdayIndex = getWeekdayIndexInTimeZone(now, storeTz); // 0..6
|
|
25
|
-
const today = safeHours.find((h) => h?.day === storeWeekdayIndex);
|
|
26
|
-
|
|
27
|
-
const status = computeStatus({
|
|
28
|
-
now,
|
|
29
|
-
today,
|
|
30
|
-
openingSoonMinutes,
|
|
31
|
-
closingSoonMinutes,
|
|
32
|
-
t,
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
// Display Monday..Sunday
|
|
36
|
-
const displayOrder = [1, 2, 3, 4, 5, 6, 0];
|
|
37
|
-
const byDay = new Map(safeHours.map((h) => [h.day, h]));
|
|
38
|
-
const rows = displayOrder.map((dayIdx) => {
|
|
39
|
-
const h = byDay.get(dayIdx);
|
|
40
|
-
const label = t(`default:${DAY_LABELS[dayIdx].toLowerCase()}`);
|
|
41
|
-
const isToday = dayIdx === storeWeekdayIndex;
|
|
42
|
-
|
|
43
|
-
let text = t("default:closed");
|
|
44
|
-
if (h?.open) {
|
|
45
|
-
const openDate = buildOpenDateForDisplay(h);
|
|
46
|
-
const closeDate = buildCloseDateForDisplay(h, openDate);
|
|
47
|
-
if (openDate && closeDate) {
|
|
48
|
-
text = `${formatTimeLocal(openDate)} - ${formatTimeLocal(closeDate)}`;
|
|
49
|
-
} else {
|
|
50
|
-
text = t("default:open");
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return { dayIdx, label, text, isToday };
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
return {
|
|
58
|
-
statusLabel: t(`default:${status.label.toLowerCase().replace(" ", "_")}`), // "Closed" | "Opening Soon" | "Open" | "Closing Soon"
|
|
59
|
-
statusDetail: status.detail, // "Opens at 9am" | "Closes at 4:30pm" | ""
|
|
60
|
-
rows,
|
|
61
|
-
};
|
|
62
|
-
}, [hours, openingSoonMinutes, closingSoonMinutes]);
|
|
63
|
-
|
|
64
|
-
return (
|
|
65
|
-
<>
|
|
66
|
-
{computed.rows.map((row) => (
|
|
67
|
-
<>
|
|
68
|
-
<div
|
|
69
|
-
key={row.dayIdx}
|
|
70
|
-
className={`flex justify-between items-center py-2 ${row.isToday ? "bg-primary/5 -mx-4 px-4 rounded-lg" : ""
|
|
71
|
-
}`}
|
|
72
|
-
>
|
|
73
|
-
<span className={`font-medium ${row.isToday ? "text-primary" : ""}`}>
|
|
74
|
-
{row.label}
|
|
75
|
-
{row.isToday && <span className="ml-2 text-xs">(Today)</span>}
|
|
76
|
-
</span>
|
|
77
|
-
<span className="text-muted-foreground">{row.text}</span>
|
|
78
|
-
</div>
|
|
79
|
-
</>
|
|
80
|
-
))}
|
|
81
|
-
</>
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Toggle label logic based on today's schedule (store timezone for "today",
|
|
87
|
-
* comparisons against the user's current instant).
|
|
88
|
-
*
|
|
89
|
-
* Adds a detail line:
|
|
90
|
-
* - Closed / Opening Soon -> "Opens at X"
|
|
91
|
-
* - Open / Closing Soon -> "Closes at Y"
|
|
92
|
-
*/
|
|
93
|
-
function computeStatus({ now, today, openingSoonMinutes, closingSoonMinutes, t }) {
|
|
94
|
-
if (!today || !today.open) return { label: t('default:closed'), detail: "" };
|
|
95
|
-
|
|
96
|
-
const openDate = buildOpenDateForDisplay(today);
|
|
97
|
-
const closeDate = buildCloseDateForDisplay(today, openDate);
|
|
98
|
-
|
|
99
|
-
// If schedule incomplete, still show basic label
|
|
100
|
-
if (!openDate || !closeDate) return { label: t('default:open'), detail: "" };
|
|
101
|
-
|
|
102
|
-
const nowMs = now.getTime();
|
|
103
|
-
const openMs = openDate.getTime();
|
|
104
|
-
const closeMs = closeDate.getTime();
|
|
105
|
-
|
|
106
|
-
const openingSoonStart = openMs - openingSoonMinutes * 60 * 1000;
|
|
107
|
-
const closingSoonStart = closeMs - closingSoonMinutes * 60 * 1000;
|
|
108
|
-
|
|
109
|
-
const opensDetail = t('default:opens_at', { time: formatTimeLocal(openDate) });
|
|
110
|
-
const closesDetail = t('default:closes_at', { time: formatTimeLocal(closeDate) });
|
|
111
|
-
|
|
112
|
-
if (nowMs < openMs) {
|
|
113
|
-
if (nowMs >= openingSoonStart) return { label: t('default:opening_soon'), detail: opensDetail };
|
|
114
|
-
return { label: t('default:closed'), detail: opensDetail };
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (nowMs >= openMs && nowMs < closeMs) {
|
|
118
|
-
if (nowMs >= closingSoonStart) return { label: t('default:closing_soon'), detail: closesDetail };
|
|
119
|
-
return { label: t('default:open'), detail: closesDetail };
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return { label: t('default:closed'), detail: opensDetail };
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/** Returns weekday index (0=Sun..6=Sat) for a Date, as seen in `timeZone`. */
|
|
126
|
-
function getWeekdayIndexInTimeZone(date, timeZone) {
|
|
127
|
-
const weekday = new Intl.DateTimeFormat("en-US", { weekday: "short", timeZone }).format(date);
|
|
128
|
-
switch (weekday) {
|
|
129
|
-
case "Sun":
|
|
130
|
-
return 0;
|
|
131
|
-
case "Mon":
|
|
132
|
-
return 1;
|
|
133
|
-
case "Tue":
|
|
134
|
-
return 2;
|
|
135
|
-
case "Wed":
|
|
136
|
-
return 3;
|
|
137
|
-
case "Thu":
|
|
138
|
-
return 4;
|
|
139
|
-
case "Fri":
|
|
140
|
-
return 5;
|
|
141
|
-
case "Sat":
|
|
142
|
-
return 6;
|
|
143
|
-
default:
|
|
144
|
-
return new Date(date).getDay();
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Build "open" Date (instant) for display in user's local time.
|
|
150
|
-
* Prefer `open_at` because it encodes offset and is DST-correct for that date.
|
|
151
|
-
*/
|
|
152
|
-
function buildOpenDateForDisplay(h) {
|
|
153
|
-
if (!h) return null;
|
|
154
|
-
|
|
155
|
-
if (h.open_at) {
|
|
156
|
-
const d = new Date(h.open_at);
|
|
157
|
-
return isValidDate(d) ? d : null;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (typeof h.open_at_hour === "number" && typeof h.open_at_minute === "number") {
|
|
161
|
-
return buildDateFromLocalStoreTime({
|
|
162
|
-
storeHour: h.open_at_hour,
|
|
163
|
-
storeMinute: h.open_at_minute,
|
|
164
|
-
utcOffsetMinute: h.utc_offset_minute,
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
return null;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Build "close" Date (instant). If close <= open, assume next day.
|
|
173
|
-
*/
|
|
174
|
-
function buildCloseDateForDisplay(h, openDate) {
|
|
175
|
-
if (!h) return null;
|
|
176
|
-
|
|
177
|
-
if (typeof h.close_at_hour !== "number" || typeof h.close_at_minute !== "number") return null;
|
|
178
|
-
|
|
179
|
-
let closeDate = buildDateFromLocalStoreTime({
|
|
180
|
-
storeHour: h.close_at_hour,
|
|
181
|
-
storeMinute: h.close_at_minute,
|
|
182
|
-
utcOffsetMinute: h.utc_offset_minute,
|
|
183
|
-
anchorDate: openDate || undefined,
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
if (!closeDate || !openDate) return closeDate;
|
|
187
|
-
|
|
188
|
-
if (closeDate.getTime() <= openDate.getTime()) {
|
|
189
|
-
closeDate = new Date(closeDate.getTime() + 24 * 60 * 60 * 1000);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return closeDate;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Build an instant from a store-local time-of-day plus a fixed UTC offset.
|
|
197
|
-
* storeLocal = UTC + offset => UTC = storeLocal - offset
|
|
198
|
-
*/
|
|
199
|
-
function buildDateFromLocalStoreTime({ storeHour, storeMinute, utcOffsetMinute, anchorDate }) {
|
|
200
|
-
const base = anchorDate ? new Date(anchorDate) : new Date();
|
|
201
|
-
const y = base.getFullYear();
|
|
202
|
-
const m = base.getMonth();
|
|
203
|
-
const d = base.getDate();
|
|
204
|
-
|
|
205
|
-
const utcMillis = Date.UTC(y, m, d, storeHour, storeMinute, 0) - utcOffsetMinute * 60 * 1000;
|
|
206
|
-
const out = new Date(utcMillis);
|
|
207
|
-
|
|
208
|
-
return isValidDate(out) ? out : null;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function isValidDate(d) {
|
|
212
|
-
return d instanceof Date && !Number.isNaN(d.getTime());
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/** Format in user's local timezone as "9am" or "4:30pm". */
|
|
216
|
-
function formatTimeLocal(date) {
|
|
217
|
-
return new Intl.DateTimeFormat("en-US", {
|
|
218
|
-
hour: "numeric",
|
|
219
|
-
minute: "2-digit",
|
|
220
|
-
})
|
|
221
|
-
.format(date)
|
|
222
|
-
.replace(":00", "")
|
|
223
|
-
.replace(" AM", "am")
|
|
224
|
-
.replace(" PM", "pm");
|
|
225
|
-
}
|
|
@@ -1,241 +0,0 @@
|
|
|
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
|
|
@@ -1,169 +0,0 @@
|
|
|
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
|