Files
motovaultpro/motovaultpro_mobile_v2.jsx
Eric Gullickson d60c3ec00e MVP with new UX
2025-08-09 17:45:54 -05:00

458 lines
14 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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>
);
}