Files
motovaultpro/frontend/src/features/vehicles/pages/VehiclesPage.tsx

193 lines
6.2 KiB
TypeScript

/**
* @ai-summary Main vehicles page with React 19 advanced features
* @ai-context Enhanced with Suspense, useOptimistic, and useTransition
*/
import React, { useState, useEffect, useTransition } from 'react';
import { Box, Typography, Grid, Button as MuiButton, TextField, IconButton } from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import SearchIcon from '@mui/icons-material/Search';
import ClearIcon from '@mui/icons-material/Clear';
import { useVehicles } from '../hooks/useVehicles';
import { useOptimisticVehicles } from '../hooks/useOptimisticVehicles';
import { useVehicleSearch } from '../hooks/useVehicleTransitions';
import { VehicleCard } from '../components/VehicleCard';
import { VehicleForm } from '../components/VehicleForm';
import { Card } from '../../../shared-minimal/components/Card';
import { VehicleListSuspense, FormSuspense } from '../../../components/SuspenseWrappers';
import { useAppStore } from '../../../core/store';
import { useNavigate } from 'react-router-dom';
export const VehiclesPage: React.FC = () => {
const navigate = useNavigate();
const { data: vehicles, isLoading } = useVehicles();
const setSelectedVehicle = useAppStore(state => state.setSelectedVehicle);
// React 19 optimistic updates and transitions
const {
optimisticVehicles,
isPending: isOptimisticPending,
optimisticCreateVehicle,
optimisticDeleteVehicle
} = useOptimisticVehicles(vehicles || []);
const {
searchQuery,
filteredVehicles,
isPending: isSearchPending,
handleSearch,
clearSearch,
updateVehicles
} = useVehicleSearch(optimisticVehicles);
const [isPending, startTransition] = useTransition();
const [showForm, setShowForm] = useState(false);
// Update search vehicles when optimistic vehicles change
useEffect(() => {
updateVehicles(optimisticVehicles);
}, [optimisticVehicles, updateVehicles]);
const handleSelectVehicle = (id: string) => {
// Use transition for navigation to avoid blocking UI
startTransition(() => {
setSelectedVehicle(id);
navigate(`/vehicles/${id}`);
});
};
const handleDelete = async (id: string) => {
if (confirm('Are you sure you want to delete this vehicle?')) {
await optimisticDeleteVehicle(id);
}
};
const handleCreateVehicle = async (data: any) => {
await optimisticCreateVehicle(data);
// Use transition for UI state change
startTransition(() => {
setShowForm(false);
});
};
if (isLoading) {
return (
<Box sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '50vh'
}}>
<Typography color="text.secondary">Loading vehicles...</Typography>
</Box>
);
}
return (
<VehicleListSuspense>
<Box sx={{ py: 2 }}>
<Box sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
mb: 4
}}>
<Typography variant="h4" sx={{ fontWeight: 700, color: 'text.primary' }}>
My Vehicles
</Typography>
{!showForm && (
<MuiButton
variant="contained"
startIcon={<AddIcon />}
onClick={() => startTransition(() => setShowForm(true))}
sx={{ borderRadius: '999px' }}
disabled={isPending || isOptimisticPending}
>
Add Vehicle
</MuiButton>
)}
</Box>
{/* Search functionality */}
{vehicles && vehicles.length > 0 && (
<Box sx={{ mb: 3 }}>
<TextField
fullWidth
placeholder="Search vehicles by name, make, model, or VIN..."
value={searchQuery}
onChange={(e) => handleSearch(e.target.value)}
InputProps={{
startAdornment: <SearchIcon sx={{ mr: 1, color: 'text.secondary' }} />,
endAdornment: searchQuery && (
<IconButton onClick={clearSearch} size="small">
<ClearIcon />
</IconButton>
),
}}
sx={{
'& .MuiOutlinedInput-root': {
borderRadius: '999px',
backgroundColor: isSearchPending ? 'action.hover' : 'background.paper'
}
}}
/>
{searchQuery && (
<Typography variant="body2" sx={{ mt: 1, color: 'text.secondary' }}>
{isSearchPending ? 'Searching...' : `Found ${filteredVehicles.length} vehicle(s)`}
</Typography>
)}
</Box>
)}
{showForm && (
<FormSuspense>
<Card className="mb-6">
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>
Add New Vehicle
</Typography>
<VehicleForm
onSubmit={handleCreateVehicle}
onCancel={() => startTransition(() => setShowForm(false))}
loading={isOptimisticPending}
/>
</Card>
</FormSuspense>
)}
{optimisticVehicles.length === 0 ? (
<Card>
<Box sx={{ textAlign: 'center', py: 8 }}>
<Typography color="text.secondary" sx={{ mb: 3 }}>
No vehicles added yet
</Typography>
{!showForm && (
<MuiButton
variant="contained"
startIcon={<AddIcon />}
onClick={() => startTransition(() => setShowForm(true))}
sx={{ borderRadius: '999px' }}
disabled={isPending || isOptimisticPending}
>
Add Your First Vehicle
</MuiButton>
)}
</Box>
</Card>
) : (
<Grid container spacing={3}>
{filteredVehicles.map((vehicle) => (
<Grid item xs={12} md={6} lg={4} key={vehicle.id}>
<VehicleCard
vehicle={vehicle}
onEdit={(v) => console.log('Edit', v)}
onDelete={handleDelete}
onSelect={handleSelectVehicle}
/>
</Grid>
))}
</Grid>
)}
</Box>
</VehicleListSuspense>
);
};