Enhance FormFieldWrapper with blur validation and toasts

Adds validation on blur mode to FormFieldWrapper, introduces animated validation states, and implements standardized form submission toasts via a new formToasts helper; updates ParkForm and RideForm to use the new toast system and to propagate error state with scroll-to-error support.
This commit is contained in:
gpt-engineer-app[bot]
2025-11-11 23:43:01 +00:00
parent 7d085a0702
commit 92e93bfc9d
5 changed files with 150 additions and 24 deletions

View File

@@ -75,6 +75,12 @@ interface FormFieldWrapperProps {
/** Hide automatic hint */
hideHint?: boolean;
/** When to show validation feedback */
validationMode?: 'realtime' | 'onBlur';
/** Callback when field is blurred (for onBlur mode) */
onBlur?: () => void;
}
/**
@@ -192,7 +198,10 @@ export function FormFieldWrapper({
textareaProps,
className,
hideHint = false,
validationMode = 'realtime',
onBlur,
}: FormFieldWrapperProps) {
const [hasBlurred, setHasBlurred] = React.useState(false);
const isTextarea = fieldType === 'textarea' || fieldType === 'submission-notes';
const autoHint = getAutoHint(fieldType);
const displayHint = hint || autoHint;
@@ -203,12 +212,27 @@ export function FormFieldWrapper({
const charCount = typeof value === 'string' ? value.length : 0;
// Determine validation state
const shouldShowValidation = validationMode === 'realtime' || (validationMode === 'onBlur' && hasBlurred);
const hasValue = value !== undefined && value !== null && value !== '';
const isValid = !error && hasValue;
const hasError = !!error;
const isValid = shouldShowValidation && !error && hasValue;
const hasError = shouldShowValidation && !!error;
// Blur handler
const handleBlur = (e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setHasBlurred(true);
if (validationMode === 'onBlur' && onBlur) {
onBlur();
}
// Call original onBlur if provided
if ('value' in e.target && textareaProps?.onBlur) {
textareaProps.onBlur(e as React.FocusEvent<HTMLTextAreaElement>);
} else if (inputProps?.onBlur) {
inputProps.onBlur(e as React.FocusEvent<HTMLInputElement>);
}
};
return (
<div className={cn("space-y-2", className)}>
<div className={cn("space-y-2", className)} data-error={hasError ? "true" : undefined}>
{/* Label with optional terminology tooltip */}
<Label htmlFor={id} className="flex items-center gap-2">
{termKey ? (
@@ -232,11 +256,13 @@ export function FormFieldWrapper({
<Textarea
id={id}
className={cn(
"pr-10",
error && "border-destructive",
isValid && "border-green-500/50"
"pr-10 transition-all duration-300 ease-in-out",
"focus:ring-2 focus:ring-primary/20 focus:border-primary",
error && "border-destructive focus:ring-destructive/20",
isValid && "border-green-500/50 focus:ring-green-500/20"
)}
maxLength={maxLength}
onBlur={handleBlur}
{...textareaProps}
/>
) : (
@@ -244,23 +270,25 @@ export function FormFieldWrapper({
id={id}
type={inputType}
className={cn(
"pr-10",
error && "border-destructive",
isValid && "border-green-500/50"
"pr-10 transition-all duration-300 ease-in-out",
"focus:ring-2 focus:ring-primary/20 focus:border-primary",
error && "border-destructive focus:ring-destructive/20",
isValid && "border-green-500/50 focus:ring-green-500/20"
)}
maxLength={maxLength}
onBlur={handleBlur}
{...inputProps}
/>
)}
{/* Validation icon */}
{/* Validation icon with animation */}
{(isValid || hasError) && (
<div className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none">
{isValid && (
<CheckCircle2 className="h-4 w-4 text-green-500" />
<CheckCircle2 className="h-4 w-4 text-green-500 animate-fade-in" />
)}
{hasError && (
<AlertCircle className="h-4 w-4 text-destructive" />
<AlertCircle className="h-4 w-4 text-destructive animate-fade-in" />
)}
</div>
)}
@@ -268,7 +296,7 @@ export function FormFieldWrapper({
{/* Hint text (if not hidden and exists) */}
{!hideHint && displayHint && !error && (
<p className="text-xs text-muted-foreground">
<p className="text-xs text-muted-foreground animate-slide-in-down">
{displayHint}
{showCharCount && ` (${charCount}/${maxLength} characters)`}
</p>
@@ -281,9 +309,9 @@ export function FormFieldWrapper({
</p>
)}
{/* Error message */}
{/* Error message with animation */}
{error && (
<p className="text-sm text-destructive">
<p className="text-sm text-destructive animate-slide-in-down">
{error}
{showCharCount && ` (${charCount}/${maxLength})`}
</p>
@@ -299,6 +327,7 @@ export const formFieldPresets = {
websiteUrl: (props: Partial<FormFieldWrapperProps>) => ({
fieldType: 'url' as FormFieldType,
label: 'Website URL',
validationMode: 'onBlur',
...props,
}),