From 92e93bfc9d415493c99f03fb4d8f7d0f8ee32407 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Tue, 11 Nov 2025 23:43:01 +0000 Subject: [PATCH] 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. --- src/components/admin/ParkForm.tsx | 15 +++++- src/components/admin/RideForm.tsx | 20 +++++--- src/components/ui/form-field-wrapper.tsx | 59 +++++++++++++++------ src/lib/formToasts.ts | 65 ++++++++++++++++++++++++ tailwind.config.ts | 15 ++++++ 5 files changed, 150 insertions(+), 24 deletions(-) create mode 100644 src/lib/formToasts.ts diff --git a/src/components/admin/ParkForm.tsx b/src/components/admin/ParkForm.tsx index fb8cdbcf..e6098f0e 100644 --- a/src/components/admin/ParkForm.tsx +++ b/src/components/admin/ParkForm.tsx @@ -17,6 +17,7 @@ import { FlexibleDateInput, type DatePrecision } from '@/components/ui/flexible- import { SlugField } from '@/components/ui/slug-field'; import { toast } from '@/hooks/use-toast'; import { handleError } from '@/lib/errorHandler'; +import { formToasts } from '@/lib/formToasts'; import { MapPin, Save, X, Plus, AlertCircle, Info } from 'lucide-react'; import { toDateOnly, parseDateOnly, toDateWithPrecision } from '@/lib/dateUtils'; import { Badge } from '@/components/ui/badge'; @@ -294,7 +295,16 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }: await onSubmit(submissionData); - // Parent component handles success feedback + // Show success toast + if (isModerator()) { + formToasts.success.moderatorApproval('Park', data.name); + } else if (isEditing) { + formToasts.success.update('Park', data.name); + } else { + formToasts.success.create('Park', data.name); + } + + // Parent component handles modal closing/navigation } catch (error: unknown) { const errorMessage = getErrorMessage(error); handleError(error, { @@ -308,6 +318,9 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }: } }); + // Show error toast + formToasts.error.generic(errorMessage); + // Re-throw so parent can handle modal closing throw error; } finally { diff --git a/src/components/admin/RideForm.tsx b/src/components/admin/RideForm.tsx index 17b65cee..8ec12015 100644 --- a/src/components/admin/RideForm.tsx +++ b/src/components/admin/RideForm.tsx @@ -24,6 +24,7 @@ import { Checkbox } from '@/components/ui/checkbox'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { toast } from '@/hooks/use-toast'; import { handleError } from '@/lib/errorHandler'; +import { formToasts } from '@/lib/formToasts'; import { Plus, Zap, Save, X, Building2, AlertCircle, Info, HelpCircle } from 'lucide-react'; import { toDateOnly, parseDateOnly, toDateWithPrecision } from '@/lib/dateUtils'; import { useUnitPreferences } from '@/hooks/useUnitPreferences'; @@ -360,14 +361,14 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }: // Pass clean data to parent with extended fields await onSubmit(metricData); - toast({ - title: isEditing ? "Ride Updated" : "Submission Sent", - description: isEditing - ? "The ride information has been updated successfully." - : tempNewManufacturer - ? "Ride, manufacturer, and model submitted for review" - : "Ride submitted for review" - }); + // Show success toast + if (isModerator()) { + formToasts.success.moderatorApproval('Ride', data.name); + } else if (isEditing) { + formToasts.success.update('Ride', data.name); + } else { + formToasts.success.create('Ride', data.name); + } } catch (error: unknown) { handleError(error, { action: isEditing ? 'Update Ride' : 'Create Ride', @@ -378,6 +379,9 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }: } }); + // Show error toast + formToasts.error.generic(getErrorMessage(error)); + // Re-throw so parent can handle modal closing throw error; } finally { diff --git a/src/components/ui/form-field-wrapper.tsx b/src/components/ui/form-field-wrapper.tsx index cb9e1e61..77b51b7e 100644 --- a/src/components/ui/form-field-wrapper.tsx +++ b/src/components/ui/form-field-wrapper.tsx @@ -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) => { + 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); + } else if (inputProps?.onBlur) { + inputProps.onBlur(e as React.FocusEvent); + } + }; return ( -
+
{/* Label with optional terminology tooltip */}