w-funnel/src/components/funnel/templates/FormTemplate/FormTemplate.tsx
dev.daminik00 b28d22967f story
2025-09-28 16:35:41 +02:00

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>
);
}