193 lines
6.2 KiB
TypeScript
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>
|
|
);
|
|
}; |