Modernization Project Complete. Updated to latest versions of frameworks.
This commit is contained in:
136
frontend/src/features/vehicles/hooks/useOptimisticVehicles.ts
Normal file
136
frontend/src/features/vehicles/hooks/useOptimisticVehicles.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* @ai-summary React 19 useOptimistic hook for vehicle operations
|
||||
* @ai-context Provides optimistic updates for better perceived performance
|
||||
*/
|
||||
|
||||
import { useOptimistic, useTransition } from 'react';
|
||||
import { Vehicle, CreateVehicleRequest } from '../types/vehicles.types';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { vehiclesApi } from '../api/vehicles.api';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
type VehicleAction =
|
||||
| { type: 'add'; vehicle: Vehicle }
|
||||
| { type: 'delete'; id: string }
|
||||
| { type: 'update'; id: string; updates: Partial<Vehicle> };
|
||||
|
||||
function vehicleReducer(state: Vehicle[], action: VehicleAction): Vehicle[] {
|
||||
switch (action.type) {
|
||||
case 'add':
|
||||
return [...state, action.vehicle];
|
||||
case 'delete':
|
||||
return state.filter(v => v.id !== action.id);
|
||||
case 'update':
|
||||
return state.map(v =>
|
||||
v.id === action.id ? { ...v, ...action.updates } : v
|
||||
);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export function useOptimisticVehicles(vehicles: Vehicle[] = []) {
|
||||
const [optimisticVehicles, addOptimistic] = useOptimistic(
|
||||
vehicles,
|
||||
vehicleReducer
|
||||
);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const optimisticCreateVehicle = async (data: CreateVehicleRequest) => {
|
||||
// Create optimistic vehicle with temporary ID
|
||||
const optimisticVehicle: Vehicle = {
|
||||
id: `temp-${Date.now()}`,
|
||||
vin: data.vin,
|
||||
nickname: data.nickname,
|
||||
color: data.color,
|
||||
licensePlate: data.licensePlate,
|
||||
odometerReading: data.odometerReading || 0,
|
||||
make: undefined,
|
||||
model: undefined,
|
||||
year: undefined,
|
||||
engine: undefined,
|
||||
transmission: undefined,
|
||||
trimLevel: undefined,
|
||||
driveType: undefined,
|
||||
fuelType: undefined,
|
||||
isActive: true,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
userId: 'current-user' // This would come from auth context
|
||||
};
|
||||
|
||||
// Show optimistic update immediately
|
||||
startTransition(() => {
|
||||
addOptimistic({ type: 'add', vehicle: optimisticVehicle });
|
||||
});
|
||||
|
||||
try {
|
||||
// Perform actual API call
|
||||
const createdVehicle = await vehiclesApi.create(data);
|
||||
|
||||
// Invalidate and refetch to get real data
|
||||
await queryClient.invalidateQueries({ queryKey: ['vehicles'] });
|
||||
toast.success('Vehicle added successfully');
|
||||
|
||||
return createdVehicle;
|
||||
} catch (error: any) {
|
||||
// Revert optimistic update on error
|
||||
await queryClient.invalidateQueries({ queryKey: ['vehicles'] });
|
||||
toast.error(error.response?.data?.error || 'Failed to add vehicle');
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const optimisticDeleteVehicle = async (id: string) => {
|
||||
// Show optimistic delete immediately
|
||||
startTransition(() => {
|
||||
addOptimistic({ type: 'delete', id });
|
||||
});
|
||||
|
||||
try {
|
||||
// Perform actual API call
|
||||
await vehiclesApi.delete(id);
|
||||
|
||||
// Invalidate and refetch to ensure consistency
|
||||
await queryClient.invalidateQueries({ queryKey: ['vehicles'] });
|
||||
toast.success('Vehicle deleted successfully');
|
||||
} catch (error: any) {
|
||||
// Revert optimistic update on error
|
||||
await queryClient.invalidateQueries({ queryKey: ['vehicles'] });
|
||||
toast.error(error.response?.data?.error || 'Failed to delete vehicle');
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const optimisticUpdateVehicle = async (id: string, updates: Partial<Vehicle>) => {
|
||||
// Show optimistic update immediately
|
||||
startTransition(() => {
|
||||
addOptimistic({ type: 'update', id, updates });
|
||||
});
|
||||
|
||||
try {
|
||||
// Perform actual API call
|
||||
const updatedVehicle = await vehiclesApi.update(id, updates);
|
||||
|
||||
// Invalidate and refetch to get real data
|
||||
await queryClient.invalidateQueries({ queryKey: ['vehicles'] });
|
||||
toast.success('Vehicle updated successfully');
|
||||
|
||||
return updatedVehicle;
|
||||
} catch (error: any) {
|
||||
// Revert optimistic update on error
|
||||
await queryClient.invalidateQueries({ queryKey: ['vehicles'] });
|
||||
toast.error(error.response?.data?.error || 'Failed to update vehicle');
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
optimisticVehicles,
|
||||
isPending,
|
||||
optimisticCreateVehicle,
|
||||
optimisticDeleteVehicle,
|
||||
optimisticUpdateVehicle
|
||||
};
|
||||
}
|
||||
191
frontend/src/features/vehicles/hooks/useVehicleTransitions.ts
Normal file
191
frontend/src/features/vehicles/hooks/useVehicleTransitions.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* @ai-summary React 19 useTransition hooks for vehicle operations
|
||||
* @ai-context Non-blocking updates for better UI responsiveness
|
||||
*/
|
||||
|
||||
import { useTransition, useState, useCallback } from 'react';
|
||||
import { Vehicle } from '../types/vehicles.types';
|
||||
|
||||
export function useVehicleSearch(vehicles: Vehicle[]) {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [filteredVehicles, setFilteredVehicles] = useState<Vehicle[]>(vehicles);
|
||||
|
||||
const handleSearch = useCallback((query: string) => {
|
||||
// High priority: Update search input immediately
|
||||
setSearchQuery(query);
|
||||
|
||||
// Low priority: Update filtered results (non-blocking)
|
||||
startTransition(() => {
|
||||
if (!query.trim()) {
|
||||
setFilteredVehicles(vehicles);
|
||||
} else {
|
||||
const filtered = vehicles.filter(vehicle => {
|
||||
const searchTerm = query.toLowerCase();
|
||||
return (
|
||||
vehicle.nickname?.toLowerCase().includes(searchTerm) ||
|
||||
vehicle.make?.toLowerCase().includes(searchTerm) ||
|
||||
vehicle.model?.toLowerCase().includes(searchTerm) ||
|
||||
vehicle.vin.toLowerCase().includes(searchTerm) ||
|
||||
vehicle.year?.toString().includes(searchTerm)
|
||||
);
|
||||
});
|
||||
setFilteredVehicles(filtered);
|
||||
}
|
||||
});
|
||||
}, [vehicles]);
|
||||
|
||||
const clearSearch = useCallback(() => {
|
||||
setSearchQuery('');
|
||||
startTransition(() => {
|
||||
setFilteredVehicles(vehicles);
|
||||
});
|
||||
}, [vehicles]);
|
||||
|
||||
// Update filtered vehicles when vehicles data changes
|
||||
const updateVehicles = useCallback((newVehicles: Vehicle[]) => {
|
||||
startTransition(() => {
|
||||
if (!searchQuery.trim()) {
|
||||
setFilteredVehicles(newVehicles);
|
||||
} else {
|
||||
// Re-apply search filter to new data
|
||||
const filtered = newVehicles.filter(vehicle => {
|
||||
const searchTerm = searchQuery.toLowerCase();
|
||||
return (
|
||||
vehicle.nickname?.toLowerCase().includes(searchTerm) ||
|
||||
vehicle.make?.toLowerCase().includes(searchTerm) ||
|
||||
vehicle.model?.toLowerCase().includes(searchTerm) ||
|
||||
vehicle.vin.toLowerCase().includes(searchTerm) ||
|
||||
vehicle.year?.toString().includes(searchTerm)
|
||||
);
|
||||
});
|
||||
setFilteredVehicles(filtered);
|
||||
}
|
||||
});
|
||||
}, [searchQuery]);
|
||||
|
||||
return {
|
||||
searchQuery,
|
||||
filteredVehicles,
|
||||
isPending,
|
||||
handleSearch,
|
||||
clearSearch,
|
||||
updateVehicles
|
||||
};
|
||||
}
|
||||
|
||||
export function useVehicleSort(vehicles: Vehicle[]) {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [sortBy, setSortBy] = useState<'name' | 'make' | 'year' | 'created'>('created');
|
||||
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
|
||||
const [sortedVehicles, setSortedVehicles] = useState<Vehicle[]>(vehicles);
|
||||
|
||||
const handleSort = useCallback((newSortBy: typeof sortBy, newSortOrder: typeof sortOrder = 'asc') => {
|
||||
// High priority: Update sort state immediately
|
||||
setSortBy(newSortBy);
|
||||
setSortOrder(newSortOrder);
|
||||
|
||||
// Low priority: Update sorted results (non-blocking)
|
||||
startTransition(() => {
|
||||
const sorted = [...vehicles].sort((a, b) => {
|
||||
let aValue: string | number | null;
|
||||
let bValue: string | number | null;
|
||||
|
||||
switch (newSortBy) {
|
||||
case 'name':
|
||||
aValue = a.nickname || `${a.make || ''} ${a.model || ''}`.trim() || 'Unknown';
|
||||
bValue = b.nickname || `${b.make || ''} ${b.model || ''}`.trim() || 'Unknown';
|
||||
break;
|
||||
case 'make':
|
||||
aValue = a.make || 'Unknown';
|
||||
bValue = b.make || 'Unknown';
|
||||
break;
|
||||
case 'year':
|
||||
aValue = a.year || 0;
|
||||
bValue = b.year || 0;
|
||||
break;
|
||||
case 'created':
|
||||
aValue = a.createdAt;
|
||||
bValue = b.createdAt;
|
||||
break;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (typeof aValue === 'string' && typeof bValue === 'string') {
|
||||
const result = aValue.localeCompare(bValue);
|
||||
return newSortOrder === 'asc' ? result : -result;
|
||||
} else {
|
||||
const result = (aValue as number) - (bValue as number);
|
||||
return newSortOrder === 'asc' ? result : -result;
|
||||
}
|
||||
});
|
||||
setSortedVehicles(sorted);
|
||||
});
|
||||
}, [vehicles]);
|
||||
|
||||
// Update sorted vehicles when vehicles data changes
|
||||
const updateVehicles = useCallback((_newVehicles: Vehicle[]) => {
|
||||
startTransition(() => {
|
||||
handleSort(sortBy, sortOrder);
|
||||
});
|
||||
}, [sortBy, sortOrder, handleSort]);
|
||||
|
||||
return {
|
||||
sortBy,
|
||||
sortOrder,
|
||||
sortedVehicles,
|
||||
isPending,
|
||||
handleSort,
|
||||
updateVehicles
|
||||
};
|
||||
}
|
||||
|
||||
export function useVehicleFilter(vehicles: Vehicle[]) {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [activeFilters, setActiveFilters] = useState<{
|
||||
make?: string;
|
||||
year?: number;
|
||||
isActive?: boolean;
|
||||
}>({});
|
||||
const [filteredVehicles, setFilteredVehicles] = useState<Vehicle[]>(vehicles);
|
||||
|
||||
const applyFilters = useCallback((filters: typeof activeFilters) => {
|
||||
// High priority: Update filter state immediately
|
||||
setActiveFilters(filters);
|
||||
|
||||
// Low priority: Apply filters (non-blocking)
|
||||
startTransition(() => {
|
||||
let filtered = vehicles;
|
||||
|
||||
if (filters.make) {
|
||||
filtered = filtered.filter(v => v.make === filters.make);
|
||||
}
|
||||
|
||||
if (filters.year) {
|
||||
filtered = filtered.filter(v => v.year === filters.year);
|
||||
}
|
||||
|
||||
if (filters.isActive !== undefined) {
|
||||
filtered = filtered.filter(v => v.isActive === filters.isActive);
|
||||
}
|
||||
|
||||
setFilteredVehicles(filtered);
|
||||
});
|
||||
}, [vehicles]);
|
||||
|
||||
const clearFilters = useCallback(() => {
|
||||
setActiveFilters({});
|
||||
startTransition(() => {
|
||||
setFilteredVehicles(vehicles);
|
||||
});
|
||||
}, [vehicles]);
|
||||
|
||||
return {
|
||||
activeFilters,
|
||||
filteredVehicles,
|
||||
isPending,
|
||||
applyFilters,
|
||||
clearFilters
|
||||
};
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
/**
|
||||
* @ai-summary Mobile-optimized vehicles screen with Material Design 3
|
||||
* @ai-summary Mobile vehicles screen with React 19 enhancements
|
||||
* @ai-context Enhanced with Suspense, optimistic updates, and transitions
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useTransition, useEffect } from 'react';
|
||||
import { Box, Typography, Grid } from '@mui/material';
|
||||
import { useVehicles } from '../hooks/useVehicles';
|
||||
import { useOptimisticVehicles } from '../hooks/useOptimisticVehicles';
|
||||
import { useVehicleSearch } from '../hooks/useVehicleTransitions';
|
||||
import { MobileVehiclesSuspense } from '../../../components/SuspenseWrappers';
|
||||
import { VehicleMobileCard } from './VehicleMobileCard';
|
||||
import { Vehicle } from '../types/vehicles.types';
|
||||
|
||||
@@ -32,6 +36,31 @@ export const VehiclesMobileScreen: React.FC<VehiclesMobileScreenProps> = ({
|
||||
onVehicleSelect
|
||||
}) => {
|
||||
const { data: vehicles, isLoading } = useVehicles();
|
||||
const [_isPending, startTransition] = useTransition();
|
||||
|
||||
// React 19 optimistic updates
|
||||
const {
|
||||
optimisticVehicles,
|
||||
isPending: isOptimisticPending
|
||||
} = useOptimisticVehicles(vehicles || []);
|
||||
|
||||
// Enhanced search with transitions
|
||||
const {
|
||||
filteredVehicles,
|
||||
updateVehicles
|
||||
} = useVehicleSearch(optimisticVehicles);
|
||||
|
||||
// Update search when optimistic vehicles change
|
||||
useEffect(() => {
|
||||
updateVehicles(optimisticVehicles);
|
||||
}, [optimisticVehicles, updateVehicles]);
|
||||
|
||||
const handleVehicleSelect = (vehicle: Vehicle) => {
|
||||
// Use transition to avoid blocking UI during navigation
|
||||
startTransition(() => {
|
||||
onVehicleSelect?.(vehicle);
|
||||
});
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -43,7 +72,7 @@ export const VehiclesMobileScreen: React.FC<VehiclesMobileScreenProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (!vehicles?.length) {
|
||||
if (!optimisticVehicles.length) {
|
||||
return (
|
||||
<Box sx={{ pb: 10 }}>
|
||||
<Section title="Vehicles">
|
||||
@@ -61,19 +90,21 @@ export const VehiclesMobileScreen: React.FC<VehiclesMobileScreenProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ pb: 10 }}>
|
||||
<Section title="Vehicles">
|
||||
<Grid container spacing={2}>
|
||||
{vehicles.map((vehicle) => (
|
||||
<Grid item xs={12} key={vehicle.id}>
|
||||
<VehicleMobileCard
|
||||
vehicle={vehicle}
|
||||
onClick={() => onVehicleSelect?.(vehicle)}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Section>
|
||||
</Box>
|
||||
<MobileVehiclesSuspense>
|
||||
<Box sx={{ pb: 10 }}>
|
||||
<Section title={`Vehicles ${isOptimisticPending ? '(Updating...)' : ''}`}>
|
||||
<Grid container spacing={2}>
|
||||
{filteredVehicles.map((vehicle) => (
|
||||
<Grid item xs={12} key={vehicle.id}>
|
||||
<VehicleMobileCard
|
||||
vehicle={vehicle}
|
||||
onClick={() => handleVehicleSelect(vehicle)}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Section>
|
||||
</Box>
|
||||
</MobileVehiclesSuspense>
|
||||
);
|
||||
};
|
||||
@@ -1,37 +1,75 @@
|
||||
/**
|
||||
* @ai-summary Main vehicles page with Material Design 3
|
||||
* @ai-summary Main vehicles page with React 19 advanced features
|
||||
* @ai-context Enhanced with Suspense, useOptimistic, and useTransition
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Box, Typography, Grid, Button as MuiButton } from '@mui/material';
|
||||
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 { useVehicles, useCreateVehicle, useDeleteVehicle } from '../hooks/useVehicles';
|
||||
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 createVehicle = useCreateVehicle();
|
||||
const deleteVehicle = useDeleteVehicle();
|
||||
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) => {
|
||||
setSelectedVehicle(id);
|
||||
navigate(`/vehicles/${id}`);
|
||||
// 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 deleteVehicle.mutateAsync(id);
|
||||
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={{
|
||||
@@ -46,45 +84,77 @@ export const VehiclesPage: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<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={() => setShowForm(true)}
|
||||
sx={{ borderRadius: '999px' }}
|
||||
>
|
||||
Add Vehicle
|
||||
</MuiButton>
|
||||
<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>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{showForm && (
|
||||
<Card className="mb-6">
|
||||
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>
|
||||
Add New Vehicle
|
||||
</Typography>
|
||||
<VehicleForm
|
||||
onSubmit={async (data) => {
|
||||
await createVehicle.mutateAsync(data);
|
||||
setShowForm(false);
|
||||
}}
|
||||
onCancel={() => setShowForm(false)}
|
||||
loading={createVehicle.isPending}
|
||||
/>
|
||||
</Card>
|
||||
<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>
|
||||
)}
|
||||
|
||||
{vehicles?.length === 0 ? (
|
||||
{optimisticVehicles.length === 0 ? (
|
||||
<Card>
|
||||
<Box sx={{ textAlign: 'center', py: 8 }}>
|
||||
<Typography color="text.secondary" sx={{ mb: 3 }}>
|
||||
@@ -94,8 +164,9 @@ export const VehiclesPage: React.FC = () => {
|
||||
<MuiButton
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={() => setShowForm(true)}
|
||||
onClick={() => startTransition(() => setShowForm(true))}
|
||||
sx={{ borderRadius: '999px' }}
|
||||
disabled={isPending || isOptimisticPending}
|
||||
>
|
||||
Add Your First Vehicle
|
||||
</MuiButton>
|
||||
@@ -104,7 +175,7 @@ export const VehiclesPage: React.FC = () => {
|
||||
</Card>
|
||||
) : (
|
||||
<Grid container spacing={3}>
|
||||
{vehicles?.map((vehicle) => (
|
||||
{filteredVehicles.map((vehicle) => (
|
||||
<Grid item xs={12} md={6} lg={4} key={vehicle.id}>
|
||||
<VehicleCard
|
||||
vehicle={vehicle}
|
||||
@@ -116,6 +187,7 @@ export const VehiclesPage: React.FC = () => {
|
||||
))}
|
||||
</Grid>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</VehicleListSuspense>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user