feat: navigation and UX improvements complete
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}}
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
66
frontend/src/shared-minimal/theme/useThemeSync.ts
Normal file
66
frontend/src/shared-minimal/theme/useThemeSync.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user