feat: navigation and UX improvements complete

This commit is contained in:
Eric Gullickson
2025-12-26 09:25:42 -06:00
parent 50baec390f
commit 8c13dc0a55
23 changed files with 327 additions and 126 deletions

View File

@@ -17,6 +17,7 @@ import CloseIcon from '@mui/icons-material/Close';
import { useAppStore } from '../core/store';
import { Button } from '../shared-minimal/components/Button';
import { NotificationBell } from '../features/notifications';
import { useThemeSync } from '../shared-minimal/theme/useThemeSync';
interface LayoutProps {
children: React.ReactNode;
@@ -28,6 +29,9 @@ export const Layout: React.FC<LayoutProps> = ({ children, mobileMode = false })
const { sidebarOpen, toggleSidebar, setSidebarOpen } = useAppStore();
const location = useLocation();
// Sync theme preference with backend
useThemeSync();
// Ensure desktop has a visible navigation by default (only on mount)
React.useEffect(() => {
if (!mobileMode && !sidebarOpen) {
@@ -72,7 +76,7 @@ export const Layout: React.FC<LayoutProps> = ({ children, mobileMode = false })
/>
<div className="flex items-center gap-2">
<NotificationBell />
<div className="text-xs text-slate-500">v1.0</div>
<div className="text-xs text-slate-500 dark:text-titanio">v1.0</div>
</div>
</div>
</div>
@@ -157,7 +161,7 @@ export const Layout: React.FC<LayoutProps> = ({ children, mobileMode = false })
? 'primary.main'
: 'transparent',
color: isActive
? '#FFFFFF'
? 'primary.contrastText'
: 'text.primary',
'&:hover': {
backgroundColor: isActive

View File

@@ -49,38 +49,38 @@ export const SignupForm: React.FC<SignupFormProps> = ({ onSubmit, loading }) =>
return (
<form onSubmit={handleSubmit(handleFormSubmit)} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Email Address <span className="text-red-500">*</span>
<label className="block text-sm font-medium text-avus mb-1">
Email Address <span className="text-red-400">*</span>
</label>
<input
{...register('email')}
type="email"
inputMode="email"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500 min-h-[44px]"
className="w-full px-3 py-2 border rounded-md min-h-[44px] bg-scuro text-avus border-silverstone placeholder-canna focus:outline-none focus:ring-2 focus:ring-abudhabi focus:border-abudhabi"
placeholder="your.email@example.com"
style={{ fontSize: '16px' }}
/>
{errors.email && (
<p className="mt-1 text-sm text-red-600">{errors.email.message}</p>
<p className="mt-1 text-sm text-red-400">{errors.email.message}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Password <span className="text-red-500">*</span>
<label className="block text-sm font-medium text-avus mb-1">
Password <span className="text-red-400">*</span>
</label>
<div className="relative">
<input
{...register('password')}
type={showPassword ? 'text' : 'password'}
className="w-full px-3 py-2 pr-10 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500 min-h-[44px]"
className="w-full px-3 py-2 pr-10 border rounded-md min-h-[44px] bg-scuro text-avus border-silverstone placeholder-canna focus:outline-none focus:ring-2 focus:ring-abudhabi focus:border-abudhabi"
placeholder="At least 8 characters"
style={{ fontSize: '16px' }}
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700 focus:outline-none min-w-[44px] min-h-[44px] flex items-center justify-center"
className="absolute right-3 top-1/2 -translate-y-1/2 text-titanio hover:text-avus focus:outline-none min-w-[44px] min-h-[44px] flex items-center justify-center"
aria-label={showPassword ? 'Hide password' : 'Show password'}
>
{showPassword ? (
@@ -96,29 +96,29 @@ export const SignupForm: React.FC<SignupFormProps> = ({ onSubmit, loading }) =>
</button>
</div>
{errors.password && (
<p className="mt-1 text-sm text-red-600">{errors.password.message}</p>
<p className="mt-1 text-sm text-red-400">{errors.password.message}</p>
)}
<p className="mt-1 text-xs text-gray-600">
<p className="mt-1 text-xs text-titanio">
Must be at least 8 characters with one uppercase letter and one number
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Confirm Password <span className="text-red-500">*</span>
<label className="block text-sm font-medium text-avus mb-1">
Confirm Password <span className="text-red-400">*</span>
</label>
<div className="relative">
<input
{...register('confirmPassword')}
type={showConfirmPassword ? 'text' : 'password'}
className="w-full px-3 py-2 pr-10 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500 min-h-[44px]"
className="w-full px-3 py-2 pr-10 border rounded-md min-h-[44px] bg-scuro text-avus border-silverstone placeholder-canna focus:outline-none focus:ring-2 focus:ring-abudhabi focus:border-abudhabi"
placeholder="Re-enter your password"
style={{ fontSize: '16px' }}
/>
<button
type="button"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700 focus:outline-none min-w-[44px] min-h-[44px] flex items-center justify-center"
className="absolute right-3 top-1/2 -translate-y-1/2 text-titanio hover:text-avus focus:outline-none min-w-[44px] min-h-[44px] flex items-center justify-center"
aria-label={showConfirmPassword ? 'Hide password' : 'Show password'}
>
{showConfirmPassword ? (
@@ -134,7 +134,7 @@ export const SignupForm: React.FC<SignupFormProps> = ({ onSubmit, loading }) =>
</button>
</div>
{errors.confirmPassword && (
<p className="mt-1 text-sm text-red-600">{errors.confirmPassword.message}</p>
<p className="mt-1 text-sm text-red-400">{errors.confirmPassword.message}</p>
)}
</div>

View File

@@ -25,24 +25,28 @@ export const SignupPage: React.FC = () => {
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-rose-50 flex items-center justify-center p-4">
<div className="min-h-screen bg-nero flex items-center justify-center p-4">
<div className="w-full max-w-md">
<div className="bg-white rounded-lg shadow-lg p-8">
<div className="bg-nero border border-white/10 rounded-lg shadow-lg shadow-black/30 p-8">
<div className="text-center mb-8">
<h1 className="text-3xl font-bold text-primary-600 mb-2">MotoVaultPro</h1>
<h2 className="text-xl font-semibold text-gray-800">Create Your Account</h2>
<p className="text-sm text-gray-600 mt-2">
<img
src="/images/logos/motovaultpro-title-slogan.png"
alt="MotoVaultPro - Precision Vehicle Management"
className="h-12 md:h-14 w-auto mx-auto mb-4"
/>
<h2 className="text-xl font-semibold text-avus">Create Your Account</h2>
<p className="text-sm text-titanio mt-2">
Start tracking your vehicle maintenance and fuel logs
</p>
</div>
<SignupForm onSubmit={handleSubmit} loading={isPending} />
<div className="mt-6 text-center text-sm text-gray-600">
<div className="mt-6 text-center text-sm text-titanio">
Already have an account?{' '}
<button
onClick={() => navigate('/login')}
className="text-primary-600 hover:text-primary-700 font-medium focus:outline-none focus:underline"
className="text-primary-500 hover:text-primary-400 font-medium focus:outline-none focus:underline"
>
Login
</button>

View File

@@ -158,7 +158,7 @@ export const DocumentForm: React.FC<DocumentFormProps> = ({ onSuccess, onCancel
<form onSubmit={handleSubmit} className="w-full">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex flex-col">
<label className="text-sm font-medium text-slate-700 mb-1">Vehicle</label>
<label className="text-sm font-medium text-slate-700 dark:text-avus mb-1">Vehicle</label>
<select
className="h-11 min-h-[44px] rounded-lg border px-3 bg-white text-gray-900 border-slate-300 focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-scuro dark:text-avus dark:border-silverstone dark:focus:ring-abudhabi dark:focus:border-abudhabi"
value={vehicleID}
@@ -173,7 +173,7 @@ export const DocumentForm: React.FC<DocumentFormProps> = ({ onSuccess, onCancel
</div>
<div className="flex flex-col">
<label className="text-sm font-medium text-slate-700 mb-1">Document Type</label>
<label className="text-sm font-medium text-slate-700 dark:text-avus mb-1">Document Type</label>
<select
className="h-11 min-h-[44px] rounded-lg border px-3 bg-white text-gray-900 border-slate-300 focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-scuro dark:text-avus dark:border-silverstone dark:focus:ring-abudhabi dark:focus:border-abudhabi"
value={documentType}
@@ -186,7 +186,7 @@ export const DocumentForm: React.FC<DocumentFormProps> = ({ onSuccess, onCancel
</div>
<div className="flex flex-col md:col-span-2">
<label className="text-sm font-medium text-slate-700 mb-1">Title</label>
<label className="text-sm font-medium text-slate-700 dark:text-avus mb-1">Title</label>
<input
className="h-11 min-h-[44px] rounded-lg border px-3 bg-white text-gray-900 border-slate-300 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-scuro dark:text-avus dark:border-silverstone dark:placeholder-canna dark:focus:ring-abudhabi dark:focus:border-abudhabi"
type="text"
@@ -204,7 +204,7 @@ export const DocumentForm: React.FC<DocumentFormProps> = ({ onSuccess, onCancel
{documentType === 'insurance' && (
<>
<div className="flex flex-col">
<label className="text-sm font-medium text-slate-700 mb-1">Insurance company</label>
<label className="text-sm font-medium text-slate-700 dark:text-avus mb-1">Insurance company</label>
<input
className="h-11 min-h-[44px] rounded-lg border px-3 bg-white text-gray-900 border-slate-300 focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-scuro dark:text-avus dark:border-silverstone dark:focus:ring-abudhabi dark:focus:border-abudhabi"
type="text"
@@ -213,7 +213,7 @@ export const DocumentForm: React.FC<DocumentFormProps> = ({ onSuccess, onCancel
/>
</div>
<div className="flex flex-col">
<label className="text-sm font-medium text-slate-700 mb-1">Policy number</label>
<label className="text-sm font-medium text-slate-700 dark:text-avus mb-1">Policy number</label>
<input
className="h-11 min-h-[44px] rounded-lg border px-3 bg-white text-gray-900 border-slate-300 focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-scuro dark:text-avus dark:border-silverstone dark:focus:ring-abudhabi dark:focus:border-abudhabi"
type="text"
@@ -260,7 +260,7 @@ export const DocumentForm: React.FC<DocumentFormProps> = ({ onSuccess, onCancel
</div>
<div className="flex flex-col">
<label className="text-sm font-medium text-slate-700 mb-1">Bodily Injury (Person)</label>
<label className="text-sm font-medium text-slate-700 dark:text-avus mb-1">Bodily Injury (Person)</label>
<input
className="h-11 min-h-[44px] rounded-lg border px-3 bg-white text-gray-900 border-slate-300 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-scuro dark:text-avus dark:border-silverstone dark:placeholder-canna dark:focus:ring-abudhabi dark:focus:border-abudhabi"
type="text"
@@ -270,7 +270,7 @@ export const DocumentForm: React.FC<DocumentFormProps> = ({ onSuccess, onCancel
/>
</div>
<div className="flex flex-col">
<label className="text-sm font-medium text-slate-700 mb-1">Bodily Injury (Incident)</label>
<label className="text-sm font-medium text-slate-700 dark:text-avus mb-1">Bodily Injury (Incident)</label>
<input
className="h-11 min-h-[44px] rounded-lg border px-3 bg-white text-gray-900 border-slate-300 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-scuro dark:text-avus dark:border-silverstone dark:placeholder-canna dark:focus:ring-abudhabi dark:focus:border-abudhabi"
type="text"
@@ -281,7 +281,7 @@ export const DocumentForm: React.FC<DocumentFormProps> = ({ onSuccess, onCancel
</div>
<div className="flex flex-col">
<label className="text-sm font-medium text-slate-700 mb-1">Property Damage</label>
<label className="text-sm font-medium text-slate-700 dark:text-avus mb-1">Property Damage</label>
<input
className="h-11 min-h-[44px] rounded-lg border px-3 bg-white text-gray-900 border-slate-300 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-scuro dark:text-avus dark:border-silverstone dark:placeholder-canna dark:focus:ring-abudhabi dark:focus:border-abudhabi"
type="text"
@@ -291,7 +291,7 @@ export const DocumentForm: React.FC<DocumentFormProps> = ({ onSuccess, onCancel
/>
</div>
<div className="flex flex-col">
<label className="text-sm font-medium text-slate-700 mb-1">Premium</label>
<label className="text-sm font-medium text-slate-700 dark:text-avus mb-1">Premium</label>
<input
className="h-11 min-h-[44px] rounded-lg border px-3 bg-white text-gray-900 border-slate-300 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-scuro dark:text-avus dark:border-silverstone dark:placeholder-canna dark:focus:ring-abudhabi dark:focus:border-abudhabi"
type="number"
@@ -307,7 +307,7 @@ export const DocumentForm: React.FC<DocumentFormProps> = ({ onSuccess, onCancel
{documentType === 'registration' && (
<>
<div className="flex flex-col">
<label className="text-sm font-medium text-slate-700 mb-1">License Plate</label>
<label className="text-sm font-medium text-slate-700 dark:text-avus mb-1">License Plate</label>
<input
className="h-11 min-h-[44px] rounded-lg border px-3 bg-white text-gray-900 border-slate-300 focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-scuro dark:text-avus dark:border-silverstone dark:focus:ring-abudhabi dark:focus:border-abudhabi"
type="text"
@@ -334,7 +334,7 @@ export const DocumentForm: React.FC<DocumentFormProps> = ({ onSuccess, onCancel
/>
</div>
<div className="flex flex-col md:col-span-2">
<label className="text-sm font-medium text-slate-700 mb-1">Cost</label>
<label className="text-sm font-medium text-slate-700 dark:text-avus mb-1">Cost</label>
<input
className="h-11 min-h-[44px] rounded-lg border px-3 bg-white text-gray-900 border-slate-300 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-scuro dark:text-avus dark:border-silverstone dark:placeholder-canna dark:focus:ring-abudhabi dark:focus:border-abudhabi"
type="number"
@@ -356,16 +356,16 @@ export const DocumentForm: React.FC<DocumentFormProps> = ({ onSuccess, onCancel
onChange={(e) => setScanForMaintenance(e.target.checked)}
className="w-5 h-5 rounded border-slate-300 text-primary-600 focus:ring-primary-500 dark:border-silverstone dark:focus:ring-abudhabi"
/>
<span className="ml-2 text-sm font-medium text-slate-700">
<span className="ml-2 text-sm font-medium text-slate-700 dark:text-avus">
Scan for Maintenance Schedule
</span>
</label>
<span className="ml-2 text-xs text-slate-500">(Coming soon)</span>
<span className="ml-2 text-xs text-slate-500 dark:text-titanio">(Coming soon)</span>
</div>
)}
<div className="flex flex-col md:col-span-2">
<label className="text-sm font-medium text-slate-700 mb-1">Notes</label>
<label className="text-sm font-medium text-slate-700 dark:text-avus mb-1">Notes</label>
<textarea
className="min-h-[88px] rounded-lg border px-3 py-2 bg-white text-gray-900 border-slate-300 focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-scuro dark:text-avus dark:border-silverstone dark:focus:ring-abudhabi dark:focus:border-abudhabi"
value={notes}
@@ -374,7 +374,7 @@ export const DocumentForm: React.FC<DocumentFormProps> = ({ onSuccess, onCancel
</div>
<div className="flex flex-col md:col-span-2">
<label className="text-sm font-medium text-slate-700 mb-1">Upload image/PDF</label>
<label className="text-sm font-medium text-slate-700 dark:text-avus mb-1">Upload image/PDF</label>
<input
className="h-11 min-h-[44px] rounded-lg border px-3 py-2 bg-white text-gray-900 border-slate-300 focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-scuro dark:text-avus dark:border-silverstone dark:focus:ring-abudhabi dark:focus:border-abudhabi file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-primary-500/10 file:text-primary-600 dark:file:bg-abudhabi/20 dark:file:text-abudhabi"
type="file"
@@ -382,13 +382,13 @@ export const DocumentForm: React.FC<DocumentFormProps> = ({ onSuccess, onCancel
onChange={(e) => setFile(e.target.files?.[0] || null)}
/>
{uploadProgress > 0 && uploadProgress < 100 && (
<div className="text-sm text-slate-600 mt-1">Uploading... {uploadProgress}%</div>
<div className="text-sm text-slate-600 dark:text-titanio mt-1">Uploading... {uploadProgress}%</div>
)}
</div>
</div>
{error && (
<div className="text-red-600 text-sm mt-3">{error}</div>
<div className="text-red-600 dark:text-red-400 text-sm mt-3">{error}</div>
)}
<div className="flex flex-col sm:flex-row gap-2 mt-4">

View File

@@ -187,12 +187,13 @@ const FuelLogFormComponent: React.FC<{ onSuccess?: () => void; initial?: Partial
fullWidth
InputProps={{
readOnly: true,
sx: {
backgroundColor: 'grey.50',
sx: (theme) => ({
backgroundColor: theme.palette.mode === 'dark' ? '#4C4E4D' : 'grey.50',
'& .MuiOutlinedInput-input': {
cursor: 'default',
color: theme.palette.mode === 'dark' ? '#F2F3F6' : 'inherit',
},
},
}),
}}
helperText="Calculated from distance ÷ fuel amount"
sx={{

View File

@@ -330,10 +330,10 @@ export const StationPicker: React.FC<StationPickerProps> = ({
}}
/>
)}
sx={{
sx={(theme) => ({
'& .MuiAutocomplete-groupLabel': {
fontWeight: 600,
backgroundColor: 'grey.100',
backgroundColor: theme.palette.mode === 'dark' ? '#4C4E4D' : 'grey.100',
fontSize: '0.75rem',
textTransform: 'uppercase',
letterSpacing: '0.5px'
@@ -342,7 +342,7 @@ export const StationPicker: React.FC<StationPickerProps> = ({
minHeight: '44px', // Mobile touch target
padding: '8px 16px'
}
}}
})}
/>
);
};

View File

@@ -11,6 +11,7 @@ export interface UserPreferences {
unitSystem: UnitSystem;
currencyCode: string;
timeZone: string;
darkMode: boolean | null;
createdAt: string;
updatedAt: string;
}
@@ -19,4 +20,5 @@ export interface UpdatePreferencesRequest {
unitSystem?: UnitSystem;
currencyCode?: string;
timeZone?: string;
darkMode?: boolean | null;
}

View File

@@ -396,7 +396,7 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div className="mb-6">
<label className="block text-sm font-medium text-gray-700 mb-2">
<label className="block text-sm font-medium text-gray-700 dark:text-avus mb-2">
Vehicle Photo
</label>
<VehicleImageUpload
@@ -408,10 +408,10 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-gray-700 dark:text-avus mb-1">
VIN or License Plate <span className="text-red-500">*</span>
</label>
<p className="text-xs text-gray-600 mb-2">
<p className="text-xs text-gray-600 dark:text-titanio mb-2">
Enter vehicle VIN (optional)
</p>
<input
@@ -421,14 +421,14 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
style={{ fontSize: '16px' }}
/>
{errors.vin && (
<p className="mt-1 text-sm text-red-600">{errors.vin.message}</p>
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.vin.message}</p>
)}
</div>
{/* Vehicle Specification Dropdowns */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-gray-700 dark:text-avus mb-1">
Year
</label>
<select
@@ -451,7 +451,7 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-gray-700 dark:text-avus mb-1">
Make
</label>
<select
@@ -483,7 +483,7 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-gray-700 dark:text-avus mb-1">
Model
</label>
<select
@@ -518,7 +518,7 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
{/* Trim (left) */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-gray-700 dark:text-avus mb-1">
Trim
</label>
<select
@@ -551,7 +551,7 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
{/* Engine (middle) */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-gray-700 dark:text-avus mb-1">
Engine
</label>
<select
@@ -579,7 +579,7 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
{/* Transmission (right) */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-gray-700 dark:text-avus mb-1">
Transmission
</label>
<select
@@ -607,7 +607,7 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-gray-700 dark:text-avus mb-1">
Nickname
</label>
<input
@@ -620,7 +620,7 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-gray-700 dark:text-avus mb-1">
Color
</label>
<input
@@ -632,7 +632,7 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-gray-700 dark:text-avus mb-1">
License Plate
</label>
<input
@@ -642,13 +642,13 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
style={{ fontSize: '16px' }}
/>
{errors.licensePlate && (
<p className="mt-1 text-sm text-red-600">{errors.licensePlate.message}</p>
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.licensePlate.message}</p>
)}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-gray-700 dark:text-avus mb-1">
Current Odometer Reading
</label>
<input

View File

@@ -29,12 +29,12 @@ const DetailField: React.FC<{
className?: string;
}> = ({ label, value, isRequired, className = "" }) => (
<div className={`space-y-1 ${className}`}>
<label className="block text-sm font-medium text-gray-700">
<label className="block text-sm font-medium text-gray-700 dark:text-avus">
{label} {isRequired && <span className="text-red-500">*</span>}
</label>
<div className="px-3 py-2 bg-gray-50 border border-gray-200 rounded-md">
<span className="text-gray-900">
{value || <span className="text-gray-400 italic">Not provided</span>}
<div className="px-3 py-2 bg-gray-50 dark:bg-scuro border border-gray-200 dark:border-silverstone rounded-md">
<span className="text-gray-900 dark:text-avus">
{value || <span className="text-gray-400 dark:text-titanio italic">Not provided</span>}
</span>
</div>
</div>

View File

@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { useAuth0 } from '@auth0/auth0-react';
import { useNavigate } from 'react-router-dom';
import { HeroCarousel } from './HomePage/HeroCarousel';
@@ -8,8 +8,18 @@ import { motion } from 'framer-motion';
export const HomePage = () => {
const { loginWithRedirect, isAuthenticated } = useAuth0();
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [isScrolled, setIsScrolled] = useState(false);
const navigate = useNavigate();
useEffect(() => {
const handleScroll = () => {
setIsScrolled(window.scrollY > 100);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
const handleAuthAction = () => {
if (isAuthenticated) {
navigate('/garage');
@@ -26,8 +36,8 @@ export const HomePage = () => {
return (
<div className="min-h-screen bg-nero text-avus">
{/* Navigation Bar */}
<nav className="sticky top-0 z-50 bg-nero/90 backdrop-blur border-b border-white/10">
<div className="max-w-7xl mx-auto px-4 md:px-8">
<nav className={`fixed top-0 left-0 right-0 z-50 transition-colors duration-300 ${isScrolled ? 'bg-nero/95 backdrop-blur-sm' : 'bg-transparent'}`}>
<div className="w-full px-4 md:px-8 lg:px-12">
<div className="flex justify-between items-center h-16">
{/* Logo */}
<div className="flex-shrink-0">
@@ -99,7 +109,7 @@ export const HomePage = () => {
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="md:hidden py-4 space-y-3 bg-nero border-t border-white/10"
className="md:hidden py-4 space-y-3 bg-nero/95 backdrop-blur-sm border-t border-white/10"
>
<a
href="#home"
@@ -138,7 +148,7 @@ export const HomePage = () => {
{/* Hero Carousel */}
<section id="home">
<HeroCarousel />
<HeroCarousel onGetStarted={handleAuthAction} />
</section>
{/* Welcome Section */}

View File

@@ -1,4 +1,4 @@
import { useRef } from 'react';
import React, { useRef } from 'react';
import Slider from 'react-slick';
import 'slick-carousel/slick/slick.css';
import 'slick-carousel/slick/slick-theme.css';
@@ -22,8 +22,8 @@ const heroSlides: HeroSlide[] = [
},
{
id: 3,
imageSrc: 'https://images.unsplash.com/photo-1552519507-da3b142c6e3d?w=1920&h=1080&fit=crop',
imageAlt: 'Green Performance Car',
imageSrc: 'https://images.unsplash.com/photo-1609138315745-4e44ac3bbd8d?w=1920&h=1080&fit=crop&crop=focalpoint&fp-x=0.5&fp-y=0.6&q=80&auto=format&ixlib=rb-4.1.0',
imageAlt: 'Ferrari SF90',
},
{
id: 4,
@@ -32,8 +32,8 @@ const heroSlides: HeroSlide[] = [
},
{
id: 5,
imageSrc: 'https://images.unsplash.com/photo-1549317661-bd32c8ce0db2?w=1920&h=1080&fit=crop',
imageAlt: 'SUV on Road',
imageSrc: 'https://images.unsplash.com/photo-1740095960937-c7b03f511da4?w=1920&h=1080&fit=crop&crop=focalpoint&fp-x=0.5&fp-y=0.6&q=80&auto=format&ixlib=rb-4.1.0',
imageAlt: 'Corvette Z06 - Black Rose',
},
{
id: 6,
@@ -42,7 +42,11 @@ const heroSlides: HeroSlide[] = [
},
];
export const HeroCarousel = () => {
interface HeroCarouselProps {
onGetStarted: () => void;
}
export const HeroCarousel: React.FC<HeroCarouselProps> = ({ onGetStarted }) => {
const sliderRef = useRef<Slider>(null);
const settings = {
@@ -76,6 +80,19 @@ export const HeroCarousel = () => {
))}
</Slider>
{/* Hero Text Overlay - positioned outside Slider to prevent fading */}
<div className="absolute inset-0 flex flex-col items-center justify-center text-center px-4 pointer-events-none">
<h1 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold text-white mb-6 md:mb-8 drop-shadow-lg">
Track every mile. Own every detail.
</h1>
<button
onClick={onGetStarted}
className="pointer-events-auto bg-primary-500 hover:bg-primary-600 text-white font-semibold py-3 px-8 md:py-4 md:px-10 rounded-lg text-lg md:text-xl transition-colors duration-300 shadow-lg shadow-black/30 focus:outline-none focus:ring-2 focus:ring-primary-500/50"
>
Get Started
</button>
</div>
<style>{`
.hero-carousel .slick-dots {
bottom: 25px;

View File

@@ -1,10 +1,10 @@
/**
* @ai-summary Theme context for managing light/dark mode across the app
* Uses same localStorage key as useSettings for consistency
* Supports: system preference detection, localStorage persistence, backend sync
* Applies Tailwind dark class to document root
*/
import { createContext, useContext, useEffect, useMemo, useState, useCallback, ReactNode } from 'react';
import { createContext, useContext, useEffect, useMemo, useState, useCallback, ReactNode, useRef } from 'react';
import { ThemeProvider as MuiThemeProvider, Theme } from '@mui/material/styles';
import { CssBaseline } from '@mui/material';
import { md3LightTheme, md3DarkTheme } from './md3Theme';
@@ -15,8 +15,10 @@ const SETTINGS_STORAGE_KEY = 'motovaultpro-mobile-settings';
interface ThemeContextValue {
isDarkMode: boolean;
toggleDarkMode: () => void;
setDarkMode: (value: boolean) => void;
setDarkMode: (value: boolean, syncToBackend?: boolean) => void;
theme: Theme;
// Callback to register backend sync function
registerBackendSync: (syncFn: (darkMode: boolean) => void) => void;
}
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
@@ -25,13 +27,21 @@ interface ThemeProviderProps {
children: ReactNode;
}
// Read dark mode preference from localStorage (synced with useSettings)
const getStoredDarkMode = (): boolean => {
// Detect system dark mode preference
const getSystemPreference = (): boolean => {
if (typeof window !== 'undefined' && window.matchMedia) {
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
return false;
};
// Check if user has explicitly set a preference in localStorage
const hasStoredPreference = (): boolean => {
try {
const stored = localStorage.getItem(SETTINGS_STORAGE_KEY);
if (stored) {
const settings = JSON.parse(stored);
return settings.darkMode ?? false;
return settings.darkMode !== undefined && settings.darkMode !== null;
}
} catch {
// Ignore parse errors
@@ -39,6 +49,31 @@ const getStoredDarkMode = (): boolean => {
return false;
};
// Read dark mode preference from localStorage
const getStoredDarkMode = (): boolean | undefined => {
try {
const stored = localStorage.getItem(SETTINGS_STORAGE_KEY);
if (stored) {
const settings = JSON.parse(stored);
if (settings.darkMode !== undefined && settings.darkMode !== null) {
return settings.darkMode;
}
}
} catch {
// Ignore parse errors
}
return undefined;
};
// Get initial dark mode: localStorage > system preference
const getInitialDarkMode = (): boolean => {
const stored = getStoredDarkMode();
if (stored !== undefined) {
return stored;
}
return getSystemPreference();
};
// Update dark mode in localStorage while preserving other settings
const setStoredDarkMode = (darkMode: boolean): void => {
try {
@@ -52,15 +87,33 @@ const setStoredDarkMode = (darkMode: boolean): void => {
};
export const ThemeProvider = ({ children }: ThemeProviderProps) => {
const [isDarkMode, setIsDarkMode] = useState<boolean>(getStoredDarkMode);
const [isDarkMode, setIsDarkMode] = useState<boolean>(getInitialDarkMode);
const [isInitialized, setIsInitialized] = useState(false);
const backendSyncRef = useRef<((darkMode: boolean) => void) | null>(null);
// Initialize on mount
useEffect(() => {
setIsDarkMode(getStoredDarkMode());
setIsDarkMode(getInitialDarkMode());
setIsInitialized(true);
}, []);
// Listen for system preference changes (only when no explicit preference set)
useEffect(() => {
if (typeof window === 'undefined' || !window.matchMedia) return;
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = (e: MediaQueryListEvent) => {
// Only react to system changes if user hasn't set explicit preference
if (!hasStoredPreference()) {
setIsDarkMode(e.matches);
}
};
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
}, []);
// Apply dark class to document root for Tailwind
useEffect(() => {
const root = document.documentElement;
@@ -90,13 +143,23 @@ export const ThemeProvider = ({ children }: ThemeProviderProps) => {
return () => window.removeEventListener('storage', handleStorageChange);
}, []);
const setDarkMode = useCallback((value: boolean) => {
// Register backend sync function (called by useThemeSync hook)
const registerBackendSync = useCallback((syncFn: (darkMode: boolean) => void) => {
backendSyncRef.current = syncFn;
}, []);
const setDarkMode = useCallback((value: boolean, syncToBackend = true) => {
setIsDarkMode(value);
setStoredDarkMode(value);
// Sync to backend if registered and requested
if (syncToBackend && backendSyncRef.current) {
backendSyncRef.current(value);
}
}, []);
const toggleDarkMode = useCallback(() => {
setDarkMode(!isDarkMode);
setDarkMode(!isDarkMode, true);
}, [isDarkMode, setDarkMode]);
const theme = useMemo(() => {
@@ -109,8 +172,9 @@ export const ThemeProvider = ({ children }: ThemeProviderProps) => {
toggleDarkMode,
setDarkMode,
theme,
registerBackendSync,
}),
[isDarkMode, toggleDarkMode, setDarkMode, theme]
[isDarkMode, toggleDarkMode, setDarkMode, theme, registerBackendSync]
);
// Prevent flash of wrong theme during SSR/initial load

View File

@@ -0,0 +1,66 @@
/**
* @ai-summary Hook to sync dark mode preference with backend
* Load from backend on auth, sync changes to backend on toggle
*/
import { useEffect, useRef } from 'react';
import { useAuth0 } from '@auth0/auth0-react';
import { usePreferences, useUpdatePreferences } from '../../features/settings/hooks/usePreferences';
import { useTheme } from './ThemeContext';
/**
* Syncs theme preference with backend.
* Place this hook in an authenticated component (e.g., Layout or App authenticated section).
*
* Behavior:
* - On auth: loads darkMode from backend and updates ThemeContext
* - On toggle: syncs new darkMode value to backend
*/
export const useThemeSync = () => {
const { isAuthenticated } = useAuth0();
const { data: preferences, isLoading } = usePreferences();
const updatePreferences = useUpdatePreferences();
const { setDarkMode, registerBackendSync, isDarkMode } = useTheme();
const hasLoadedFromBackend = useRef(false);
// Register backend sync function
useEffect(() => {
const syncToBackend = (darkMode: boolean) => {
if (isAuthenticated) {
updatePreferences.mutate({ darkMode });
}
};
registerBackendSync(syncToBackend);
}, [isAuthenticated, updatePreferences, registerBackendSync]);
// Load from backend on initial auth (only once)
useEffect(() => {
if (
isAuthenticated &&
!isLoading &&
preferences &&
!hasLoadedFromBackend.current
) {
hasLoadedFromBackend.current = true;
// Only update if backend has an explicit preference
if (preferences.darkMode !== null && preferences.darkMode !== undefined) {
// Update local state without syncing back to backend
setDarkMode(preferences.darkMode, false);
}
}
}, [isAuthenticated, isLoading, preferences, setDarkMode]);
// Reset flag when user logs out
useEffect(() => {
if (!isAuthenticated) {
hasLoadedFromBackend.current = false;
}
}, [isAuthenticated]);
return {
isDarkMode,
isLoading: isLoading && isAuthenticated,
};
};