@windstream/react-shared-components 0.2.45 → 0.2.46

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.
@@ -0,0 +1,557 @@
1
+ "use client";
2
+
3
+ import React, { useState } from "react";
4
+ import { Button } from "../../../components/button";
5
+ import { Input } from "../../../components/input";
6
+ import { MaterialIcon } from "../../../components/material-icon";
7
+ import { Select, type SelectOption } from "../../../components/select";
8
+ import { Text } from "../../../components/text";
9
+ import { Tooltip } from "../../../components/tooltip";
10
+ import { cx } from "../../../utils";
11
+ import {
12
+ fiberFormAddressCollectionFormSchema,
13
+ fiberFormInputInitialValues,
14
+ fiberFormInputList,
15
+ } from "./constants";
16
+ import type {
17
+ FiberAddress,
18
+ FiberAddressCandidate,
19
+ FiberFormErrorProps,
20
+ FiberFormProps,
21
+ FiberFormValues,
22
+ } from "./types";
23
+ import { Field, Form, Formik, type FieldProps } from "formik";
24
+
25
+ const phonePattern = /\D/g;
26
+ const formatPhone = (value: string) => {
27
+ const digits = value.replace(phonePattern, "").slice(0, 10);
28
+ if (digits.length < 4) return digits;
29
+ if (digits.length < 7) return `(${digits.slice(0, 3)}) ${digits.slice(3)}`;
30
+ return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`;
31
+ };
32
+
33
+ const ErrorMessage = ({ name, errors, touched }: FiberFormErrorProps) => {
34
+ const error = errors[name];
35
+ return touched[name] && typeof error === "string" ? (
36
+ <Text className="mt-1 text-text-critical">{error}</Text>
37
+ ) : null;
38
+ };
39
+
40
+ const FieldLabel = ({
41
+ children,
42
+ required = false,
43
+ }: {
44
+ children: React.ReactNode;
45
+ required?: boolean;
46
+ }) => (
47
+ <label className="mb-1 block text-sm font-medium text-text">
48
+ {children}
49
+ {required && <span className="text-text-critical">*</span>}
50
+ </label>
51
+ );
52
+
53
+ export const FiberForm: React.FC<FiberFormProps> = ({
54
+ InputComponent,
55
+ AddressAutocomplete,
56
+ initialValues = fiberFormInputInitialValues,
57
+ inputList = fiberFormInputList,
58
+ formSchema = fiberFormAddressCollectionFormSchema,
59
+ onSubmit,
60
+ lookupAddress,
61
+ buttonText = "See plans",
62
+ buttonClassName,
63
+ bottomTextDisclaimer,
64
+ hideLegalCopy = false,
65
+ checkBoxEnabled = true,
66
+ copyRightLinks,
67
+ className,
68
+ onAddressEvent,
69
+ }) => {
70
+ const FormInput = InputComponent ?? Input;
71
+ const [candidates, setCandidates] = useState<FiberAddressCandidate[]>([]);
72
+ const [showCandidates, setShowCandidates] = useState(false);
73
+ const [unitOptions, setUnitOptions] = useState<SelectOption[]>([]);
74
+ const [selectedUnit, setSelectedUnit] = useState<SelectOption>();
75
+ const [showManualUnit, setShowManualUnit] = useState(false);
76
+ const placeholderBody2Style = {
77
+ fontSize: "1.125rem",
78
+ lineHeight: "1.75rem",
79
+ fontWeight: 500,
80
+ };
81
+ const findCandidates = async (address: string, values: FiberFormValues) => {
82
+ onAddressEvent?.(address);
83
+ if (!lookupAddress || address.length < 3) {
84
+ setCandidates([]);
85
+ setShowCandidates(false);
86
+ return;
87
+ }
88
+ try {
89
+ const result = await lookupAddress(address, values);
90
+ setCandidates(result);
91
+ setShowCandidates(result.length > 0);
92
+ } catch {
93
+ setCandidates([]);
94
+ setShowCandidates(false);
95
+ }
96
+ };
97
+
98
+ const selectCandidate = (
99
+ candidate: FiberAddressCandidate,
100
+ setFieldValue: (
101
+ field: string,
102
+ value: unknown,
103
+ shouldValidate?: boolean
104
+ ) => void
105
+ ) => {
106
+ const fullAddress = [
107
+ candidate.addressLine1,
108
+ candidate.city,
109
+ candidate.state,
110
+ candidate.postalCode,
111
+ ]
112
+ .filter(Boolean)
113
+ .join(", ");
114
+ const units = (candidate.units ?? []).filter(unit => unit.addressLine2);
115
+ setFieldValue("address", fullAddress);
116
+ setFieldValue("addressToSend", candidate);
117
+ setFieldValue("zip", candidate.postalCode ?? "", false);
118
+ setFieldValue("isManualAddress", false, false);
119
+ setFieldValue("hasUnits", units.length > 0, false);
120
+ setFieldValue(
121
+ "unit",
122
+ units.length > 0 ? "" : (candidate.addressLine2 ?? ""),
123
+ false
124
+ );
125
+ setCandidates([]);
126
+ setShowCandidates(false);
127
+ setSelectedUnit(undefined);
128
+ setUnitOptions([
129
+ ...units.map(unit => ({
130
+ label: unit.addressLine2,
131
+ value: unit.dfAddressId ?? "",
132
+ data: unit,
133
+ })),
134
+ ...(units.length > 0
135
+ ? [
136
+ { label: "I don't see my unit", value: "manual" },
137
+ { label: "I don't have a unit", value: "no-unit" },
138
+ ]
139
+ : []),
140
+ ]);
141
+ };
142
+
143
+ return (
144
+ <Formik
145
+ initialValues={initialValues}
146
+ validationSchema={formSchema}
147
+ onSubmit={async (values, helpers) => {
148
+ await onSubmit(values);
149
+ helpers.resetForm({ values: initialValues });
150
+ setSelectedUnit(undefined);
151
+ setUnitOptions([]);
152
+ setShowManualUnit(false);
153
+ }}
154
+ >
155
+ {({
156
+ errors,
157
+ touched,
158
+ isSubmitting,
159
+ submitCount,
160
+ setFieldValue,
161
+ values,
162
+ }) => (
163
+ <Form className={cx("w-full", className)}>
164
+ <div className="flex flex-col gap-5 sm:flex-row">
165
+ {inputList.firstName && (
166
+ <div className="flex-1">
167
+ <FieldLabel required>First name</FieldLabel>
168
+ <Field
169
+ as={FormInput}
170
+ style={placeholderBody2Style}
171
+ name="firstName"
172
+ placeholder={inputList.firstName.placeholder ?? "First name"}
173
+ />
174
+ <ErrorMessage
175
+ name="firstName"
176
+ errors={errors}
177
+ touched={touched}
178
+ />
179
+ </div>
180
+ )}
181
+ {inputList.lastName && (
182
+ <div className="flex-1">
183
+ <FieldLabel required>Last name</FieldLabel>
184
+ <Field
185
+ as={FormInput}
186
+ style={placeholderBody2Style}
187
+ name="lastName"
188
+ placeholder={inputList.lastName.placeholder ?? "Last name"}
189
+ />
190
+ <ErrorMessage
191
+ name="lastName"
192
+ errors={errors}
193
+ touched={touched}
194
+ />
195
+ </div>
196
+ )}
197
+ </div>
198
+
199
+ {inputList.address && (
200
+ <div className="mt-8 flex flex-col gap-5 sm:flex-row">
201
+ <div className="sm:flex-[3]">
202
+ <FieldLabel required>Address</FieldLabel>
203
+ <Field name="address">
204
+ {({ field }: FieldProps) => (
205
+ <AddressAutocomplete
206
+ value={field.value || ""}
207
+ placeholder="Address, city, state, ZIP"
208
+ onChange={address => {
209
+ setFieldValue("address", address, false);
210
+ setFieldValue("isManualAddress", true, false);
211
+ void findCandidates(address, values);
212
+ }}
213
+ onSelectAddress={result => {
214
+ if (!result) return;
215
+ const units = (result.addressToSend.units ?? []).filter(
216
+ unit => unit.addressLine2
217
+ );
218
+ setFieldValue("address", result.address);
219
+ setFieldValue("addressToSend", result.addressToSend);
220
+ setFieldValue("unit", result.unit ?? "", false);
221
+ setFieldValue("isManualAddress", false, false);
222
+ setFieldValue("hasUnits", units.length > 0, false);
223
+ setUnitOptions([
224
+ ...units.map(unit => ({
225
+ label: unit.addressLine2,
226
+ value: unit.dfAddressId ?? "",
227
+ data: unit,
228
+ })),
229
+ ...(units.length > 0
230
+ ? [
231
+ {
232
+ label: "I don't see my unit",
233
+ value: "manual",
234
+ },
235
+ {
236
+ label: "I don't have a unit",
237
+ value: "no-unit",
238
+ },
239
+ ]
240
+ : []),
241
+ ]);
242
+ }}
243
+ onKeyDown={event => {
244
+ if (event.key === "Enter") event.preventDefault();
245
+ }}
246
+ />
247
+ )}
248
+ </Field>
249
+ <ErrorMessage
250
+ name="address"
251
+ errors={errors}
252
+ touched={touched}
253
+ />
254
+ {showCandidates && (
255
+ <ul className="shadow-dark-drop mt-2 max-h-56 overflow-auto rounded-lg border border-input-border bg-white">
256
+ {candidates.map((candidate, index) => (
257
+ <li
258
+ key={`${candidate.dfAddressId ?? index}-${candidate.addressLine1}`}
259
+ >
260
+ <button
261
+ type="button"
262
+ className="flex w-full items-center gap-2 px-4 py-3 text-left hover:bg-bg-surface-hover"
263
+ onClick={() =>
264
+ selectCandidate(candidate, setFieldValue)
265
+ }
266
+ >
267
+ <MaterialIcon
268
+ name="location_on"
269
+ size={24}
270
+ fill={1}
271
+ className="text-green-600"
272
+ />
273
+ {[
274
+ candidate.addressLine1,
275
+ candidate.city,
276
+ candidate.state,
277
+ candidate.postalCode,
278
+ ]
279
+ .filter(Boolean)
280
+ .join(", ")}
281
+ </button>
282
+ </li>
283
+ ))}
284
+ </ul>
285
+ )}
286
+ </div>
287
+ <div className="w-full sm:w-[216px] sm:flex-none">
288
+ <div className="mb-1 hidden h-[20px] sm:block" />
289
+ <Select
290
+ options={unitOptions}
291
+ value={selectedUnit}
292
+ placeholder="Apt, Suite, etc."
293
+ maxMenuHeight={200}
294
+ components={{
295
+ DropdownIndicator: () => (
296
+ <svg
297
+ width="20"
298
+ height="20"
299
+ viewBox="0 0 20 20"
300
+ fill="none"
301
+ xmlns="http://www.w3.org/2000/svg"
302
+ aria-hidden="true"
303
+ >
304
+ <path
305
+ d="M5 7.5L10 12.5L15 7.5"
306
+ stroke="#1B2A4E"
307
+ strokeWidth="1.5"
308
+ strokeLinecap="round"
309
+ strokeLinejoin="round"
310
+ />
311
+ </svg>
312
+ ),
313
+ }}
314
+ styles={{
315
+ option: base => ({ ...base, ...placeholderBody2Style }),
316
+ singleValue: base => ({
317
+ ...base,
318
+ ...placeholderBody2Style,
319
+ }),
320
+ placeholder: base => ({
321
+ ...base,
322
+ ...placeholderBody2Style,
323
+ }),
324
+ input: base => ({ ...base, ...placeholderBody2Style }),
325
+ indicatorsContainer: base => ({
326
+ ...base,
327
+ alignItems: "center",
328
+ }),
329
+ }}
330
+ className="w-full sm:w-[216px]"
331
+ controlClassName="!h-[56px] !min-h-[56px] !gap-2 !rounded-[12px] !border !border-solid !border-[#CECECE] !bg-white !pl-4 !pr-4 !flex-nowrap [&>div:first-child]:!overflow-visible [&>div:first-child>div]:!max-w-none [&>div:first-child>div]:!overflow-visible [&>div:first-child>div]:!whitespace-nowrap"
332
+ onChange={option => {
333
+ const selected = option as SelectOption | null;
334
+ if (!selected) return;
335
+ if (selected.value === "manual") {
336
+ setShowManualUnit(true);
337
+ setFieldValue("hasUnits", true, false);
338
+ setFieldValue("unit", "", false);
339
+ setFieldValue("addressToSend", {
340
+ ...values.addressToSend,
341
+ addressLine2: "",
342
+ dfAddressId: undefined,
343
+ } as FiberAddress);
344
+ setSelectedUnit(selected);
345
+ return;
346
+ }
347
+ if (selected.value === "no-unit") {
348
+ setShowManualUnit(false);
349
+ setFieldValue("unit", "", false);
350
+ setFieldValue("hasUnits", false, false);
351
+ setFieldValue("addressToSend", {
352
+ ...values.addressToSend,
353
+ addressLine2: "",
354
+ dfAddressId: undefined,
355
+ } as FiberAddress);
356
+ setSelectedUnit(selected);
357
+ return;
358
+ }
359
+ setShowManualUnit(false);
360
+ setSelectedUnit(selected);
361
+ setFieldValue("unit", selected.label);
362
+ setFieldValue("hasUnits", false, false);
363
+ setFieldValue("addressToSend", {
364
+ ...values.addressToSend,
365
+ addressLine2: selected.label,
366
+ dfAddressId: selected.value,
367
+ } as FiberAddress);
368
+ }}
369
+ />
370
+ {!showManualUnit &&
371
+ submitCount > 0 &&
372
+ (values.hasUnits ?? false) &&
373
+ !selectedUnit &&
374
+ typeof errors.unit === "string" && (
375
+ <span className="mt-1 block text-sm font-medium text-red-500">
376
+ {errors.unit}
377
+ </span>
378
+ )}
379
+ {showManualUnit && (
380
+ <div className="mt-5 w-full sm:w-[216px]">
381
+ <label className="mb-1 block text-sm font-medium text-neutral-900">
382
+ Unit<span className="text-red-500">*</span>
383
+ </label>
384
+ <Field
385
+ as={FormInput}
386
+ style={placeholderBody2Style}
387
+ name="unit"
388
+ placeholder="Please enter your unit"
389
+ />
390
+ {submitCount > 0 && typeof errors.unit === "string" && (
391
+ <span className="mt-1 block text-sm font-medium text-red-500">
392
+ {errors.unit}
393
+ </span>
394
+ )}
395
+ </div>
396
+ )}
397
+ </div>
398
+ </div>
399
+ )}
400
+
401
+ <div className="mt-8 flex flex-col gap-5 sm:flex-row">
402
+ {inputList.phoneNumber && (
403
+ <div className="flex-1">
404
+ <FieldLabel required>Phone number</FieldLabel>
405
+ <Field name="phone">
406
+ {({ field, form }: FieldProps) => (
407
+ <FormInput
408
+ {...field}
409
+ type="tel"
410
+ style={placeholderBody2Style}
411
+ placeholder="(555) 555-5555"
412
+ onChange={event =>
413
+ form.setFieldValue(
414
+ "phone",
415
+ formatPhone(event.currentTarget.value)
416
+ )
417
+ }
418
+ />
419
+ )}
420
+ </Field>
421
+ <ErrorMessage name="phone" errors={errors} touched={touched} />
422
+ </div>
423
+ )}
424
+ {inputList.emailAddress && (
425
+ <div className="flex-1">
426
+ <FieldLabel required>Email address</FieldLabel>
427
+ <Field
428
+ as={FormInput}
429
+ style={placeholderBody2Style}
430
+ name="email"
431
+ type="email"
432
+ placeholder="john.doe@example.com"
433
+ />
434
+ <ErrorMessage name="email" errors={errors} touched={touched} />
435
+ </div>
436
+ )}
437
+ </div>
438
+
439
+ <div className="mt-8 flex flex-col gap-5 sm:flex-row">
440
+ {inputList.referralCode && (
441
+ <div className="flex-1">
442
+ <FieldLabel>Referral code</FieldLabel>
443
+ <Field
444
+ as={FormInput}
445
+ style={placeholderBody2Style}
446
+ name="referralCode"
447
+ placeholder="Optional"
448
+ />
449
+ <ErrorMessage
450
+ name="referralCode"
451
+ errors={errors}
452
+ touched={touched}
453
+ />
454
+ </div>
455
+ )}
456
+ {inputList.notes && (
457
+ <div className="flex-1">
458
+ <FieldLabel>Notes or comments</FieldLabel>
459
+ <Field
460
+ as={FormInput}
461
+ style={placeholderBody2Style}
462
+ name="notes"
463
+ placeholder="Optional"
464
+ />
465
+ </div>
466
+ )}
467
+ </div>
468
+
469
+ {checkBoxEnabled && !hideLegalCopy && (
470
+ <div className="mt-16 text-neutral-600">
471
+ <Field name="isMarketingCom">
472
+ {({ field }: FieldProps) => (
473
+ <div className="mb-7 mt-4 flex items-center gap-1 pl-1">
474
+ <input
475
+ type="checkbox"
476
+ className="h-4 w-4 cursor-pointer align-middle accent-[#1d9d77]"
477
+ {...field}
478
+ checked={field.value}
479
+ id="recieve-message-checkbox"
480
+ />
481
+ <label
482
+ htmlFor="recieve-message-checkbox"
483
+ className="mx-1 mb-3 mr-0 cursor-pointer align-middle text-micro font-medium leading-6 text-neutral-600"
484
+ >
485
+ Receive text messages for installation, account updates,
486
+ and marketing communications.
487
+ </label>
488
+ <Tooltip
489
+ tooltipMsg="By opting in to receive text messages, you will receive automated text message related to service notifications including installations, service changes and updates, billing information, marketing offers from Kinetic including cart reminders. Consent is not a condition of purchase. Reply STOP at any time to unsubscribe. Message frequency varies. Msg & data rates may apply. To contact our Customer Support via email, chat, or phone, visit our Support page below."
490
+ className="ff-NR mt-0"
491
+ >
492
+ <MaterialIcon
493
+ name="info"
494
+ size={20}
495
+ className="tooltip"
496
+ style={{ verticalAlign: "top" }}
497
+ />
498
+ </Tooltip>
499
+ </div>
500
+ )}
501
+ </Field>
502
+ </div>
503
+ )}
504
+ {!hideLegalCopy && (
505
+ <Text className="body3 my-[32px] text-neutral-600 lg:mb-[64px] lg:mt-[32px]">
506
+ By submitting this form you consent to receive marketing messages
507
+ from Kinetic by text, email or phone to the contact provided.
508
+ Consent is not a condition of purchase. Msg &amp; data rates may
509
+ apply. Msg frequency varies. Unsubscribe at any time by replying
510
+ STOP or clicking the unsubscribe link.
511
+ </Text>
512
+ )}
513
+ {bottomTextDisclaimer && !hideLegalCopy && (
514
+ <Text className="mt-2 text-center text-text-secondary">
515
+ {bottomTextDisclaimer}
516
+ </Text>
517
+ )}
518
+ {copyRightLinks && !hideLegalCopy && (
519
+ <ul className="mt-16 flex list-disc justify-center gap-8 text-text-secondary">
520
+ {copyRightLinks.map(link => (
521
+ <li key={link.title}>
522
+ <a
523
+ href={link.href}
524
+ target="_blank"
525
+ rel="noreferrer"
526
+ className="underline"
527
+ >
528
+ {link.title}
529
+ </a>
530
+ </li>
531
+ ))}
532
+ </ul>
533
+ )}
534
+ <div className="mt-8 flex items-center justify-center px-[60px] lg:mt-[64px]">
535
+ <Button
536
+ type="submit"
537
+ disabled={isSubmitting}
538
+ className={cx(
539
+ "min-h-14 rounded-xl bg-bg-fill-brand px-8 py-4 font-bold text-white disabled:opacity-50",
540
+ buttonClassName
541
+ )}
542
+ >
543
+ {isSubmitting ? "Submitting..." : buttonText}
544
+ </Button>
545
+ </div>
546
+ </Form>
547
+ )}
548
+ </Formik>
549
+ );
550
+ };
551
+
552
+ export {
553
+ fiberFormAddressCollectionFormSchema,
554
+ fiberFormInputInitialValues,
555
+ fiberFormInputList,
556
+ } from "./constants";
557
+ export type * from "./types";
@@ -0,0 +1,92 @@
1
+ import type { ComponentType, KeyboardEvent, ReactNode } from "react";
2
+ import type { FormikErrors, FormikTouched, FormikValues } from "formik";
3
+ import type * as Yup from "yup";
4
+
5
+ export type FiberAddressUnit = {
6
+ addressLine2: string;
7
+ dfAddressId?: string;
8
+ };
9
+
10
+ export type FiberAddress = {
11
+ addressLine1?: string;
12
+ addressLine2?: string | null;
13
+ city?: string;
14
+ state?: string;
15
+ postalCode?: string;
16
+ dfAddressId?: string;
17
+ units?: FiberAddressUnit[];
18
+ [key: string]: unknown;
19
+ };
20
+
21
+ export type FiberAddressAutocompleteResult = {
22
+ address: string;
23
+ unit?: string;
24
+ addressToSend: FiberAddress;
25
+ placeId?: string;
26
+ };
27
+
28
+ export type FiberAddressAutocompleteProps = {
29
+ value: string;
30
+ placeholder?: string;
31
+ errorText?: string;
32
+ onChange: (address: string) => void;
33
+ onSelectAddress: (result?: FiberAddressAutocompleteResult) => void;
34
+ onKeyDown: (event: KeyboardEvent<HTMLElement>) => void;
35
+ };
36
+
37
+ export type FiberAddressCandidate = FiberAddress;
38
+
39
+ export type FiberFormValues = FormikValues & {
40
+ firstName: string;
41
+ lastName: string;
42
+ address: string;
43
+ phone: string;
44
+ email: string;
45
+ unit: string;
46
+ zip: string;
47
+ isManualAddress: boolean;
48
+ isMarketingCom: boolean;
49
+ addressToSend: FiberAddress;
50
+ notes: string;
51
+ referralCode: string;
52
+ hasUnits: boolean;
53
+ };
54
+
55
+ export type FiberFormInputList = Partial<{
56
+ firstName: { placeholder?: string };
57
+ lastName: { placeholder?: string };
58
+ address: { placeholder?: string };
59
+ phoneNumber: { placeholder?: string };
60
+ emailAddress: { placeholder?: string };
61
+ unit: { placeholder?: string };
62
+ zip: { placeholder?: string };
63
+ notes: { placeholder?: string };
64
+ referralCode: { placeholder?: string };
65
+ }>;
66
+
67
+ export type FiberFormProps = {
68
+ InputComponent?: ComponentType<any>;
69
+ AddressAutocomplete: ComponentType<FiberAddressAutocompleteProps>;
70
+ initialValues?: FiberFormValues;
71
+ inputList?: FiberFormInputList;
72
+ formSchema?: Yup.AnyObjectSchema;
73
+ onSubmit: (values: FiberFormValues) => Promise<void>;
74
+ lookupAddress?: (
75
+ address: string,
76
+ values: FiberFormValues
77
+ ) => Promise<FiberAddressCandidate[]>;
78
+ buttonText?: string;
79
+ buttonClassName?: string;
80
+ bottomTextDisclaimer?: ReactNode;
81
+ hideLegalCopy?: boolean;
82
+ checkBoxEnabled?: boolean;
83
+ copyRightLinks?: Array<{ title: string; href: string }>;
84
+ className?: string;
85
+ onAddressEvent?: (address: string) => void;
86
+ };
87
+
88
+ export type FiberFormErrorProps = {
89
+ name: string;
90
+ errors: FormikErrors<FiberFormValues>;
91
+ touched: FormikTouched<FiberFormValues>;
92
+ };
@@ -364,7 +364,7 @@ export const PrimaryHero: React.FC<PrimaryHeroProps> = props => {
364
364
  hasDesktopVideo && "h-[600px] w-[600px]"
365
365
  )}
366
366
  >
367
- <Badge className="absolute -left-26 top-18 z-10 aspect-square w-52 object-cover object-center" />
367
+ <Badge className="absolute -left-10 top-18 z-10 aspect-square w-52 object-cover object-center" />
368
368
  {!hasDesktopVideo && heroImageUrl ? (
369
369
  <NextImage
370
370
  src={heroImageUrl}