"use client"; import type React from "react"; import { useState } from "react"; import { cx } from "../lib/utils"; import { OnboardingButton } from "../primitives/onboarding-button"; import { OnboardingInput } from "../primitives/onboarding-input"; import { OnboardingLabel } from "../primitives/onboarding-label"; import { OnboardingSelect, OnboardingSelectContent, OnboardingSelectItem, OnboardingSelectTrigger, OnboardingSelectValue, } from "../primitives/onboarding-select"; export interface SelectOption { value: string; label: string; } export interface TextInputField { /** Unique identifier for the field */ id: string; /** Label displayed above the input */ label: string; /** Placeholder text */ placeholder?: string; /** Optional description/helper text */ description?: string; /** Field type: 'text' for input, 'select' for dropdown */ fieldType?: "text" | "select"; /** Input type (text, email, url, etc.) - only for text fields */ type?: "text" | "email" | "url" | "tel"; /** Options for select fields */ options?: SelectOption[]; /** Whether the field is required */ required?: boolean; /** Validation pattern (regex string) */ pattern?: string; /** Error message for invalid input */ errorMessage?: string; /** Default value */ defaultValue?: string; } export interface TextInputStepProps { /** Title displayed at the top of the step */ title?: string; /** Description text below the title */ description?: string; /** Array of text input fields */ fields: TextInputField[]; /** Called when any value changes */ onValuesChange?: (values: Record) => void; /** Called when the user submits the form */ onSubmit: (values: Record) => void | Promise; /** Text for the submit button */ submitText?: string; /** Text shown while submitting */ loadingText?: string; /** Optional back button config */ backButton?: { text: string; onClick: () => void; }; } export function TextInputStep({ title = "Enter your details", description = "Please fill in the information below.", fields, onValuesChange, onSubmit, submitText = "Continue", loadingText = "Submitting...", backButton, }: TextInputStepProps) { const [values, setValues] = useState>(() => { const initial: Record = {}; fields.forEach((field) => { initial[field.id] = field.defaultValue ?? ""; }); return initial; }); const [errors, setErrors] = useState>({}); const [touched, setTouched] = useState>({}); const [loading, setLoading] = useState(false); const validateField = ( field: TextInputField, value: string, ): string | null => { if (field.required && !value?.trim()) { return field.errorMessage || `${field.label} is required`; } if (field.fieldType === "select") { // Select fields only need required validation return null; } if (field.pattern && value) { const regex = new RegExp(field.pattern); if (!regex.test(value)) { return field.errorMessage || `Invalid ${field.label.toLowerCase()}`; } } if (field.type === "email" && value) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(value)) { return field.errorMessage || "Please enter a valid email address"; } } if (field.type === "url" && value) { try { new URL(value); } catch { return field.errorMessage || "Please enter a valid URL"; } } return null; }; const handleChange = (fieldId: string, value: string) => { const newValues = { ...values, [fieldId]: value }; setValues(newValues); onValuesChange?.(newValues); // Clear error on change if field was touched if (touched[fieldId]) { const field = fields.find((f) => f.id === fieldId); if (field) { const error = validateField(field, value); setErrors((prev) => ({ ...prev, [fieldId]: error || "" })); } } }; const handleBlur = (fieldId: string) => { setTouched((prev) => ({ ...prev, [fieldId]: true })); const field = fields.find((f) => f.id === fieldId); if (field) { const error = validateField(field, values[fieldId]); setErrors((prev) => ({ ...prev, [fieldId]: error || "" })); } }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); // Validate all fields const newErrors: Record = {}; const newTouched: Record = {}; let hasErrors = false; fields.forEach((field) => { newTouched[field.id] = true; const error = validateField(field, values[field.id]); if (error) { newErrors[field.id] = error; hasErrors = true; } }); setTouched(newTouched); setErrors(newErrors); if (hasErrors) return; setLoading(true); try { await onSubmit(values); } finally { setLoading(false); } }; const isValid = fields.every((field) => { if (field.required && !values[field.id]?.trim()) return false; return !errors[field.id]; }); return (

{title}

{description}

{fields.map((field, index) => (
{field.label} {field.required && ( * )} {field.description && (

{field.description}

)} {field.fieldType === "select" ? ( { handleChange(field.id, value); setTouched((prev) => ({ ...prev, [field.id]: true })); }} > {field.options?.map((option) => ( {option.label} ))} ) : ( handleChange(field.id, e.target.value)} onBlur={() => handleBlur(field.id)} aria-invalid={touched[field.id] && !!errors[field.id]} aria-describedby={ errors[field.id] ? `${field.id}-error` : undefined } className={cx( touched[field.id] && errors[field.id] && "border-destructive", )} /> )} {touched[field.id] && errors[field.id] && (

{errors[field.id]}

)}
))}
{backButton && ( {backButton.text} )} {loading ? loadingText : submitText}
); }