keystone-design-bootstrap 1.0.55 → 1.0.57

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 (55) hide show
  1. package/dist/design_system/elements/index.js +8 -3
  2. package/dist/design_system/elements/index.js.map +1 -1
  3. package/dist/design_system/sections/index.js +203 -106
  4. package/dist/design_system/sections/index.js.map +1 -1
  5. package/dist/index.js +303 -247
  6. package/dist/index.js.map +1 -1
  7. package/dist/lib/hooks/index.js +72 -0
  8. package/dist/lib/hooks/index.js.map +1 -1
  9. package/dist/lib/server-api.js.map +1 -1
  10. package/dist/utils/phone-helpers.js +26 -0
  11. package/dist/utils/phone-helpers.js.map +1 -0
  12. package/package.json +5 -2
  13. package/src/design_system/components/ChatWidget.tsx +51 -34
  14. package/src/design_system/components/DynamicFormFields.tsx +1 -24
  15. package/src/design_system/elements/modal/modal.tsx +54 -35
  16. package/src/design_system/portal/LoginForm.tsx +358 -0
  17. package/src/design_system/portal/LoginModalController.tsx +63 -0
  18. package/src/design_system/portal/LogoutButton.tsx +22 -0
  19. package/src/design_system/portal/MessageComposer.tsx +92 -0
  20. package/src/design_system/portal/PortalPage.tsx +754 -0
  21. package/src/design_system/portal/RowThumbnail.tsx +76 -0
  22. package/src/design_system/portal/index.ts +5 -0
  23. package/src/design_system/sections/index.tsx +1 -1
  24. package/src/design_system/sections/service-menu-section.tsx +7 -108
  25. package/src/lib/actions.ts +51 -115
  26. package/src/lib/consumer-session.ts +74 -0
  27. package/src/lib/hooks/index.ts +2 -0
  28. package/src/lib/hooks/use-image-cycle.ts +105 -0
  29. package/src/lib/server-api.ts +7 -6
  30. package/src/next/routes/chat.ts +30 -58
  31. package/src/next/routes/consumer-auth.ts +180 -0
  32. package/src/types/api/consumer.ts +39 -0
  33. package/src/types/api/offer.ts +1 -1
  34. package/src/types/api/package.ts +20 -0
  35. package/src/types/api/service.ts +6 -24
  36. package/src/types/index.ts +2 -0
  37. package/src/utils/phone-helpers.ts +27 -0
  38. package/dist/blog-post-DGjaJ3wf.d.ts +0 -50
  39. package/dist/contexts/index.d.ts +0 -13
  40. package/dist/design_system/elements/index.d.ts +0 -372
  41. package/dist/design_system/logo/keystone-logo.d.ts +0 -6
  42. package/dist/design_system/sections/index.d.ts +0 -237
  43. package/dist/form-CpsCONG5.d.ts +0 -151
  44. package/dist/index.d.ts +0 -76
  45. package/dist/lib/component-registry.d.ts +0 -13
  46. package/dist/lib/hooks/index.d.ts +0 -64
  47. package/dist/lib/server-api.d.ts +0 -43
  48. package/dist/themes/index.d.ts +0 -16
  49. package/dist/types/index.d.ts +0 -264
  50. package/dist/utils/cx.d.ts +0 -15
  51. package/dist/utils/gradient-placeholder.d.ts +0 -8
  52. package/dist/utils/is-react-component.d.ts +0 -21
  53. package/dist/utils/markdown-toc.d.ts +0 -14
  54. package/dist/utils/photo-helpers.d.ts +0 -37
  55. package/dist/website-photos-Bm-CBK9g.d.ts +0 -47
@@ -0,0 +1,358 @@
1
+ 'use client';
2
+
3
+ import React, { useState, useMemo } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import { countries } from '../../utils/countries';
6
+ import { getNationalMask, formatDigitsToMask } from '../../utils/phone-helpers';
7
+
8
+ type Step = 'identifier' | 'returning' | 'new';
9
+
10
+ interface LoginFormProps {
11
+ onSuccess?: () => void;
12
+ onClose?: () => void;
13
+ }
14
+
15
+ const inputClass =
16
+ 'block w-full rounded border border-gray-300 bg-primary px-3 py-2.5 text-sm text-primary placeholder-gray-400 focus:border-gray-700 focus:outline-none focus:ring-1 focus:ring-gray-700 transition-colors';
17
+ const labelClass = 'block text-sm text-secondary mb-1';
18
+
19
+ function isValidEmail(value: string): boolean {
20
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim());
21
+ }
22
+
23
+ export function LoginForm({ onSuccess, onClose }: LoginFormProps) {
24
+ const router = useRouter();
25
+ const [step, setStep] = useState<Step>('identifier');
26
+
27
+ // Identifier step state
28
+ const [email, setEmail] = useState('');
29
+ const [phoneValue, setPhoneValue] = useState(''); // formatted national number
30
+ const [selectedCountry, setSelectedCountry] = useState('US');
31
+
32
+ // Later steps
33
+ const [welcomeName, setWelcomeName] = useState<string | null>(null);
34
+ const [firstName, setFirstName] = useState('');
35
+ const [lastName, setLastName] = useState('');
36
+ const [password, setPassword] = useState('');
37
+ const [passwordConfirm, setPasswordConfirm] = useState('');
38
+ const [error, setError] = useState<string | null>(null);
39
+ const [loading, setLoading] = useState(false);
40
+
41
+ // Phone helpers
42
+ const country = useMemo(() => countries.find((c) => c.code === selectedCountry), [selectedCountry]);
43
+ const phoneCode = country ? (country.phoneCode.startsWith('+') ? country.phoneCode : `+${country.phoneCode}`) : '+1';
44
+ const nationalMask = useMemo(() => getNationalMask(country), [country]);
45
+ const nationalPlaceholder = nationalMask ? nationalMask.replace(/#/g, '0') : '(555) 000-0000';
46
+ const countryOptions = useMemo(
47
+ () => countries.map((c) => ({ label: c.phoneCode.startsWith('+') ? c.phoneCode : `+${c.phoneCode}`, value: c.code })),
48
+ []
49
+ );
50
+
51
+ // Computed API values
52
+ const phoneDigits = phoneValue.replace(/\D/g, '');
53
+ const fullPhone = phoneDigits.length > 0 ? `${phoneCode}${phoneDigits}` : null;
54
+ const emailVal = email.trim() || null;
55
+
56
+ const handlePhoneChange = (e: React.ChangeEvent<HTMLInputElement>) => {
57
+ const digits = e.target.value.replace(/\D/g, '');
58
+ const formatted = nationalMask ? formatDigitsToMask(digits, nationalMask) : digits;
59
+ setPhoneValue(formatted);
60
+ };
61
+
62
+ const handleContinue = async (e: React.FormEvent) => {
63
+ e.preventDefault();
64
+ if (!emailVal && !fullPhone) { setError('Enter your email address or phone number to continue.'); return; }
65
+ if (emailVal && !isValidEmail(emailVal)) { setError('Enter a valid email address.'); return; }
66
+ setError(null);
67
+ setLoading(true);
68
+ try {
69
+ const res = await fetch('/api/consumer/initiate', {
70
+ method: 'POST',
71
+ headers: { 'Content-Type': 'application/json' },
72
+ body: JSON.stringify({ email: emailVal, phone: fullPhone }),
73
+ });
74
+ const result = await res.json().catch(() => ({ exists: null }));
75
+ if (result.exists === true) {
76
+ setWelcomeName(result.firstName ?? null);
77
+ setStep(result.hasPassword === false ? 'new' : 'returning');
78
+ } else if (result.exists === false) {
79
+ setStep('new');
80
+ } else {
81
+ setError('Something went wrong. Please check your connection and try again.');
82
+ }
83
+ } catch {
84
+ setError('Something went wrong. Please check your connection and try again.');
85
+ } finally {
86
+ setLoading(false);
87
+ }
88
+ };
89
+
90
+ const handleSignIn = async (e: React.FormEvent) => {
91
+ e.preventDefault();
92
+ setError(null);
93
+ setLoading(true);
94
+ try {
95
+ const res = await fetch('/api/consumer/login', {
96
+ method: 'POST',
97
+ headers: { 'Content-Type': 'application/json' },
98
+ body: JSON.stringify({ email: emailVal, phone: fullPhone, password }),
99
+ });
100
+ const result = await res.json().catch(() => ({}));
101
+ if (!res.ok) {
102
+ if (result.code === 'not_found' || result.code === 'no_password') {
103
+ setStep('new');
104
+ setError(null);
105
+ return;
106
+ }
107
+ setError(result.error || 'Login failed. Please try again.');
108
+ return;
109
+ }
110
+ if (onSuccess) onSuccess(); else router.refresh();
111
+ } catch {
112
+ setError('Something went wrong. Please check your connection and try again.');
113
+ } finally {
114
+ setLoading(false);
115
+ }
116
+ };
117
+
118
+ const handleSignUp = async (e: React.FormEvent) => {
119
+ e.preventDefault();
120
+ if (password !== passwordConfirm) { setError('Passwords do not match.'); return; }
121
+ setError(null);
122
+ setLoading(true);
123
+ try {
124
+ const res = await fetch('/api/consumer/signup', {
125
+ method: 'POST',
126
+ headers: { 'Content-Type': 'application/json' },
127
+ body: JSON.stringify({
128
+ email: emailVal,
129
+ phone: fullPhone,
130
+ password,
131
+ password_confirmation: passwordConfirm,
132
+ first_name: firstName.trim() || undefined,
133
+ last_name: lastName.trim() || undefined,
134
+ }),
135
+ });
136
+ const result = await res.json().catch(() => ({}));
137
+ if (!res.ok) { setError(result.error || 'Signup failed. Please try again.'); return; }
138
+ if (onSuccess) onSuccess(); else router.refresh();
139
+ } catch {
140
+ setError('Something went wrong. Please check your connection and try again.');
141
+ } finally {
142
+ setLoading(false);
143
+ }
144
+ };
145
+
146
+ const goBack = () => {
147
+ setStep('identifier');
148
+ setPassword('');
149
+ setPasswordConfirm('');
150
+ setError(null);
151
+ };
152
+
153
+ const identifierSummary = [emailVal, fullPhone].filter(Boolean).join(' / ');
154
+
155
+ const headerTitle =
156
+ step === 'identifier' ? 'Pricing & Booking Portal' :
157
+ step === 'returning' ? (welcomeName ? `Welcome back, ${welcomeName}!` : 'Welcome back!') :
158
+ (welcomeName ? `Welcome back, ${welcomeName}!` : 'Welcome!');
159
+
160
+ const headerSubtitle =
161
+ step === 'identifier' ? 'Enter your email or phone number to get started.' :
162
+ step === 'returning' ? 'Enter your password to sign in.' :
163
+ (welcomeName ? 'Create a password to access your account.' : "Let's get you set up.");
164
+
165
+ return (
166
+ <div>
167
+ {/* Header */}
168
+ <div className="flex items-start justify-between gap-4 mb-5">
169
+ <div>
170
+ <h3 className="text-lg font-normal text-primary">{headerTitle}</h3>
171
+ <p className="mt-0.5 text-sm text-tertiary">{headerSubtitle}</p>
172
+ </div>
173
+ {onClose && (
174
+ <button
175
+ type="button"
176
+ onClick={onClose}
177
+ className="shrink-0 p-1 text-tertiary hover:text-primary transition-colors"
178
+ aria-label="Close"
179
+ >
180
+ <svg className="size-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
181
+ <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
182
+ </svg>
183
+ </button>
184
+ )}
185
+ </div>
186
+
187
+ {error && (
188
+ <div className="mb-4 rounded border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">
189
+ {error}
190
+ </div>
191
+ )}
192
+
193
+ {/* Step: identifier */}
194
+ {step === 'identifier' && (
195
+ <form onSubmit={handleContinue} className="space-y-3">
196
+ <div>
197
+ <label className={labelClass}>Email address</label>
198
+ <input
199
+ type="email"
200
+ value={email}
201
+ onChange={(e) => setEmail(e.target.value)}
202
+ placeholder="you@example.com"
203
+ className={inputClass}
204
+ autoComplete="email"
205
+ autoFocus
206
+ />
207
+ </div>
208
+ <div className="flex items-center gap-3">
209
+ <hr className="flex-1 border-gray-200" />
210
+ <span className="text-xs text-gray-400">or</span>
211
+ <hr className="flex-1 border-gray-200" />
212
+ </div>
213
+ <div>
214
+ <label className={labelClass}>Phone number</label>
215
+ <div className="flex rounded border border-gray-300 overflow-hidden focus-within:border-gray-700 focus-within:ring-1 focus-within:ring-gray-700 transition-colors">
216
+ <select
217
+ value={selectedCountry}
218
+ onChange={(e) => { setSelectedCountry(e.target.value); setPhoneValue(''); }}
219
+ className="border-r border-gray-200 bg-gray-50 px-2 py-2.5 text-sm text-gray-700 focus:outline-none"
220
+ aria-label="Country code"
221
+ >
222
+ {countryOptions.map((opt) => (
223
+ <option key={opt.value} value={opt.value}>{opt.label}</option>
224
+ ))}
225
+ </select>
226
+ <input
227
+ type="tel"
228
+ value={phoneValue}
229
+ onChange={handlePhoneChange}
230
+ placeholder={nationalPlaceholder}
231
+ className="flex-1 px-3 py-2.5 text-sm text-gray-900 placeholder-gray-400 bg-transparent focus:outline-none"
232
+ />
233
+ </div>
234
+ </div>
235
+ <div className="pt-1">
236
+ <button
237
+ type="submit"
238
+ disabled={loading}
239
+ className="w-full rounded border border-secondary bg-primary px-4 py-2.5 text-sm font-medium text-primary hover:bg-secondary transition-colors disabled:opacity-50"
240
+ >
241
+ {loading ? 'Looking you up…' : 'Continue'}
242
+ </button>
243
+ </div>
244
+ </form>
245
+ )}
246
+
247
+ {/* Step: returning user */}
248
+ {step === 'returning' && (
249
+ <form onSubmit={handleSignIn} className="space-y-3">
250
+ <div>
251
+ <label className={labelClass}>Password</label>
252
+ <input
253
+ type="password"
254
+ value={password}
255
+ onChange={(e) => setPassword(e.target.value)}
256
+ placeholder="••••••••"
257
+ className={inputClass}
258
+ required
259
+ autoComplete="current-password"
260
+ autoFocus
261
+ />
262
+ </div>
263
+ <div className="pt-1">
264
+ <button
265
+ type="submit"
266
+ disabled={loading}
267
+ className="w-full rounded border border-secondary bg-primary px-4 py-2.5 text-sm font-medium text-primary hover:bg-secondary transition-colors disabled:opacity-50"
268
+ >
269
+ {loading ? 'Signing in…' : 'Sign in'}
270
+ </button>
271
+ </div>
272
+ <button
273
+ type="button"
274
+ onClick={goBack}
275
+ className="w-full text-center text-sm text-tertiary hover:text-primary transition-colors"
276
+ >
277
+ ← {identifierSummary ? `Not ${identifierSummary}?` : 'Change email or phone'}
278
+ </button>
279
+ </form>
280
+ )}
281
+
282
+ {/* Step: new user / claim account */}
283
+ {step === 'new' && (
284
+ <form onSubmit={handleSignUp} className="space-y-3">
285
+ {/* Only ask for name if we don't already know who they are */}
286
+ {!welcomeName && (
287
+ <div className="grid grid-cols-2 gap-3">
288
+ <div>
289
+ <label className={labelClass}>First name</label>
290
+ <input
291
+ type="text"
292
+ value={firstName}
293
+ onChange={(e) => setFirstName(e.target.value)}
294
+ placeholder="Jane"
295
+ className={inputClass}
296
+ autoComplete="given-name"
297
+ autoFocus
298
+ />
299
+ </div>
300
+ <div>
301
+ <label className={labelClass}>Last name</label>
302
+ <input
303
+ type="text"
304
+ value={lastName}
305
+ onChange={(e) => setLastName(e.target.value)}
306
+ placeholder="Smith"
307
+ className={inputClass}
308
+ autoComplete="family-name"
309
+ />
310
+ </div>
311
+ </div>
312
+ )}
313
+ <div>
314
+ <label className={labelClass}>Password</label>
315
+ <input
316
+ type="password"
317
+ value={password}
318
+ onChange={(e) => setPassword(e.target.value)}
319
+ placeholder="••••••••"
320
+ className={inputClass}
321
+ required
322
+ autoComplete="new-password"
323
+ autoFocus={!!welcomeName}
324
+ />
325
+ </div>
326
+ <div>
327
+ <label className={labelClass}>Confirm password</label>
328
+ <input
329
+ type="password"
330
+ value={passwordConfirm}
331
+ onChange={(e) => setPasswordConfirm(e.target.value)}
332
+ placeholder="••••••••"
333
+ className={inputClass}
334
+ required
335
+ autoComplete="new-password"
336
+ />
337
+ </div>
338
+ <div className="pt-1">
339
+ <button
340
+ type="submit"
341
+ disabled={loading}
342
+ className="w-full rounded border border-secondary bg-primary px-4 py-2.5 text-sm font-medium text-primary hover:bg-secondary transition-colors disabled:opacity-50"
343
+ >
344
+ {loading ? 'Creating account…' : 'Create account'}
345
+ </button>
346
+ </div>
347
+ <button
348
+ type="button"
349
+ onClick={goBack}
350
+ className="w-full text-center text-sm text-tertiary hover:text-primary transition-colors"
351
+ >
352
+ ← Back
353
+ </button>
354
+ </form>
355
+ )}
356
+ </div>
357
+ );
358
+ }
@@ -0,0 +1,63 @@
1
+ 'use client';
2
+
3
+ import React, { useState, useEffect } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import { Modal } from '../elements/modal/modal';
6
+ import { LoginForm } from './LoginForm';
7
+ import { KeystoneLogoMinimal } from '../logo/keystone-logo-minimal';
8
+
9
+ const keystoneFooter = (
10
+ <div className="px-6 py-4 bg-secondary flex flex-col items-center gap-1">
11
+ <div className="flex items-center gap-1.5">
12
+ <KeystoneLogoMinimal className="size-5 shrink-0" />
13
+ <span className="text-sm font-medium text-primary">Keystone</span>
14
+ </div>
15
+ <p className="text-xs text-quaternary">Powered by Keystone Universal Login</p>
16
+ </div>
17
+ );
18
+
19
+ /**
20
+ * Renders a single login modal for the entire portal.
21
+ * Any server-rendered element with `data-open-login-modal` will open it on click.
22
+ * After successful auth the server component re-renders with the new cookie.
23
+ */
24
+ export function LoginModalController() {
25
+ const [open, setOpen] = useState(false);
26
+ const [redirectUrl, setRedirectUrl] = useState<string | null>(null);
27
+ const router = useRouter();
28
+
29
+ useEffect(() => {
30
+ const handleClick = (e: MouseEvent) => {
31
+ const trigger = (e.target as HTMLElement).closest('[data-open-login-modal]');
32
+ if (trigger) {
33
+ e.preventDefault();
34
+ setRedirectUrl(trigger.getAttribute('data-login-redirect'));
35
+ setOpen(true);
36
+ }
37
+ };
38
+ document.addEventListener('click', handleClick);
39
+ return () => document.removeEventListener('click', handleClick);
40
+ }, []);
41
+
42
+ const handleSuccess = () => {
43
+ setOpen(false);
44
+ if (redirectUrl) {
45
+ router.push(redirectUrl);
46
+ setRedirectUrl(null);
47
+ } else {
48
+ router.refresh();
49
+ }
50
+ };
51
+
52
+ return (
53
+ <Modal
54
+ isOpen={open}
55
+ onClose={() => setOpen(false)}
56
+ hideHeader
57
+ ariaLabel="Sign in or create account"
58
+ footer={keystoneFooter}
59
+ >
60
+ <LoginForm onSuccess={handleSuccess} onClose={() => setOpen(false)} />
61
+ </Modal>
62
+ );
63
+ }
@@ -0,0 +1,22 @@
1
+ 'use client';
2
+
3
+ import { useRouter } from 'next/navigation';
4
+
5
+ export function LogoutButton() {
6
+ const router = useRouter();
7
+
8
+ const handleLogout = async () => {
9
+ await fetch('/api/consumer/logout', { method: 'POST' });
10
+ router.refresh();
11
+ };
12
+
13
+ return (
14
+ <button
15
+ type="button"
16
+ onClick={handleLogout}
17
+ className="text-xs text-tertiary hover:text-primary transition-colors"
18
+ >
19
+ Sign out
20
+ </button>
21
+ );
22
+ }
@@ -0,0 +1,92 @@
1
+ 'use client';
2
+
3
+ import React, { useState, useTransition, useRef, useEffect } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+
6
+ export function MessageComposer({ contactId }: { contactId: number }) {
7
+ const [body, setBody] = useState('');
8
+ const [error, setError] = useState<string | null>(null);
9
+ const [isPending, startTransition] = useTransition();
10
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
11
+ const router = useRouter();
12
+
13
+ // Auto-resize textarea
14
+ useEffect(() => {
15
+ const el = textareaRef.current;
16
+ if (!el) return;
17
+ el.style.height = 'auto';
18
+ el.style.height = `${Math.min(el.scrollHeight, 120)}px`;
19
+ }, [body]);
20
+
21
+ function submit() {
22
+ if (!body.trim() || isPending) return;
23
+ setError(null);
24
+ startTransition(async () => {
25
+ try {
26
+ const res = await fetch('/api/chat', {
27
+ method: 'POST',
28
+ headers: { 'Content-Type': 'application/json' },
29
+ body: JSON.stringify({ contact_id: contactId, body: body.trim() }),
30
+ });
31
+ const result = await res.json().catch(() => ({}));
32
+ if (!res.ok) {
33
+ setError(result.error || 'Failed to send message.');
34
+ } else {
35
+ setBody('');
36
+ router.refresh();
37
+ }
38
+ } catch {
39
+ setError('Failed to send message.');
40
+ }
41
+ });
42
+ }
43
+
44
+ function handleSubmit(e: React.FormEvent) {
45
+ e.preventDefault();
46
+ submit();
47
+ }
48
+
49
+ function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
50
+ if (e.key === 'Enter' && !e.shiftKey) {
51
+ e.preventDefault();
52
+ submit();
53
+ }
54
+ }
55
+
56
+ return (
57
+ <form onSubmit={handleSubmit} className="border-t border-secondary px-4 py-3">
58
+ {error && (
59
+ <p className="mb-2 text-xs text-red-600">{error}</p>
60
+ )}
61
+ <div className="flex items-end gap-2">
62
+ <textarea
63
+ ref={textareaRef}
64
+ value={body}
65
+ onChange={(e) => setBody(e.target.value)}
66
+ onKeyDown={handleKeyDown}
67
+ placeholder="Type a message… (Enter to send)"
68
+ rows={1}
69
+ disabled={isPending}
70
+ className="flex-1 resize-none rounded-xl border border-gray-300 bg-primary px-3 py-2 text-sm text-primary placeholder-gray-400 focus:border-gray-700 focus:outline-none focus:ring-1 focus:ring-gray-700 transition-colors disabled:opacity-50"
71
+ />
72
+ <button
73
+ type="submit"
74
+ disabled={!body.trim() || isPending}
75
+ className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-gray-900 text-white transition-colors hover:bg-gray-700 disabled:opacity-40 disabled:cursor-not-allowed"
76
+ aria-label="Send message"
77
+ >
78
+ {isPending ? (
79
+ <svg className="size-4 animate-spin" fill="none" viewBox="0 0 24 24">
80
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
81
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
82
+ </svg>
83
+ ) : (
84
+ <svg className="size-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
85
+ <path strokeLinecap="round" strokeLinejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.269 20.876L5.999 12zm0 0h7.5" />
86
+ </svg>
87
+ )}
88
+ </button>
89
+ </div>
90
+ </form>
91
+ );
92
+ }