From 34300a89c4faca8d93c5451e31c4a3ab005d16ae Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 5 Nov 2025 21:13:04 +0000 Subject: [PATCH] Fix: Add client-side validation --- src/components/admin/ParkForm.tsx | 40 +++++++++++++++++++++++++----- src/components/admin/RideForm.tsx | 16 +++++++++++- src/lib/entityValidationSchemas.ts | 40 ++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 7 deletions(-) diff --git a/src/components/admin/ParkForm.tsx b/src/components/admin/ParkForm.tsx index 01a726c0..a3b88d9b 100644 --- a/src/components/admin/ParkForm.tsx +++ b/src/components/admin/ParkForm.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import * as z from 'zod'; -import { entitySchemas } from '@/lib/entityValidationSchemas'; +import { entitySchemas, validateRequiredFields } from '@/lib/entityValidationSchemas'; import { validateSubmissionHandler } from '@/lib/entityFormValidation'; import { getErrorMessage } from '@/lib/errorHandler'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; @@ -17,7 +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 { MapPin, Save, X, Plus } from 'lucide-react'; +import { MapPin, Save, X, Plus, AlertCircle } from 'lucide-react'; import { toDateOnly, parseDateOnly, toDateWithPrecision } from '@/lib/dateUtils'; import { Badge } from '@/components/ui/badge'; import { Combobox } from '@/components/ui/combobox'; @@ -167,6 +167,7 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }: handleSubmit, setValue, watch, + trigger, formState: { errors } } = useForm({ resolver: zodResolver(entitySchemas.park), @@ -202,6 +203,20 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }: const handleFormSubmit = async (data: ParkFormData) => { setIsSubmitting(true); try { + // Pre-submission validation for required fields + const { valid, errors: validationErrors } = validateRequiredFields('park', data); + if (!valid) { + validationErrors.forEach(error => { + toast({ + variant: 'destructive', + title: 'Missing Required Fields', + description: error + }); + }); + setIsSubmitting(false); + return; + } + // CRITICAL: Block new photo uploads on edits if (isEditing && data.images?.uploaded) { const hasNewPhotos = data.images.uploaded.some(img => img.isLocal); @@ -405,16 +420,29 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }: {/* Location */}
- + { setValue('location', location); + // Manually trigger validation for the location field + trigger('location'); }} initialLocationId={watch('location_id')} /> -

- Search for the park's location using OpenStreetMap. Location will be created when submission is approved. -

+ {errors.location && ( +

+ + {errors.location.message} +

+ )} + {!errors.location && ( +

+ Search for the park's location using OpenStreetMap. Location will be created when submission is approved. +

+ )}
{/* Operator & Property Owner Selection */} diff --git a/src/components/admin/RideForm.tsx b/src/components/admin/RideForm.tsx index 61328cde..7a44171f 100644 --- a/src/components/admin/RideForm.tsx +++ b/src/components/admin/RideForm.tsx @@ -6,7 +6,7 @@ import { validateSubmissionHandler } from '@/lib/entityFormValidation'; import { getErrorMessage } from '@/lib/errorHandler'; import type { RideTechnicalSpec, RideCoasterStat, RideNameHistory } from '@/types/database'; import type { TempCompanyData, TempRideModelData, TempParkData } from '@/types/company'; -import { entitySchemas } from '@/lib/entityValidationSchemas'; +import { entitySchemas, validateRequiredFields } from '@/lib/entityValidationSchemas'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -266,6 +266,20 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }: const handleFormSubmit = async (data: RideFormData) => { setIsSubmitting(true); try { + // Pre-submission validation for required fields + const { valid, errors: validationErrors } = validateRequiredFields('ride', data); + if (!valid) { + validationErrors.forEach(error => { + toast({ + variant: 'destructive', + title: 'Missing Required Fields', + description: error + }); + }); + setIsSubmitting(false); + return; + } + // CRITICAL: Block new photo uploads on edits if (isEditing && data.images?.uploaded) { const hasNewPhotos = data.images.uploaded.some(img => img.isLocal); diff --git a/src/lib/entityValidationSchemas.ts b/src/lib/entityValidationSchemas.ts index 0eece9b9..be1ee0a5 100644 --- a/src/lib/entityValidationSchemas.ts +++ b/src/lib/entityValidationSchemas.ts @@ -94,6 +94,12 @@ export const parkValidationSchema = z.object({ }, { message: 'Closing date must be after opening date', path: ['closing_date'], +}).refine((data) => { + // Either location object OR location_id must be provided + return !!(data.location || data.location_id); +}, { + message: 'Location is required. Please search and select a location for the park.', + path: ['location'] }); // ============================================ @@ -280,6 +286,12 @@ export const rideValidationSchema = z.object({ .max(1000, 'Submission notes must be less than 1000 characters') .nullish() .transform(val => val ?? undefined), +}).refine((data) => { + // park_id is required (either real UUID or temp- reference) + return !!(data.park_id && data.park_id.trim().length > 0); +}, { + message: 'Park is required. Please select or create a park for this ride.', + path: ['park_id'] }); // ============================================ @@ -774,3 +786,31 @@ export async function validateMultipleItems( return results; } + +/** + * Validate required fields before submission + * Returns user-friendly error messages + */ +export function validateRequiredFields( + entityType: keyof typeof entitySchemas, + data: any +): { valid: boolean; errors: string[] } { + const errors: string[] = []; + + if (entityType === 'park') { + if (!data.location && !data.location_id) { + errors.push('Location is required. Please search and select a location for the park.'); + } + } + + if (entityType === 'ride') { + if (!data.park_id || data.park_id.trim().length === 0) { + errors.push('Park is required. Please select or create a park for this ride.'); + } + } + + return { + valid: errors.length === 0, + errors + }; +}