diff --git a/CLAUDE.md b/CLAUDE.md index f3bd0e4..523db4a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,3 +1,7 @@ +Load .ai/context.json to understand the project's danger zones and loading strategies + +Load AI_README.md and @PROJECT_MAP.md to gain context on the application. + CRITICAL: All development practices and choices should be made taking into account the most context effecient interation with another AI. Any AI should be able to understand this applicaiton with minimal prompting. CRITICAL: All development/testing happens in Docker containers no local package installations: diff --git a/frontend/package.json b/frontend/package.json index 620bdc8..60f615e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,7 +24,12 @@ "zod": "^3.22.4", "date-fns": "^3.0.0", "clsx": "^2.0.0", - "react-hot-toast": "^2.4.1" + "react-hot-toast": "^2.4.1", + "framer-motion": "^10.16.16", + "@mui/material": "^5.15.0", + "@emotion/react": "^11.11.1", + "@emotion/styled": "^11.11.0", + "@mui/icons-material": "^5.15.0" }, "devDependencies": { "@types/react": "^18.2.42", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b783eed..6e53120 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,50 +1,238 @@ /** - * @ai-summary Main app component with routing + * @ai-summary Main app component with routing and mobile navigation */ +import { useState, useEffect } from 'react'; import { Routes, Route, Navigate } from 'react-router-dom'; import { useAuth0 } from '@auth0/auth0-react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { ThemeProvider } from '@mui/material/styles'; +import CssBaseline from '@mui/material/CssBaseline'; +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'; +import { md3Theme } from './shared-minimal/theme/md3Theme'; import { Layout } from './components/Layout'; import { VehiclesPage } from './features/vehicles/pages/VehiclesPage'; +import { VehiclesMobileScreen } from './features/vehicles/mobile/VehiclesMobileScreen'; +import { VehicleDetailMobile } from './features/vehicles/mobile/VehicleDetailMobile'; +import { BottomNavigation, NavigationItem } from './shared-minimal/components/mobile/BottomNavigation'; +import { GlassCard } from './shared-minimal/components/mobile/GlassCard'; import { Button } from './shared-minimal/components/Button'; +import { Vehicle } from './features/vehicles/types/vehicles.types'; + function App() { const { isLoading, isAuthenticated, loginWithRedirect } = useAuth0(); + + // Mobile navigation state - detect mobile screen size with responsive updates + const [mobileMode, setMobileMode] = useState(() => { + if (typeof window !== 'undefined') { + return window.innerWidth <= 768; + } + return false; + }); + const [activeScreen, setActiveScreen] = useState("Vehicles"); + const [selectedVehicle, setSelectedVehicle] = useState(null); + + // Update mobile mode on window resize + useEffect(() => { + const checkMobileMode = () => { + const isMobile = window.innerWidth <= 768 || + /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); + console.log('Window width:', window.innerWidth, 'User agent mobile:', /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent), 'Mobile mode:', isMobile); + setMobileMode(isMobile); + }; + + // Check on mount + checkMobileMode(); + + window.addEventListener('resize', checkMobileMode); + return () => window.removeEventListener('resize', checkMobileMode); + }, []); + + // Mobile navigation items + const mobileNavItems: NavigationItem[] = [ + { key: "Dashboard", label: "Dashboard", icon: }, + { key: "Vehicles", label: "Vehicles", icon: }, + { key: "Log Fuel", label: "Log Fuel", icon: }, + { key: "Settings", label: "Settings", icon: }, + ]; + + console.log('MotoVaultPro status:', { isLoading, isAuthenticated, mobileMode, userAgent: navigator.userAgent }); + + // Debug component for testing + const DebugInfo = () => ( +
+ Mode: {mobileMode ? 'Mobile' : 'Desktop'} | Auth: {isAuthenticated ? 'Yes' : 'No'} | Screen: {typeof window !== 'undefined' ? window.innerWidth : 'N/A'}px +
+ ); + + // Placeholder screens for mobile + const DashboardScreen = () => ( +
+ +
+

Dashboard

+

Coming soon - Vehicle insights and analytics

+
+
+
+ ); + + const LogFuelScreen = () => ( +
+ +
+

Log Fuel

+

Coming soon - Fuel logging functionality

+
+
+
+ ); + + const SettingsScreen = () => ( +
+ +
+

Settings

+

Coming soon - App settings and preferences

+
+
+
+ ); if (isLoading) { + if (mobileMode) { + return ( + + + +
+
Loading...
+
+
+
+ ); + } return ( -
-
Loading...
-
+ + +
+
Loading...
+
+
); } if (!isAuthenticated) { + if (mobileMode) { + return ( + + + +
+
+

Welcome to MotoVaultPro

+

Your personal vehicle management platform

+ +
+
+ +
+
+ ); + } return ( -
-
-

MotoVaultPro

-

Your personal vehicle management platform

- + + +
+
+

MotoVaultPro

+

Your personal vehicle management platform

+ +
+
-
+ ); } + // Mobile app rendering + if (mobileMode) { + return ( + + + + + {activeScreen === "Dashboard" && ( + + + + )} + {activeScreen === "Vehicles" && ( + + {selectedVehicle ? ( + setSelectedVehicle(null)} + onLogFuel={() => setActiveScreen("Log Fuel")} + /> + ) : ( + setSelectedVehicle(vehicle)} + /> + )} + + )} + {activeScreen === "Log Fuel" && ( + + + + )} + {activeScreen === "Settings" && ( + + + + )} + + + + + + + ); + } + + // Desktop app rendering (fallback) return ( - - - } /> - } /> - Vehicle Details (TODO)
} /> - Fuel Logs (TODO)} /> - Maintenance (TODO)} /> - Stations (TODO)} /> - } /> - - + + + + + } /> + } /> + Vehicle Details (TODO)} /> + Fuel Logs (TODO)} /> + Maintenance (TODO)} /> + Stations (TODO)} /> + } /> + + + + ); } diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 511b497..15b7cbf 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -1,130 +1,244 @@ /** - * @ai-summary Main layout component with navigation + * @ai-summary Main layout component with navigation (desktop/mobile adaptive) */ import React from 'react'; import { useAuth0 } from '@auth0/auth0-react'; import { Link, useLocation } from 'react-router-dom'; +import { Container, Paper, Typography, Box, IconButton, Avatar } from '@mui/material'; +import { useTheme } from '@mui/material/styles'; +import DirectionsCarRoundedIcon from '@mui/icons-material/DirectionsCarRounded'; +import LocalGasStationRoundedIcon from '@mui/icons-material/LocalGasStationRounded'; +import BuildRoundedIcon from '@mui/icons-material/BuildRounded'; +import PlaceRoundedIcon from '@mui/icons-material/PlaceRounded'; +import MenuIcon from '@mui/icons-material/Menu'; +import CloseIcon from '@mui/icons-material/Close'; import { useAppStore } from '../core/store'; import { Button } from '../shared-minimal/components/Button'; -import { clsx } from 'clsx'; interface LayoutProps { children: React.ReactNode; + mobileMode?: boolean; } -export const Layout: React.FC = ({ children }) => { +export const Layout: React.FC = ({ children, mobileMode = false }) => { const { user, logout } = useAuth0(); const { sidebarOpen, toggleSidebar } = useAppStore(); const location = useLocation(); + const theme = useTheme(); const navigation = [ - { name: 'Vehicles', href: '/vehicles', icon: '🚗' }, - { name: 'Fuel Logs', href: '/fuel-logs', icon: '⛽' }, - { name: 'Maintenance', href: '/maintenance', icon: '🔧' }, - { name: 'Gas Stations', href: '/stations', icon: '🏪' }, + { name: 'Vehicles', href: '/vehicles', icon: }, + { name: 'Fuel Logs', href: '/fuel-logs', icon: }, + { name: 'Maintenance', href: '/maintenance', icon: }, + { name: 'Gas Stations', href: '/stations', icon: }, ]; - return ( -
- {/* Sidebar */} -
-
-

MotoVaultPro

- -
+ // Mobile layout + if (mobileMode) { + return ( +
+ + {/* App header */} +
+
+
MotoVaultPro
+
v0.1
+
+
+ {/* Content area */} +
+
+ {children} +
+
+
+
+ ); + } - + ); + })} + -
-
-
-
- - {user?.name?.charAt(0) || user?.email?.charAt(0)} - -
-
-
-

+ + + + {user?.name?.charAt(0) || user?.email?.charAt(0)} + + + {user?.name || user?.email} -

-
-
+ + + -
-
+ + {/* Main content */} -
+ {/* Top bar */} -
-
- -
+ + + Welcome back, {user?.name || user?.email} -
-
-
+ +
+ {/* Page content */} -
-
+ + {children} -
-
-
+ + + {/* Backdrop */} {sidebarOpen && ( -
)} -
+ ); }; \ No newline at end of file diff --git a/frontend/src/features/vehicles/components/VehicleCard.tsx b/frontend/src/features/vehicles/components/VehicleCard.tsx index 4c0b00f..ef256f2 100644 --- a/frontend/src/features/vehicles/components/VehicleCard.tsx +++ b/frontend/src/features/vehicles/components/VehicleCard.tsx @@ -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" }) => ( + +); + export const VehicleCard: React.FC = ({ vehicle, onEdit, onDelete, onSelect, }) => { + const displayName = vehicle.nickname || + `${vehicle.year} ${vehicle.make} ${vehicle.model}`; + return ( - onSelect(vehicle.id)}> -
-
-

- {vehicle.nickname || `${vehicle.year} ${vehicle.make} ${vehicle.model}`} -

-

VIN: {vehicle.vin}

+ + onSelect(vehicle.id)} + sx={{ flexGrow: 1 }} + > + + + + + {displayName} + + + + VIN: {vehicle.vin} + + {vehicle.licensePlate && ( -

License: {vehicle.licensePlate}

+ + License: {vehicle.licensePlate} + )} -

+ + Odometer: {vehicle.odometerReading.toLocaleString()} miles -

-
- -
- - -
-
+ + + + + + { + e.stopPropagation(); + onEdit(vehicle); + }} + sx={{ color: 'text.secondary' }} + > + + + { + e.stopPropagation(); + onDelete(vehicle.id); + }} + sx={{ color: 'error.main' }} + > + + +
); }; \ No newline at end of file diff --git a/frontend/src/features/vehicles/mobile/VehicleDetailMobile.tsx b/frontend/src/features/vehicles/mobile/VehicleDetailMobile.tsx new file mode 100644 index 0000000..61a8ad5 --- /dev/null +++ b/frontend/src/features/vehicles/mobile/VehicleDetailMobile.tsx @@ -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 +}) => ( + + + {title} + + {children} + +); + +const CarThumb: React.FC<{ color?: string }> = ({ color = "#F2EAEA" }) => ( + +); + +export const VehicleDetailMobile: React.FC = ({ + vehicle, + onBack, + onLogFuel +}) => { + const displayName = vehicle.nickname || + (vehicle.year && vehicle.make ? `${vehicle.year} ${vehicle.make}` : 'Vehicle'); + const displayModel = vehicle.model || 'Unknown Model'; + + return ( + + + + {displayName} + + + + + + + + + + {displayName} + + {displayModel} + {vehicle.licensePlate && ( + + {vehicle.licensePlate} + + )} + + + + + + + + +
+ + + {vehicle.vin && ( + + VIN + + {vehicle.vin} + + + )} + {vehicle.year && ( + + Year + {vehicle.year} + + )} + {vehicle.make && ( + + Make + {vehicle.make} + + )} + {vehicle.model && ( + + Model + {vehicle.model} + + )} + + Odometer + {vehicle.odometerReading.toLocaleString()} mi + + + +
+ +
+ + + + No recent activity + + + +
+
+ ); +}; \ No newline at end of file diff --git a/frontend/src/features/vehicles/mobile/VehicleMobileCard.tsx b/frontend/src/features/vehicles/mobile/VehicleMobileCard.tsx new file mode 100644 index 0000000..599403d --- /dev/null +++ b/frontend/src/features/vehicles/mobile/VehicleMobileCard.tsx @@ -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" }) => ( + +); + +export const VehicleMobileCard: React.FC = ({ + vehicle, + onClick, + compact = false +}) => { + const displayName = vehicle.nickname || + (vehicle.year && vehicle.make ? `${vehicle.year} ${vehicle.make}` : 'Vehicle'); + const displayModel = vehicle.model || 'Unknown Model'; + + return ( + + onClick?.(vehicle)}> + + + + {displayName} + + + {displayModel} + + {vehicle.licensePlate && ( + + {vehicle.licensePlate} + + )} + + + + ); +}; \ No newline at end of file diff --git a/frontend/src/features/vehicles/mobile/VehiclesMobileScreen.tsx b/frontend/src/features/vehicles/mobile/VehiclesMobileScreen.tsx new file mode 100644 index 0000000..730f4d1 --- /dev/null +++ b/frontend/src/features/vehicles/mobile/VehiclesMobileScreen.tsx @@ -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 +}) => ( + + + + {title} + + {right} + + {children} + +); + +export const VehiclesMobileScreen: React.FC = ({ + onVehicleSelect +}) => { + const { data: vehicles, isLoading } = useVehicles(); + + if (isLoading) { + return ( + + + Loading vehicles... + + + ); + } + + if (!vehicles?.length) { + return ( + +
+ + + No vehicles added yet + + + Add your first vehicle to get started + + +
+
+ ); + } + + return ( + +
+ + {vehicles.map((vehicle) => ( + + onVehicleSelect?.(vehicle)} + /> + + ))} + +
+
+ ); +}; \ No newline at end of file diff --git a/frontend/src/features/vehicles/pages/VehiclesPage.tsx b/frontend/src/features/vehicles/pages/VehiclesPage.tsx index b79c725..abce956 100644 --- a/frontend/src/features/vehicles/pages/VehiclesPage.tsx +++ b/frontend/src/features/vehicles/pages/VehiclesPage.tsx @@ -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 ( -
-
Loading vehicles...
-
+ + Loading vehicles... + ); } return ( -
-
-

My Vehicles

+ + + + My Vehicles + {!showForm && ( - + } + onClick={() => setShowForm(true)} + sx={{ borderRadius: '999px' }} + > + Add Vehicle + )} -
+ {showForm && ( - -

Add New Vehicle

+ + + Add New Vehicle + { await createVehicle.mutateAsync(data); @@ -64,26 +86,36 @@ export const VehiclesPage: React.FC = () => { {vehicles?.length === 0 ? ( -
-

No vehicles added yet

+ + + No vehicles added yet + {!showForm && ( - + } + onClick={() => setShowForm(true)} + sx={{ borderRadius: '999px' }} + > + Add Your First Vehicle + )} -
+
) : ( -
+ {vehicles?.map((vehicle) => ( - console.log('Edit', v)} - onDelete={handleDelete} - onSelect={handleSelectVehicle} - /> + + console.log('Edit', v)} + onDelete={handleDelete} + onSelect={handleSelectVehicle} + /> + ))} -
+ )} -
+
); }; \ No newline at end of file diff --git a/frontend/src/shared-minimal/components/Card.tsx b/frontend/src/shared-minimal/components/Card.tsx index 7b5c782..68f935a 100644 --- a/frontend/src/shared-minimal/components/Card.tsx +++ b/frontend/src/shared-minimal/components/Card.tsx @@ -1,9 +1,9 @@ /** - * @ai-summary Reusable card component + * @ai-summary Reusable card component with Material Design 3 */ import React from 'react'; -import { clsx } from 'clsx'; +import { Card as MuiCard, CardContent } from '@mui/material'; interface CardProps { children: React.ReactNode; @@ -18,24 +18,27 @@ export const Card: React.FC = ({ padding = 'md', onClick, }) => { - const paddings = { - none: '', - sm: 'p-3', - md: 'p-4', - lg: 'p-6', + const paddingStyles = { + none: 0, + sm: 1, + md: 2, + lg: 3, }; return ( -
- {children} -
+ + {children} + + ); }; \ No newline at end of file diff --git a/frontend/src/shared-minimal/components/mobile/BottomNavigation.tsx b/frontend/src/shared-minimal/components/mobile/BottomNavigation.tsx new file mode 100644 index 0000000..fbce9d6 --- /dev/null +++ b/frontend/src/shared-minimal/components/mobile/BottomNavigation.tsx @@ -0,0 +1,50 @@ +/** + * @ai-summary Bottom navigation component with Material Design 3 + */ + +import React from 'react'; +import { BottomNavigation as MuiBottomNavigation, BottomNavigationAction } from '@mui/material'; + +export interface NavigationItem { + key: string; + label: string; + icon: React.ReactNode; +} + +interface BottomNavigationProps { + items: NavigationItem[]; + activeItem: string; + onItemSelect: (key: string) => void; +} + +export const BottomNavigation: React.FC = ({ + items, + activeItem, + onItemSelect +}) => { + const activeIndex = items.findIndex(item => item.key === activeItem); + + return ( + onItemSelect(items[newValue].key)} + sx={{ + position: 'fixed', + bottom: 0, + left: 0, + right: 0, + zIndex: 1000, + width: '100%' + }} + > + {items.map(({ key, label, icon }) => ( + + ))} + + ); +}; \ No newline at end of file diff --git a/frontend/src/shared-minimal/components/mobile/GlassCard.tsx b/frontend/src/shared-minimal/components/mobile/GlassCard.tsx new file mode 100644 index 0000000..6413641 --- /dev/null +++ b/frontend/src/shared-minimal/components/mobile/GlassCard.tsx @@ -0,0 +1,41 @@ +/** + * @ai-summary Glass morphism card component for mobile UI + */ + +import React from 'react'; +import { clsx } from 'clsx'; + +interface GlassCardProps { + children: React.ReactNode; + className?: string; + padding?: 'none' | 'sm' | 'md' | 'lg'; + onClick?: () => void; +} + +export const GlassCard: React.FC = ({ + children, + className, + padding = 'md', + onClick, +}) => { + const paddings = { + none: '', + sm: 'p-3', + md: 'p-4', + lg: 'p-6', + }; + + return ( +
+ {children} +
+ ); +}; \ No newline at end of file diff --git a/frontend/src/shared-minimal/components/mobile/MobileContainer.tsx b/frontend/src/shared-minimal/components/mobile/MobileContainer.tsx new file mode 100644 index 0000000..2628cd0 --- /dev/null +++ b/frontend/src/shared-minimal/components/mobile/MobileContainer.tsx @@ -0,0 +1,23 @@ +/** + * @ai-summary Mobile app container with glass morphism styling + */ + +import React from 'react'; + +interface MobileContainerProps { + children: React.ReactNode; + className?: string; +} + +export const MobileContainer: React.FC = ({ + children, + className = '' +}) => { + return ( +
+
+ {children} +
+
+ ); +}; \ No newline at end of file diff --git a/frontend/src/shared-minimal/components/mobile/MobilePill.tsx b/frontend/src/shared-minimal/components/mobile/MobilePill.tsx new file mode 100644 index 0000000..61d3848 --- /dev/null +++ b/frontend/src/shared-minimal/components/mobile/MobilePill.tsx @@ -0,0 +1,40 @@ +/** + * @ai-summary Mobile pill button component with gradient styling + */ + +import React from 'react'; +import { clsx } from 'clsx'; + +// Theme colors now defined in Tailwind config + +interface MobilePillProps { + active?: boolean; + label: string; + onClick?: () => void; + icon?: React.ReactNode; + className?: string; +} + +export const MobilePill: React.FC = ({ + active = false, + label, + onClick, + icon, + className +}) => { + return ( + + ); +}; \ No newline at end of file diff --git a/frontend/src/shared-minimal/theme/md3Theme.ts b/frontend/src/shared-minimal/theme/md3Theme.ts new file mode 100644 index 0000000..6888abc --- /dev/null +++ b/frontend/src/shared-minimal/theme/md3Theme.ts @@ -0,0 +1,96 @@ +/** + * @ai-summary Material Design 3 theme configuration for MotoVaultPro + */ + +import { createTheme, alpha } from '@mui/material/styles'; + +// Brand color from mockup +const primaryHex = '#7A212A'; + +export const md3Theme = 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 + { + 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 + }, + }, + }, + }, + }, +}); \ No newline at end of file diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index f1e3f66..d8d48cd 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -9,13 +9,18 @@ export default { colors: { primary: { 50: '#eff6ff', - 500: '#3b82f6', - 600: '#2563eb', - 700: '#1d4ed8', + 500: '#7A212A', + 600: '#7A212A', + 700: '#9c2a36', }, gray: { 850: '#18202f', - } + }, + 'moto-red': '#7A212A', + 'moto-red-light': '#9c2a36', + }, + backgroundImage: { + 'gradient-moto': 'linear-gradient(90deg, #7A212A, #9c2a36)', }, }, }, diff --git a/moto_vault_pro_mobile_ux_prototype_react.jsx b/moto_vault_pro_mobile_ux_prototype_react.jsx deleted file mode 100644 index a81f829..0000000 --- a/moto_vault_pro_mobile_ux_prototype_react.jsx +++ /dev/null @@ -1,387 +0,0 @@ -import React, { useMemo, useState } from "react"; -import { motion, AnimatePresence } from "framer-motion"; - -// Theme -const primary = "#7A212A"; // requested -const primaryLight = "#9c2a36"; - -// Icons -const IconDash = (props) => ( - - - - - -); -const IconCar = (props) => ( - - - - - - -); -const IconFuel = (props) => ( - - - - - -); -const IconSettings = (props) => ( - - - - -); - -// Visual -const CarThumb = ({ color = "#e5e7eb" }) => ( - - - - - - - - - - - - -); - -const vehiclesSeed = [ - { id: 1, year: 2020, make: "Toyota", model: "Camry", color: "#93c5fd" }, - { id: 2, year: 2018, make: "Ford", model: "F-150", color: "#a5b4fc" }, - { id: 3, year: 2022, make: "Honda", model: "CR-V", color: "#fbcfe8" }, - { id: 4, year: 2015, make: "Subaru", model: "Outback", color: "#86efac" }, -]; - -const Section = ({ title, children, right }) => ( -
-
-

{title}

- {right} -
- {children} -
-); - -const Pill = ({ active, label, onClick, icon }) => ( - -); - -const SparkBar = ({ values }) => { - const max = Math.max(...values, 1); - return ( -
- {values.map((v, i) => ( -
- ))} -
- ); -}; - -const StatCard = ({ label, value, sub }) => ( -
-
{label}
-
{value}
- {sub &&
{sub}
} -
-); - -const VehicleCard = ({ v, onClick, compact=false }) => ( - -); - -const VehiclesScreen = ({ vehicles, onOpen }) => ( -
-
-
- {vehicles.map((v) => ( - - ))} -
-
-
-); - -const RecentVehicles = ({ recent, onOpen }) => ( -
Last used
}> -
- {recent.map((v) => ( - - ))} -
- -); - -const DashboardScreen = ({ recent }) => ( -
-
- - -
-
-
-
Fuel Spent This Month
-
Last 30 days
-
- -
- -
-
-
Fuel Spend Per Vehicle
-
This month
-
-
- {[ - { name: "Camry", val: 92 }, - { name: "F-150", val: 104 }, - { name: "CR‑V", val: 42 }, - ].map(({ name, val }) => ( -
-
{name}
-
-
-
-
${val}
-
- ))} -
-
- - -
-); - -const Field = ({ label, children }) => ( -
- - {children} -
-); - -const FuelForm = ({ units, vehicles, onSave }) => { - const [vehicleId, setVehicleId] = useState(vehicles[0]?.id || ""); - const [date, setDate] = useState(() => new Date().toISOString().slice(0,10)); - const [odo, setOdo] = useState(15126); - const [qty, setQty] = useState(12.5); - const [price, setPrice] = useState(3.79); - const [octane, setOctane] = useState("87"); - const dist = units.distance === "mi" ? "mi" : "km"; - const fuel = units.fuel === "gal" ? "gal" : "L"; - - const inputCls = "w-full h-11 px-3 rounded-xl border border-slate-200 bg-white/80 backdrop-blur focus:outline-none focus:ring-2 focus:ring-[rgba(122,33,42,0.35)]"; - const selectCls = inputCls; - - return ( -
{ e.preventDefault(); onSave?.({ vehicleId, date, odo, qty, price, octane }); }} - > -
-
-
- - - -
-
- - setDate(e.target.value)} className={inputCls}/> - -
- - setOdo(Number(e.target.value))} className={inputCls}/> - - - setQty(Number(e.target.value))} className={inputCls}/> - - - setPrice(Number(e.target.value))} className={inputCls}/> - - - - -
- -
-
- ); -}; - -const VehicleDetail = ({ vehicle, onBack, onLogFuel }) => ( -
- -
-
-
-
{vehicle.year} {vehicle.make}
-
{vehicle.model}
-
-
-
- - -
-
-
- {[{d:"Apr 24", odo:"15,126 mi"},{d:"Mar 13", odo:"14,300 mi"},{d:"Jan 10", odo:"14,055 mi"}].map((r,i)=>( -
- {r.d}{r.odo} -
- ))} -
-
-
-); - -const SettingsScreen = ({ units, setUnits }) => ( -
-
-
-
-
Distance
-
- {[ ["mi","Miles"], ["km","Kilometers"] ].map(([val,label])=> ( - - ))} -
-
-
-
Fuel
-
- {[ ["gal","US Gallons"], ["L","Liters"] ].map(([val,label])=> ( - - ))} -
-
-
-
-
-); - -const BottomNav = ({ active, setActive }) => ( -
- {[ - { key: "Dashboard", icon: }, - { key: "Vehicles", icon: }, - { key: "Log Fuel", icon: }, - { key: "Settings", icon: }, - ].map(({ key, icon }) => ( - setActive(key)} - /> - ))} -
-); - -export default function App() { - const [active, setActive] = useState("Dashboard"); - const [units, setUnits] = useState({ distance: "mi", fuel: "gal" }); - const [vehicles, setVehicles] = useState(vehiclesSeed); - const [recentIds, setRecentIds] = useState([1,2,3]); - const [openVehicle, setOpenVehicle] = useState(null); - - const recent = useMemo(() => recentIds.map(id => vehicles.find(v=>v.id===id)).filter(Boolean), [recentIds, vehicles]); - - const handleOpenVehicle = (v) => { - setOpenVehicle(v); - setActive("Vehicles"); - setRecentIds(prev => [v.id, ...prev.filter(x=>x!==v.id)].slice(0,4)); - }; - - return ( -
-
- {/* App header */} -
-
-
MotoVaultPro
-
v0.1
-
-
- -
-
- - {active === "Dashboard" && ( - - - - )} - {active === "Vehicles" && ( - - {openVehicle ? ( - setOpenVehicle(null)} onLogFuel={()=>setActive("Log Fuel")} /> - ) : ( - - )} - - )} - {active === "Log Fuel" && ( - - setActive("Vehicles")} /> - - )} - {active === "Settings" && ( - - - - )} - -
-
- -
-
- ); -} diff --git a/motovaultpro_mobile_v2.jsx b/motovaultpro_mobile_v2.jsx new file mode 100644 index 0000000..151eefe --- /dev/null +++ b/motovaultpro_mobile_v2.jsx @@ -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 ( + + + + ); +}; + +const VehicleCard = ({ + v, + onClick, +}: { + v: Vehicle; + onClick?: (v: Vehicle) => void; +}) => ( + + onClick?.(v)}> + + + + {v.make} {v.model} + + + {v.year} + + + + +); + +// ---- Screens ---- +const Dashboard = ({ recent }: { recent: Vehicle[] }) => ( + + + Dashboard + + + + + Recent Vehicles + + + + + {recent.map((v) => ( + + ))} + + + + + + + + + Fuel Spent This Month + + + $134.22 + + + + + + + + + + + + Average Price + + + $3.69/gal + + + + + + + + + + + +); + +const Vehicles = ({ + vehicles, + onOpen, +}: { + vehicles: Vehicle[]; + onOpen: (v: Vehicle) => void; +}) => ( + + + Vehicles + + + {vehicles.map((v) => ( + + + + ))} + + +); + +const VehicleDetail = ({ v, onBack }: { v: Vehicle; onBack: () => void }) => ( + + + + {v.make} {v.model} + + + + Fuel Logs + + + + Apr 24 + 15,126 mi + + + + + Mar 13 + 14,300 mi + + + + + Jan 10 + 14,055 mi + + + +); + +const LogFuel = ({ vehicles }: { vehicles: Vehicle[] }) => { + const [vehicleId, setVehicleId] = useState(vehicles[0]?.id || 1); + const [date, setDate] = useState(new Date().toISOString().slice(0, 10)); + const [odo, setOdo] = useState(15126); + const [qty, setQty] = useState(12.5); + const [price, setPrice] = useState(3.79); + const handleSave = () => { + alert(`Saved fuel log for vehicle ${vehicleId}`); + }; + return ( + + + Log Fuel + + + + + Vehicle + + + + + setDate(e.target.value)} + InputLabelProps={{ shrink: true }} + /> + + + setOdo(Number(e.target.value))} + /> + + + setQty(Number(e.target.value))} + /> + + + setPrice(Number(e.target.value))} + /> + + + + + + + + + + ); +}; + +const Settings = () => { + const [distance, setDistance] = useState("mi"); + const [fuel, setFuel] = useState("gal"); + return ( + + + Settings + + + Distance + + v && setDistance(v)} + sx={{ mb: 3 }} + > + Miles + Kilometers + + + + Fuel + + v && setFuel(v)} + > + U.S. Gallons + Liters + + + ); +}; + +// ---- (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); + const recent = useMemo(() => vehiclesSeed.slice(0, 2), []); + + return ( + + + + + + {tab === 0 && } + {tab === 1 && + (open ? ( + setOpen(null)} /> + ) : ( + + ))} + {tab === 2 && } + {tab === 3 && } + setTab(v)} + sx={{ position: "sticky", bottom: 0, mt: 2 }} + > + } /> + } /> + } /> + } /> + + + + + ); +}