143 lines
4.2 KiB
TypeScript
143 lines
4.2 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
|
|
import { TextInput } from "@/components/ui/TextInput/TextInput";
|
|
|
|
import type { FormScreenDefinition } from "@/lib/funnel/types";
|
|
import { TemplateLayout } from "../layouts/TemplateLayout";
|
|
|
|
interface FormTemplateProps {
|
|
screen: FormScreenDefinition;
|
|
formData: Record<string, string>;
|
|
onFormDataChange: (data: Record<string, string>) => void;
|
|
onContinue: () => void;
|
|
canGoBack: boolean;
|
|
onBack: () => void;
|
|
screenProgress?: { current: number; total: number };
|
|
defaultTexts?: { nextButton?: string; continueButton?: string };
|
|
}
|
|
|
|
export function FormTemplate({
|
|
screen,
|
|
formData,
|
|
onFormDataChange,
|
|
onContinue,
|
|
canGoBack,
|
|
onBack,
|
|
screenProgress,
|
|
defaultTexts,
|
|
}: FormTemplateProps) {
|
|
const [localFormData, setLocalFormData] = useState<Record<string, string>>(formData);
|
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
|
|
useEffect(() => {
|
|
setLocalFormData(formData);
|
|
}, [formData]);
|
|
|
|
useEffect(() => {
|
|
onFormDataChange(localFormData);
|
|
}, [localFormData, onFormDataChange]);
|
|
|
|
const validateField = (fieldId: string, value: string): string | null => {
|
|
const field = screen.fields.find(f => f.id === fieldId);
|
|
if (!field) return null;
|
|
|
|
if (field.required && !value.trim()) {
|
|
return screen.validationMessages?.required?.replace('${field}', field.label || field.id) || `${field.label || field.id} is required`;
|
|
}
|
|
|
|
if (field.maxLength && value.length > field.maxLength) {
|
|
return screen.validationMessages?.maxLength?.replace('${maxLength}', String(field.maxLength)) || `Maximum ${field.maxLength} characters allowed`;
|
|
}
|
|
|
|
if (field.validation?.pattern) {
|
|
const regex = new RegExp(field.validation.pattern);
|
|
if (!regex.test(value)) {
|
|
return field.validation.message || screen.validationMessages?.invalidFormat || "Invalid format";
|
|
}
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
const handleFieldChange = (fieldId: string, value: string) => {
|
|
setLocalFormData(prev => ({ ...prev, [fieldId]: value }));
|
|
|
|
if (errors[fieldId]) {
|
|
setErrors(prev => {
|
|
const newErrors = { ...prev };
|
|
delete newErrors[fieldId];
|
|
return newErrors;
|
|
});
|
|
}
|
|
};
|
|
|
|
const validateForm = () => {
|
|
const newErrors: Record<string, string> = {};
|
|
|
|
screen.fields.forEach(field => {
|
|
const value = localFormData[field.id] || "";
|
|
const error = validateField(field.id, value);
|
|
if (error) {
|
|
newErrors[field.id] = error;
|
|
}
|
|
});
|
|
|
|
setErrors(newErrors);
|
|
return Object.keys(newErrors).length === 0;
|
|
};
|
|
|
|
const handleContinue = () => {
|
|
if (validateForm()) {
|
|
onContinue();
|
|
}
|
|
};
|
|
|
|
const isFormComplete = screen.fields.every(field => {
|
|
const value = localFormData[field.id] || "";
|
|
if (field.required) {
|
|
return value.trim().length > 0;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
return (
|
|
<TemplateLayout
|
|
screen={screen}
|
|
canGoBack={canGoBack}
|
|
onBack={onBack}
|
|
screenProgress={screenProgress}
|
|
titleDefaults={{ font: "manrope", weight: "bold", align: "left", size: "2xl", color: "default" }}
|
|
subtitleDefaults={{ font: "manrope", weight: "medium", color: "default", align: "left", size: "lg" }}
|
|
actionButtonOptions={{
|
|
defaultText: defaultTexts?.continueButton || "Continue",
|
|
disabled: !isFormComplete,
|
|
onClick: handleContinue,
|
|
}}
|
|
>
|
|
<div className="w-full mt-[22px] space-y-4">
|
|
{screen.fields.map((field) => (
|
|
<div key={field.id}>
|
|
<TextInput
|
|
label={field.label}
|
|
placeholder={field.placeholder}
|
|
type={field.type || "text"}
|
|
value={localFormData[field.id] || ""}
|
|
onChange={(e) => handleFieldChange(field.id, e.target.value)}
|
|
maxLength={field.maxLength}
|
|
aria-invalid={!!errors[field.id]}
|
|
aria-errormessage={errors[field.id]}
|
|
/>
|
|
{errors[field.id] && (
|
|
<p className="text-destructive font-inter font-medium text-xs mt-1">
|
|
{errors[field.id]}
|
|
</p>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</TemplateLayout>
|
|
);
|
|
}
|