feat: Add park selection to RideForm

This commit is contained in:
gpt-engineer-app[bot]
2025-11-05 21:18:26 +00:00
parent 34300a89c4
commit 7476fbd5da
2 changed files with 139 additions and 3 deletions

View File

@@ -23,10 +23,10 @@ import { SlugField } from '@/components/ui/slug-field';
import { Checkbox } from '@/components/ui/checkbox'; import { Checkbox } from '@/components/ui/checkbox';
import { toast } from '@/hooks/use-toast'; import { toast } from '@/hooks/use-toast';
import { handleError } from '@/lib/errorHandler'; import { handleError } from '@/lib/errorHandler';
import { Plus, Zap, Save, X, Building2 } from 'lucide-react'; import { Plus, Zap, Save, X, Building2, AlertCircle } from 'lucide-react';
import { toDateOnly, parseDateOnly, toDateWithPrecision } from '@/lib/dateUtils'; import { toDateOnly, parseDateOnly, toDateWithPrecision } from '@/lib/dateUtils';
import { useUnitPreferences } from '@/hooks/useUnitPreferences'; import { useUnitPreferences } from '@/hooks/useUnitPreferences';
import { useManufacturers, useRideModels } from '@/hooks/useAutocompleteData'; import { useManufacturers, useRideModels, useParks } from '@/hooks/useAutocompleteData';
import { useUserRole } from '@/hooks/useUserRole'; import { useUserRole } from '@/hooks/useUserRole';
import { ManufacturerForm } from './ManufacturerForm'; import { ManufacturerForm } from './ManufacturerForm';
import { RideModelForm } from './RideModelForm'; import { RideModelForm } from './RideModelForm';
@@ -208,12 +208,14 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }:
// Fetch data // Fetch data
const { manufacturers, loading: manufacturersLoading } = useManufacturers(); const { manufacturers, loading: manufacturersLoading } = useManufacturers();
const { rideModels, loading: modelsLoading } = useRideModels(selectedManufacturerId); const { rideModels, loading: modelsLoading } = useRideModels(selectedManufacturerId);
const { parks, loading: parksLoading } = useParks();
const { const {
register, register,
handleSubmit, handleSubmit,
setValue, setValue,
watch, watch,
trigger,
formState: { errors } formState: { errors }
} = useForm<RideFormData>({ } = useForm<RideFormData>({
resolver: zodResolver(entitySchemas.ride), resolver: zodResolver(entitySchemas.ride),
@@ -256,11 +258,13 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }:
ride_model_id: initialData?.ride_model_id || undefined, ride_model_id: initialData?.ride_model_id || undefined,
source_url: initialData?.source_url || '', source_url: initialData?.source_url || '',
submission_notes: initialData?.submission_notes || '', submission_notes: initialData?.submission_notes || '',
images: { uploaded: [] } images: { uploaded: [] },
park_id: initialData?.park_id || undefined
} }
}); });
const selectedCategory = watch('category'); const selectedCategory = watch('category');
const isParkPreselected = !!initialData?.park_id; // Coming from park detail page
const handleFormSubmit = async (data: RideFormData) => { const handleFormSubmit = async (data: RideFormData) => {
@@ -419,6 +423,96 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }:
/> />
</div> </div>
{/* Park Selection */}
<div className="space-y-4">
<h3 className="text-lg font-semibold">Park Information</h3>
<div className="space-y-2">
<Label className="flex items-center gap-1">
Park
<span className="text-destructive">*</span>
</Label>
{tempNewPark ? (
// Show temp park badge
<div className="flex items-center gap-2 p-3 border rounded-md bg-green-50 dark:bg-green-950">
<Badge variant="secondary">New</Badge>
<span className="font-medium">{tempNewPark.name}</span>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => {
setTempNewPark(null);
}}
disabled={isParkPreselected}
>
<X className="w-4 h-4" />
</Button>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setIsParkModalOpen(true)}
disabled={isParkPreselected}
>
Edit
</Button>
</div>
) : (
// Show combobox for existing parks
<Combobox
options={parks}
value={watch('park_id') || undefined}
onValueChange={(value) => {
setValue('park_id', value);
trigger('park_id');
}}
placeholder={isParkPreselected ? "Park pre-selected" : "Select a park"}
searchPlaceholder="Search parks..."
emptyText="No parks found"
loading={parksLoading}
disabled={isParkPreselected}
/>
)}
{/* Validation error display */}
{errors.park_id && (
<p className="text-sm text-destructive flex items-center gap-1">
<AlertCircle className="w-4 h-4" />
{errors.park_id.message}
</p>
)}
{/* Create New Park Button */}
{!tempNewPark && !isParkPreselected && (
<Button
type="button"
variant="outline"
size="sm"
className="w-full"
onClick={() => setIsParkModalOpen(true)}
>
<Plus className="w-4 h-4 mr-2" />
Create New Park
</Button>
)}
{/* Help text */}
{isParkPreselected ? (
<p className="text-sm text-muted-foreground">
Park is pre-selected from the park detail page and cannot be changed.
</p>
) : (
<p className="text-sm text-muted-foreground">
{tempNewPark
? "New park will be created when submission is approved"
: "Select the park where this ride is located"}
</p>
)}
</div>
</div>
{/* Category and Status */} {/* Category and Status */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="space-y-2"> <div className="space-y-2">

View File

@@ -301,4 +301,46 @@ export function usePropertyOwners() {
}, []); }, []);
return { propertyOwners, loading }; return { propertyOwners, loading };
}
/**
* Hook to fetch all parks for autocomplete
* Returns parks as combobox options
*/
export function useParks() {
const [parks, setParks] = useState<ComboboxOption[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
async function fetchParks() {
setLoading(true);
try {
const { data, error } = await supabase
.from('parks')
.select('id, name, slug')
.order('name');
if (error) throw error;
setParks(
(data || []).map(park => ({
label: park.name,
value: park.id
}))
);
} catch (error: unknown) {
handleNonCriticalError(error, { action: 'Fetch parks' });
toast.error('Failed to load parks', {
description: 'Please refresh the page and try again.',
});
setParks([]);
} finally {
setLoading(false);
}
}
fetchParks();
}, []);
return { parks, loading };
} }