Very minimal MVP

This commit is contained in:
Eric Gullickson
2025-08-23 09:54:22 -05:00
parent d60c3ec00e
commit 6683f1eeff
12 changed files with 661 additions and 13 deletions

View File

@@ -1,16 +1,24 @@
/**
* @ai-summary Vehicle form component for create/edit
* @ai-summary Vehicle form component for create/edit with dropdown cascades
*/
import React from 'react';
import React, { useState, useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { Button } from '../../../shared-minimal/components/Button';
import { CreateVehicleRequest } from '../types/vehicles.types';
import { CreateVehicleRequest, DropdownOption } from '../types/vehicles.types';
import { vehiclesApi } from '../api/vehicles.api';
const vehicleSchema = z.object({
vin: z.string().length(17, 'VIN must be exactly 17 characters'),
make: z.string().optional(),
model: z.string().optional(),
engine: z.string().optional(),
transmission: z.string().optional(),
trimLevel: z.string().optional(),
driveType: z.string().optional(),
fuelType: z.string().optional(),
nickname: z.string().optional(),
color: z.string().optional(),
licensePlate: z.string().optional(),
@@ -30,15 +38,74 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
initialData,
loading,
}) => {
const [makes, setMakes] = useState<DropdownOption[]>([]);
const [models, setModels] = useState<DropdownOption[]>([]);
const [transmissions, setTransmissions] = useState<DropdownOption[]>([]);
const [engines, setEngines] = useState<DropdownOption[]>([]);
const [trims, setTrims] = useState<DropdownOption[]>([]);
const [selectedMake, setSelectedMake] = useState<string>('');
const [loadingDropdowns, setLoadingDropdowns] = useState(false);
const {
register,
handleSubmit,
formState: { errors },
watch,
setValue,
} = useForm<CreateVehicleRequest>({
resolver: zodResolver(vehicleSchema),
defaultValues: initialData,
});
const watchedMake = watch('make');
// Load dropdown data on component mount
useEffect(() => {
const loadInitialData = async () => {
setLoadingDropdowns(true);
try {
const [makesData, transmissionsData, enginesData, trimsData] = await Promise.all([
vehiclesApi.getMakes(),
vehiclesApi.getTransmissions(),
vehiclesApi.getEngines(),
vehiclesApi.getTrims(),
]);
setMakes(makesData);
setTransmissions(transmissionsData);
setEngines(enginesData);
setTrims(trimsData);
} catch (error) {
console.error('Failed to load dropdown data:', error);
} finally {
setLoadingDropdowns(false);
}
};
loadInitialData();
}, []);
// Load models when make changes
useEffect(() => {
if (watchedMake && watchedMake !== selectedMake) {
const loadModels = async () => {
try {
const modelsData = await vehiclesApi.getModels(watchedMake);
setModels(modelsData);
setSelectedMake(watchedMake);
// Clear model selection when make changes
setValue('model', '');
} catch (error) {
console.error('Failed to load models:', error);
setModels([]);
}
};
loadModels();
}
}, [watchedMake, selectedMake, setValue]);
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div>
@@ -55,6 +122,101 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
)}
</div>
{/* Vehicle Specification Dropdowns */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Make
</label>
<select
{...register('make')}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500"
disabled={loadingDropdowns}
>
<option value="">Select Make</option>
{makes.map((make) => (
<option key={make.id} value={make.name}>
{make.name}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Model
</label>
<select
{...register('model')}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500"
disabled={!watchedMake || models.length === 0}
>
<option value="">Select Model</option>
{models.map((model) => (
<option key={model.id} value={model.name}>
{model.name}
</option>
))}
</select>
</div>
</div>
<div className="grid grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Engine
</label>
<select
{...register('engine')}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500"
disabled={loadingDropdowns}
>
<option value="">Select Engine</option>
{engines.map((engine) => (
<option key={engine.id} value={engine.name}>
{engine.name}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Transmission
</label>
<select
{...register('transmission')}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500"
disabled={loadingDropdowns}
>
<option value="">Select Transmission</option>
{transmissions.map((transmission) => (
<option key={transmission.id} value={transmission.name}>
{transmission.name}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Trim Level
</label>
<select
{...register('trimLevel')}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500"
disabled={loadingDropdowns}
>
<option value="">Select Trim</option>
{trims.map((trim) => (
<option key={trim.id} value={trim.name}>
{trim.name}
</option>
))}
</select>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Nickname
@@ -106,7 +268,7 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
<Button variant="secondary" onClick={onCancel} type="button">
Cancel
</Button>
<Button type="submit" loading={loading}>
<Button type="submit" loading={loading || loadingDropdowns}>
{initialData ? 'Update Vehicle' : 'Add Vehicle'}
</Button>
</div>