MVP with new UX

This commit is contained in:
Eric Gullickson
2025-08-09 17:45:54 -05:00
parent 8f5117a4e2
commit d60c3ec00e
18 changed files with 1572 additions and 573 deletions

View File

@@ -1,11 +1,12 @@
/**
* @ai-summary Vehicle card component
* @ai-summary Vehicle card component with Material Design 3
*/
import React from 'react';
import { Card, CardContent, CardActionArea, Box, Typography, IconButton } from '@mui/material';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
import { Vehicle } from '../types/vehicles.types';
import { Card } from '../../../shared-minimal/components/Card';
import { Button } from '../../../shared-minimal/components/Button';
interface VehicleCardProps {
vehicle: Vehicle;
@@ -14,51 +15,96 @@ interface VehicleCardProps {
onSelect: (id: string) => void;
}
const CarThumb: React.FC<{ color?: string }> = ({ color = "#F2EAEA" }) => (
<Box
sx={{
height: 96,
bgcolor: color,
borderRadius: 2,
mb: 2,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
/>
);
export const VehicleCard: React.FC<VehicleCardProps> = ({
vehicle,
onEdit,
onDelete,
onSelect,
}) => {
const displayName = vehicle.nickname ||
`${vehicle.year} ${vehicle.make} ${vehicle.model}`;
return (
<Card className="hover:shadow-md transition-shadow cursor-pointer" onClick={() => onSelect(vehicle.id)}>
<div className="flex justify-between items-start">
<div className="flex-1">
<h3 className="text-lg font-semibold text-gray-900">
{vehicle.nickname || `${vehicle.year} ${vehicle.make} ${vehicle.model}`}
</h3>
<p className="text-sm text-gray-500 mt-1">VIN: {vehicle.vin}</p>
<Card
sx={{
height: '100%',
display: 'flex',
flexDirection: 'column',
'&:hover': {
boxShadow: 3,
},
transition: 'box-shadow 0.2s ease-in-out'
}}
>
<CardActionArea
onClick={() => onSelect(vehicle.id)}
sx={{ flexGrow: 1 }}
>
<CardContent>
<CarThumb color={vehicle.color || "#F2EAEA"} />
<Typography variant="h6" sx={{ fontWeight: 700, mb: 1 }}>
{displayName}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 0.5 }}>
VIN: {vehicle.vin}
</Typography>
{vehicle.licensePlate && (
<p className="text-sm text-gray-500">License: {vehicle.licensePlate}</p>
<Typography variant="body2" color="text.secondary" sx={{ mb: 0.5 }}>
License: {vehicle.licensePlate}
</Typography>
)}
<p className="text-sm text-gray-600 mt-2">
<Typography variant="body2" color="text.primary" sx={{ mt: 1, fontWeight: 500 }}>
Odometer: {vehicle.odometerReading.toLocaleString()} miles
</p>
</div>
<div className="flex gap-2">
<Button
size="sm"
variant="secondary"
onClick={(e) => {
e.stopPropagation();
onEdit(vehicle);
}}
>
Edit
</Button>
<Button
size="sm"
variant="danger"
onClick={(e) => {
e.stopPropagation();
onDelete(vehicle.id);
}}
>
Delete
</Button>
</div>
</div>
</Typography>
</CardContent>
</CardActionArea>
<Box sx={{
display: 'flex',
justifyContent: 'flex-end',
gap: 1,
p: 2,
pt: 0
}}>
<IconButton
size="small"
onClick={(e) => {
e.stopPropagation();
onEdit(vehicle);
}}
sx={{ color: 'text.secondary' }}
>
<EditIcon fontSize="small" />
</IconButton>
<IconButton
size="small"
onClick={(e) => {
e.stopPropagation();
onDelete(vehicle.id);
}}
sx={{ color: 'error.main' }}
>
<DeleteIcon fontSize="small" />
</IconButton>
</Box>
</Card>
);
};

View File

@@ -0,0 +1,138 @@
/**
* @ai-summary Mobile vehicle detail screen with Material Design 3
*/
import React from 'react';
import { Box, Typography, Button, Card, CardContent, Divider } from '@mui/material';
import { Vehicle } from '../types/vehicles.types';
// Theme colors now defined in Tailwind config
interface VehicleDetailMobileProps {
vehicle: Vehicle;
onBack: () => void;
onLogFuel?: () => void;
}
const Section: React.FC<{ title: string; children: React.ReactNode }> = ({
title,
children
}) => (
<Box sx={{ mb: 3 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600, mb: 1 }}>
{title}
</Typography>
{children}
</Box>
);
const CarThumb: React.FC<{ color?: string }> = ({ color = "#F2EAEA" }) => (
<Box
sx={{
height: 96,
bgcolor: color,
borderRadius: 3,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
/>
);
export const VehicleDetailMobile: React.FC<VehicleDetailMobileProps> = ({
vehicle,
onBack,
onLogFuel
}) => {
const displayName = vehicle.nickname ||
(vehicle.year && vehicle.make ? `${vehicle.year} ${vehicle.make}` : 'Vehicle');
const displayModel = vehicle.model || 'Unknown Model';
return (
<Box sx={{ pb: 10 }}>
<Button variant="text" onClick={onBack}>
Back
</Button>
<Typography variant="h4" sx={{ mt: 1, mb: 2 }}>
{displayName}
</Typography>
<Divider sx={{ my: 2 }} />
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, mb: 3 }}>
<Box sx={{ width: 112 }}>
<CarThumb color={vehicle.color || "#F2EAEA"} />
</Box>
<Box>
<Typography variant="h5" sx={{ fontWeight: 600 }}>
{displayName}
</Typography>
<Typography color="text.secondary">{displayModel}</Typography>
{vehicle.licensePlate && (
<Typography variant="body2" color="text.secondary">
{vehicle.licensePlate}
</Typography>
)}
</Box>
</Box>
<Box sx={{ display: 'flex', gap: 1.5, mb: 3 }}>
<Button
variant="contained"
onClick={onLogFuel}
>
Add Fuel
</Button>
<Button variant="outlined">
Maintenance
</Button>
</Box>
<Section title="Vehicle Details">
<Card>
<CardContent>
{vehicle.vin && (
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
<Typography color="text.secondary">VIN</Typography>
<Typography sx={{ fontFamily: 'monospace', fontSize: 'small' }}>
{vehicle.vin}
</Typography>
</Box>
)}
{vehicle.year && (
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
<Typography color="text.secondary">Year</Typography>
<Typography>{vehicle.year}</Typography>
</Box>
)}
{vehicle.make && (
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
<Typography color="text.secondary">Make</Typography>
<Typography>{vehicle.make}</Typography>
</Box>
)}
{vehicle.model && (
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
<Typography color="text.secondary">Model</Typography>
<Typography>{vehicle.model}</Typography>
</Box>
)}
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography color="text.secondary">Odometer</Typography>
<Typography>{vehicle.odometerReading.toLocaleString()} mi</Typography>
</Box>
</CardContent>
</Card>
</Section>
<Section title="Recent Activity">
<Card>
<CardContent sx={{ textAlign: 'center', py: 8 }}>
<Typography color="text.secondary" variant="body2">
No recent activity
</Typography>
</CardContent>
</Card>
</Section>
</Box>
);
};

View File

@@ -0,0 +1,65 @@
/**
* @ai-summary Mobile-optimized vehicle card component with Material Design 3
*/
import React from 'react';
import { Card, CardActionArea, Box, Typography } from '@mui/material';
import { Vehicle } from '../types/vehicles.types';
interface VehicleMobileCardProps {
vehicle: Vehicle;
onClick?: (vehicle: Vehicle) => void;
compact?: boolean;
}
const CarThumb: React.FC<{ color?: string }> = ({ color = "#F2EAEA" }) => (
<Box
sx={{
height: 120,
bgcolor: color,
borderRadius: 3,
mb: 2,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
/>
);
export const VehicleMobileCard: React.FC<VehicleMobileCardProps> = ({
vehicle,
onClick,
compact = false
}) => {
const displayName = vehicle.nickname ||
(vehicle.year && vehicle.make ? `${vehicle.year} ${vehicle.make}` : 'Vehicle');
const displayModel = vehicle.model || 'Unknown Model';
return (
<Card
sx={{
borderRadius: 18,
overflow: 'hidden',
minWidth: compact ? 260 : 'auto',
width: compact ? 260 : '100%'
}}
>
<CardActionArea onClick={() => onClick?.(vehicle)}>
<Box sx={{ p: 2 }}>
<CarThumb color={vehicle.color || "#F2EAEA"} />
<Typography variant="h6" sx={{ fontWeight: 700, mb: 0.5 }}>
{displayName}
</Typography>
<Typography variant="body2" color="text.secondary">
{displayModel}
</Typography>
{vehicle.licensePlate && (
<Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: 'block' }}>
{vehicle.licensePlate}
</Typography>
)}
</Box>
</CardActionArea>
</Card>
);
};

View File

@@ -0,0 +1,79 @@
/**
* @ai-summary Mobile-optimized vehicles screen with Material Design 3
*/
import React from 'react';
import { Box, Typography, Grid } from '@mui/material';
import { useVehicles } from '../hooks/useVehicles';
import { VehicleMobileCard } from './VehicleMobileCard';
import { Vehicle } from '../types/vehicles.types';
interface VehiclesMobileScreenProps {
onVehicleSelect?: (vehicle: Vehicle) => void;
}
const Section: React.FC<{ title: string; children: React.ReactNode; right?: React.ReactNode }> = ({
title,
children,
right
}) => (
<Box sx={{ mb: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
{title}
</Typography>
{right}
</Box>
{children}
</Box>
);
export const VehiclesMobileScreen: React.FC<VehiclesMobileScreenProps> = ({
onVehicleSelect
}) => {
const { data: vehicles, isLoading } = useVehicles();
if (isLoading) {
return (
<Box sx={{ pb: 10 }}>
<Box sx={{ textAlign: 'center', py: 12 }}>
<Typography color="text.secondary">Loading vehicles...</Typography>
</Box>
</Box>
);
}
if (!vehicles?.length) {
return (
<Box sx={{ pb: 10 }}>
<Section title="Vehicles">
<Box sx={{ textAlign: 'center', py: 12 }}>
<Typography color="text.secondary" sx={{ mb: 2 }}>
No vehicles added yet
</Typography>
<Typography variant="caption" color="text.secondary">
Add your first vehicle to get started
</Typography>
</Box>
</Section>
</Box>
);
}
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>
);
};

View File

@@ -1,12 +1,13 @@
/**
* @ai-summary Main vehicles page
* @ai-summary Main vehicles page with Material Design 3
*/
import React, { useState } from 'react';
import { Box, Typography, Grid, Button as MuiButton } from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import { useVehicles, useCreateVehicle, useDeleteVehicle } from '../hooks/useVehicles';
import { VehicleCard } from '../components/VehicleCard';
import { VehicleForm } from '../components/VehicleForm';
import { Button } from '../../../shared-minimal/components/Button';
import { Card } from '../../../shared-minimal/components/Card';
import { useAppStore } from '../../../core/store';
import { useNavigate } from 'react-router-dom';
@@ -33,24 +34,45 @@ export const VehiclesPage: React.FC = () => {
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading vehicles...</div>
</div>
<Box sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '50vh'
}}>
<Typography color="text.secondary">Loading vehicles...</Typography>
</Box>
);
}
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<h1 className="text-2xl font-bold text-gray-900">My Vehicles</h1>
<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 && (
<Button onClick={() => setShowForm(true)}>Add Vehicle</Button>
<MuiButton
variant="contained"
startIcon={<AddIcon />}
onClick={() => setShowForm(true)}
sx={{ borderRadius: '999px' }}
>
Add Vehicle
</MuiButton>
)}
</div>
</Box>
{showForm && (
<Card>
<h2 className="text-lg font-semibold mb-4">Add New Vehicle</h2>
<Card className="mb-6">
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>
Add New Vehicle
</Typography>
<VehicleForm
onSubmit={async (data) => {
await createVehicle.mutateAsync(data);
@@ -64,26 +86,36 @@ export const VehiclesPage: React.FC = () => {
{vehicles?.length === 0 ? (
<Card>
<div className="text-center py-12">
<p className="text-gray-500 mb-4">No vehicles added yet</p>
<Box sx={{ textAlign: 'center', py: 8 }}>
<Typography color="text.secondary" sx={{ mb: 3 }}>
No vehicles added yet
</Typography>
{!showForm && (
<Button onClick={() => setShowForm(true)}>Add Your First Vehicle</Button>
<MuiButton
variant="contained"
startIcon={<AddIcon />}
onClick={() => setShowForm(true)}
sx={{ borderRadius: '999px' }}
>
Add Your First Vehicle
</MuiButton>
)}
</div>
</Box>
</Card>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<Grid container spacing={3}>
{vehicles?.map((vehicle) => (
<VehicleCard
key={vehicle.id}
vehicle={vehicle}
onEdit={(v) => console.log('Edit', v)}
onDelete={handleDelete}
onSelect={handleSelectVehicle}
/>
<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>
))}
</div>
</Grid>
)}
</div>
</Box>
);
};