MVP with new UX
This commit is contained in:
457
motovaultpro_mobile_v2.jsx
Normal file
457
motovaultpro_mobile_v2.jsx
Normal 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: "CR‑V" },
|
||||
];
|
||||
|
||||
// ---- 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user