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

457
motovaultpro_mobile_v2.jsx Normal file
View File

@@ -0,0 +1,457 @@
// App.tsx — Material Design 3 styled prototype for MotoVaultPro (MUI v5)
// Install deps (npm):
// npm install @mui/material @emotion/react @emotion/styled @mui/icons-material
// This file is TypeScript-friendly (tsx) but also works in plain React projects if the tooling supports TS.
import * as React from "react";
import { useEffect, useMemo, useState } from "react";
import { ThemeProvider, createTheme, alpha } from "@mui/material/styles";
import CssBaseline from "@mui/material/CssBaseline"; // <-- FIX: import from @mui/material, not styles
import {
Box,
Container,
Typography,
Card,
CardContent,
CardActionArea,
Button,
Grid,
Divider,
BottomNavigation,
BottomNavigationAction,
Select,
MenuItem,
FormControl,
InputLabel,
TextField,
ToggleButtonGroup,
ToggleButton,
} from "@mui/material";
import HomeRoundedIcon from "@mui/icons-material/HomeRounded";
import DirectionsCarRoundedIcon from "@mui/icons-material/DirectionsCarRounded";
import LocalGasStationRoundedIcon from "@mui/icons-material/LocalGasStationRounded";
import SettingsRoundedIcon from "@mui/icons-material/SettingsRounded";
// ---- Theme (Material Design 3-inspired) ----
const primaryHex = "#7A212A"; // brand color
const theme = createTheme({
palette: {
mode: "light",
primary: { main: primaryHex },
secondary: { main: alpha(primaryHex, 0.8) },
background: { default: "#F8F5F3", paper: "#FFFFFF" },
},
shape: { borderRadius: 16 },
typography: {
fontFamily:
"Inter, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif",
h3: { fontWeight: 700, letterSpacing: -0.5 },
},
components: {
MuiCard: {
styleOverrides: {
root: {
borderRadius: 20,
boxShadow:
"0 1px 2px rgba(16,24,40,.04), 0 4px 16px rgba(16,24,40,.06)",
},
},
},
MuiButton: {
variants: [
// Custom MD3-like "tonal" button
{
// TS note: We define a custom variant name, which runtime supports via props match,
// but TypeScript doesn't know it by default. We handle that at usage with `as any`.
props: { variant: "tonal" as any },
style: ({ theme }) => ({
backgroundColor: alpha(theme.palette.primary.main, 0.12),
color: theme.palette.primary.main,
borderRadius: 999,
textTransform: "none",
fontWeight: 600,
paddingInline: 18,
"&:hover": { backgroundColor: alpha(theme.palette.primary.main, 0.18) },
}),
},
{
props: { variant: "contained" },
style: ({ theme }) => ({
borderRadius: 999,
textTransform: "none",
fontWeight: 700,
paddingInline: 20,
boxShadow: "0 8px 24px " + alpha(theme.palette.primary.main, 0.25),
}),
},
],
},
MuiBottomNavigation: {
styleOverrides: {
root: {
borderTop: "1px solid rgba(16,24,40,.08)",
background: alpha("#FFFFFF", 0.8),
backdropFilter: "blur(8px)",
},
},
},
MuiBottomNavigationAction: {
styleOverrides: {
root: {
minWidth: 0,
paddingTop: 8,
paddingBottom: 8,
"&.Mui-selected": { color: primaryHex },
},
},
},
},
});
// ---- Types ----
export type Vehicle = {
id: number;
year: number;
make: string;
model: string;
image?: string;
};
const vehiclesSeed: Vehicle[] = [
{ id: 1, year: 2021, make: "Chevrolet", model: "Malibu" },
{ id: 2, year: 2019, make: "Toyota", model: "Camry" },
{ id: 3, year: 2018, make: "Ford", model: "F-150" },
{ id: 4, year: 2022, make: "Honda", model: "CRV" },
];
// ---- Reusable bits ----
const Spark = ({
points = [3, 5, 4, 6, 5, 7, 6] as number[],
color = primaryHex,
}) => {
const width = 120;
const height = 36;
const max = Math.max(...points);
const min = Math.min(...points);
const path = points
.map((v, i) => {
const x = (i / (points.length - 1)) * width;
const y = height - ((v - min) / (max - min || 1)) * height;
return `${i === 0 ? "M" : "L"}${x},${y}`;
})
.join(" ");
return (
<Box component="svg" width={width} height={height} viewBox={`0 0 ${width} ${height}`}>
<path d={path} fill="none" stroke={color} strokeWidth={3} strokeLinecap="round" />
</Box>
);
};
const VehicleCard = ({
v,
onClick,
}: {
v: Vehicle;
onClick?: (v: Vehicle) => void;
}) => (
<Card sx={{ borderRadius: 18, overflow: "hidden", minWidth: 260 }}>
<CardActionArea onClick={() => onClick?.(v)}>
<Box sx={{ p: 2 }}>
<Box sx={{ height: 120, bgcolor: "#F2EAEA", borderRadius: 3, mb: 2 }} />
<Typography variant="h6" sx={{ fontWeight: 700, mb: 0.5 }}>
{v.make} {v.model}
</Typography>
<Typography variant="body2" color="text.secondary">
{v.year}
</Typography>
</Box>
</CardActionArea>
</Card>
);
// ---- Screens ----
const Dashboard = ({ recent }: { recent: Vehicle[] }) => (
<Box sx={{ pb: 10 }}>
<Typography variant="h3" sx={{ mb: 2 }}>
Dashboard
</Typography>
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between", mb: 1 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
Recent Vehicles
</Typography>
</Box>
<Box sx={{ display: "flex", gap: 2, overflowX: "auto", pb: 1, mb: 3 }}>
{recent.map((v) => (
<VehicleCard key={v.id} v={v} />
))}
</Box>
<Grid container spacing={2} sx={{ mb: 2 }}>
<Grid item xs={12}>
<Card>
<CardContent sx={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<Box>
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
Fuel Spent This Month
</Typography>
<Typography variant="h4" sx={{ mt: 0.5 }}>
$134.22
</Typography>
</Box>
<Spark />
</CardContent>
</Card>
</Grid>
<Grid item xs={12}>
<Card>
<CardContent sx={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<Box>
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
Average Price
</Typography>
<Typography variant="h4" sx={{ mt: 0.5 }}>
$3.69/gal
</Typography>
</Box>
<Spark points={[2, 3, 5, 4, 6, 5, 7]} />
</CardContent>
</Card>
</Grid>
</Grid>
<Box sx={{ display: "flex", gap: 1.5 }}>
</Box>
</Box>
);
const Vehicles = ({
vehicles,
onOpen,
}: {
vehicles: Vehicle[];
onOpen: (v: Vehicle) => void;
}) => (
<Box sx={{ pb: 10 }}>
<Typography variant="h3" sx={{ mb: 2 }}>
Vehicles
</Typography>
<Grid container spacing={2}>
{vehicles.map((v) => (
<Grid item xs={12} key={v.id}>
<VehicleCard v={v} onClick={onOpen} />
</Grid>
))}
</Grid>
</Box>
);
const VehicleDetail = ({ v, onBack }: { v: Vehicle; onBack: () => void }) => (
<Box sx={{ pb: 10 }}>
<Button variant="text" onClick={onBack}>
Back
</Button>
<Typography variant="h4" sx={{ mt: 1, mb: 2 }}>
{v.make} {v.model}
</Typography>
<Divider sx={{ my: 2 }} />
<Typography variant="subtitle1" sx={{ fontWeight: 600, mb: 1 }}>
Fuel Logs
</Typography>
<Card sx={{ mb: 1 }}>
<CardContent sx={{ display: "flex", justifyContent: "space-between" }}>
<span>Apr 24</span>
<span>15,126 mi</span>
</CardContent>
</Card>
<Card sx={{ mb: 1 }}>
<CardContent sx={{ display: "flex", justifyContent: "space-between" }}>
<span>Mar 13</span>
<span>14,300 mi</span>
</CardContent>
</Card>
<Card sx={{ mb: 1 }}>
<CardContent sx={{ display: "flex", justifyContent: "space-between" }}>
<span>Jan 10</span>
<span>14,055 mi</span>
</CardContent>
</Card>
</Box>
);
const LogFuel = ({ vehicles }: { vehicles: Vehicle[] }) => {
const [vehicleId, setVehicleId] = useState<number>(vehicles[0]?.id || 1);
const [date, setDate] = useState<string>(new Date().toISOString().slice(0, 10));
const [odo, setOdo] = useState<number>(15126);
const [qty, setQty] = useState<number>(12.5);
const [price, setPrice] = useState<number>(3.79);
const handleSave = () => {
alert(`Saved fuel log for vehicle ${vehicleId}`);
};
return (
<Box sx={{ pb: 10 }}>
<Typography variant="h3" sx={{ mb: 2 }}>
Log Fuel
</Typography>
<Grid container spacing={2}>
<Grid item xs={12}>
<FormControl fullWidth>
<InputLabel id="veh">Vehicle</InputLabel>
<Select
labelId="veh"
label="Vehicle"
value={vehicleId}
onChange={(e) => setVehicleId(Number(e.target.value))}
>
{vehicles.map((v) => (
<MenuItem key={v.id} value={v.id}>{`${v.year} ${v.make} ${v.model}`}</MenuItem>
))}
</Select>
</FormControl>
</Grid>
<Grid item xs={12}>
<TextField
fullWidth
type="date"
label="Date"
value={date}
onChange={(e) => setDate(e.target.value)}
InputLabelProps={{ shrink: true }}
/>
</Grid>
<Grid item xs={6}>
<TextField
fullWidth
type="number"
label="Odometer (mi)"
value={odo}
onChange={(e) => setOdo(Number(e.target.value))}
/>
</Grid>
<Grid item xs={6}>
<TextField
fullWidth
type="number"
label="Quantity (gal)"
value={qty}
onChange={(e) => setQty(Number(e.target.value))}
/>
</Grid>
<Grid item xs={6}>
<TextField
fullWidth
type="number"
label="Price / gal"
value={price}
onChange={(e) => setPrice(Number(e.target.value))}
/>
</Grid>
<Grid item xs={6}>
<TextField fullWidth label="Octane (gasoline)" value={"87"} />
</Grid>
<Grid item xs={12}>
<Button onClick={handleSave} size="large" variant="contained">
Save Fuel Log
</Button>
</Grid>
</Grid>
</Box>
);
};
const Settings = () => {
const [distance, setDistance] = useState("mi");
const [fuel, setFuel] = useState("gal");
return (
<Box sx={{ pb: 10 }}>
<Typography variant="h3" sx={{ mb: 2 }}>
Settings
</Typography>
<Typography variant="subtitle2" sx={{ mb: 1 }}>
Distance
</Typography>
<ToggleButtonGroup
exclusive
value={distance}
onChange={(_, v) => v && setDistance(v)}
sx={{ mb: 3 }}
>
<ToggleButton value="mi">Miles</ToggleButton>
<ToggleButton value="km">Kilometers</ToggleButton>
</ToggleButtonGroup>
<Typography variant="subtitle2" sx={{ mb: 1 }}>
Fuel
</Typography>
<ToggleButtonGroup
exclusive
value={fuel}
onChange={(_, v) => v && setFuel(v)}
>
<ToggleButton value="gal">U.S. Gallons</ToggleButton>
<ToggleButton value="L">Liters</ToggleButton>
</ToggleButtonGroup>
</Box>
);
};
// ---- (Dev) Runtime smoke tests ----
const DevTests = () => {
useEffect(() => {
console.assert(!!ThemeProvider, "ThemeProvider should be available");
console.assert(typeof createTheme === "function", "createTheme should be a function");
console.assert(!!CssBaseline, "CssBaseline should be importable from @mui/material");
console.log("[DevTests] Basic runtime checks passed.");
}, []);
return null;
};
// ---- Root App with bottom nav ----
export default function App() {
const [tab, setTab] = useState(0); // 0: Dashboard, 1: Vehicles, 2: Log Fuel, 3: Settings
const [open, setOpen] = useState<null | Vehicle>(null);
const recent = useMemo(() => vehiclesSeed.slice(0, 2), []);
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<DevTests />
<Box
sx={{
minHeight: "100vh",
display: "flex",
alignItems: "center",
justifyContent: "center",
bgcolor: theme.palette.background.default,
}}
>
<Container
maxWidth="xs"
sx={{ bgcolor: "background.paper", borderRadius: 4, p: 2, boxShadow: 6 }}
>
{tab === 0 && <Dashboard recent={recent} />}
{tab === 1 &&
(open ? (
<VehicleDetail v={open} onBack={() => setOpen(null)} />
) : (
<Vehicles vehicles={vehiclesSeed} onOpen={setOpen} />
))}
{tab === 2 && <LogFuel vehicles={vehiclesSeed} />}
{tab === 3 && <Settings />}
<BottomNavigation
showLabels
value={tab}
onChange={(_, v) => setTab(v)}
sx={{ position: "sticky", bottom: 0, mt: 2 }}
>
<BottomNavigationAction label="Dashboard" icon={<HomeRoundedIcon />} />
<BottomNavigationAction label="Vehicles" icon={<DirectionsCarRoundedIcon />} />
<BottomNavigationAction label="Log Fuel" icon={<LocalGasStationRoundedIcon />} />
<BottomNavigationAction label="Settings" icon={<SettingsRoundedIcon />} />
</BottomNavigation>
</Container>
</Box>
</ThemeProvider>
);
}