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

@@ -11,7 +11,7 @@ export class UserPreferencesRepository {
async findByUserId(userId: string): Promise<UserPreferences | null> { async findByUserId(userId: string): Promise<UserPreferences | null> {
const query = ` const query = `
SELECT id, user_id, unit_system, currency_code, time_zone, created_at, updated_at SELECT id, user_id, unit_system, currency_code, time_zone, dark_mode, created_at, updated_at
FROM user_preferences FROM user_preferences
WHERE user_id = $1 WHERE user_id = $1
`; `;
@@ -22,8 +22,8 @@ export class UserPreferencesRepository {
async create(data: CreateUserPreferencesRequest): Promise<UserPreferences> { async create(data: CreateUserPreferencesRequest): Promise<UserPreferences> {
const query = ` const query = `
INSERT INTO user_preferences (user_id, unit_system, currency_code, time_zone) INSERT INTO user_preferences (user_id, unit_system, currency_code, time_zone, dark_mode)
VALUES ($1, $2, $3, $4) VALUES ($1, $2, $3, $4, $5)
RETURNING * RETURNING *
`; `;
@@ -31,7 +31,8 @@ export class UserPreferencesRepository {
data.userId, data.userId,
data.unitSystem || 'imperial', data.unitSystem || 'imperial',
data.currencyCode || 'USD', data.currencyCode || 'USD',
data.timeZone || 'UTC' data.timeZone || 'UTC',
data.darkMode ?? null
]; ];
const result = await this.db.query(query, values); const result = await this.db.query(query, values);
@@ -55,6 +56,10 @@ export class UserPreferencesRepository {
fields.push(`time_zone = $${paramCount++}`); fields.push(`time_zone = $${paramCount++}`);
values.push(data.timeZone); values.push(data.timeZone);
} }
if (data.darkMode !== undefined) {
fields.push(`dark_mode = $${paramCount++}`);
values.push(data.darkMode);
}
if (fields.length === 0) { if (fields.length === 0) {
return this.findByUserId(userId); return this.findByUserId(userId);
@@ -90,6 +95,7 @@ export class UserPreferencesRepository {
unitSystem: row.unit_system, unitSystem: row.unit_system,
currencyCode: row.currency_code || 'USD', currencyCode: row.currency_code || 'USD',
timeZone: row.time_zone || 'UTC', timeZone: row.time_zone || 'UTC',
darkMode: row.dark_mode,
createdAt: row.created_at, createdAt: row.created_at,
updatedAt: row.updated_at, updatedAt: row.updated_at,
}; };

View File

@@ -0,0 +1,8 @@
-- Migration: Add dark_mode column to user_preferences
-- NULL = use system preference, TRUE = dark mode, FALSE = light mode
ALTER TABLE user_preferences
ADD COLUMN IF NOT EXISTS dark_mode BOOLEAN DEFAULT NULL;
-- Add comment for documentation
COMMENT ON COLUMN user_preferences.dark_mode IS 'User dark mode preference. NULL means use system preference.';

View File

@@ -11,6 +11,7 @@ export interface UserPreferences {
unitSystem: UnitSystem; unitSystem: UnitSystem;
currencyCode: string; currencyCode: string;
timeZone: string; timeZone: string;
darkMode: boolean | null;
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
} }
@@ -20,12 +21,14 @@ export interface CreateUserPreferencesRequest {
unitSystem?: UnitSystem; unitSystem?: UnitSystem;
currencyCode?: string; currencyCode?: string;
timeZone?: string; timeZone?: string;
darkMode?: boolean | null;
} }
export interface UpdateUserPreferencesRequest { export interface UpdateUserPreferencesRequest {
unitSystem?: UnitSystem; unitSystem?: UnitSystem;
currencyCode?: string; currencyCode?: string;
timeZone?: string; timeZone?: string;
darkMode?: boolean | null;
} }
export interface UserPreferencesResponse { export interface UserPreferencesResponse {
@@ -34,6 +37,7 @@ export interface UserPreferencesResponse {
unitSystem: UnitSystem; unitSystem: UnitSystem;
currencyCode: string; currencyCode: string;
timeZone: string; timeZone: string;
darkMode: boolean | null;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
} }

View File

@@ -37,6 +37,7 @@ export class UserPreferencesController {
unitSystem: preferences.unitSystem, unitSystem: preferences.unitSystem,
currencyCode: preferences.currencyCode, currencyCode: preferences.currencyCode,
timeZone: preferences.timeZone, timeZone: preferences.timeZone,
darkMode: preferences.darkMode,
createdAt: preferences.createdAt, createdAt: preferences.createdAt,
updatedAt: preferences.updatedAt, updatedAt: preferences.updatedAt,
}); });
@@ -55,7 +56,7 @@ export class UserPreferencesController {
) { ) {
try { try {
const userId = (request as any).user.sub; const userId = (request as any).user.sub;
const { unitSystem, currencyCode, timeZone } = request.body; const { unitSystem, currencyCode, timeZone, darkMode } = request.body;
// Validate unitSystem if provided // Validate unitSystem if provided
if (unitSystem && !['imperial', 'metric'].includes(unitSystem)) { if (unitSystem && !['imperial', 'metric'].includes(unitSystem)) {
@@ -73,6 +74,14 @@ export class UserPreferencesController {
}); });
} }
// Validate darkMode if provided (must be boolean or null)
if (darkMode !== undefined && darkMode !== null && typeof darkMode !== 'boolean') {
return reply.code(400).send({
error: 'Bad Request',
message: 'darkMode must be a boolean or null',
});
}
// Check if preferences exist, create if not // Check if preferences exist, create if not
let preferences = await this.repository.findByUserId(userId); let preferences = await this.repository.findByUserId(userId);
if (!preferences) { if (!preferences) {
@@ -81,12 +90,14 @@ export class UserPreferencesController {
unitSystem: unitSystem || 'imperial', unitSystem: unitSystem || 'imperial',
currencyCode: currencyCode || 'USD', currencyCode: currencyCode || 'USD',
timeZone: timeZone || 'UTC', timeZone: timeZone || 'UTC',
darkMode: darkMode,
}); });
} else { } else {
const updated = await this.repository.update(userId, { const updated = await this.repository.update(userId, {
unitSystem, unitSystem,
currencyCode, currencyCode,
timeZone, timeZone,
darkMode,
}); });
if (updated) { if (updated) {
preferences = updated; preferences = updated;
@@ -99,6 +110,7 @@ export class UserPreferencesController {
unitSystem: preferences.unitSystem, unitSystem: preferences.unitSystem,
currencyCode: preferences.currencyCode, currencyCode: preferences.currencyCode,
timeZone: preferences.timeZone, timeZone: preferences.timeZone,
darkMode: preferences.darkMode,
createdAt: preferences.createdAt, createdAt: preferences.createdAt,
updatedAt: preferences.updatedAt, updatedAt: preferences.updatedAt,
}); });

View File

@@ -22,18 +22,15 @@ You are a senior software engineer specializsing in NodeJS, Typescript, front en
- Make no assumptions. - Make no assumptions.
- Ask clarifying questions. - Ask clarifying questions.
- Ultrathink - Ultrathink
- You will be extending the "Documents" feature to include manuals. - You will be auditing the Dark vs Light theme implementation
*** CONTEXT *** *** CONTEXT ***
- This is a modern web app for managing a vehicle fleet. It has both a desktop and mobile versions of the site that both need to maintain feature parity. It's currently deployed via docker compose but in the future will be deployed via k8s. - This is a modern web app for managing a vehicle fleet. It has both a desktop and mobile versions of the site that both need to maintain feature parity. It's currently deployed via docker compose but in the future will be deployed via k8s.
- Read README.md CLAUDE.md and AI-INDEX.md and follow relevant instructions to understand this code repository in the context of this change. - Read README.md CLAUDE.md and AI-INDEX.md and follow relevant instructions to understand this code repository in the context of this change.
- You need to extend the Documents feature to include a third "Document Type" - You need to audit the Dark vs Light theme.
- Right now the document has two types. Insurance and Registration - The colors were not all changed so some of the dark theme
- The third type will be called "Manual" - The dark versus light theme does not save between logins.
- This document will just have the uploaded file and a notes field and Title field - Think hard about the color choices and if any better colors are available form the MVP-COLOR-SCHEME.md
- When implementing this we need to play for the future feature of scanning the document for maintenance schedules
- Add a toggle for this scanning. Label it "Scan for Maintenance Schedule"
- Do not implement this feature at this time but put the toggle in the interface and the backend changes to facility this workflow.
*** CHANGES TO IMPLEMENT *** *** CHANGES TO IMPLEMENT ***
- Research this code base and ask iterative questions to compile a complete plan. - Research this code base and ask iterative questions to compile a complete plan.
@@ -58,30 +55,7 @@ You are a senior software engineer specializsing in NodeJS, Typescript, front en
- Debug what could be causing this issue. No changes were made to the Gitlab server besides adding the RESEND variable so there shouldn't be anything on the server causing this issue. - Debug what could be causing this issue. No changes were made to the Gitlab server besides adding the RESEND variable so there shouldn't be anything on the server causing this issue.
$ chmod +x scripts/inject-secrets.sh $ chmod +x scripts/inject-secrets.sh
$ ./scripts/inject-secrets.sh $ ./scripts/inject-secrets.sh
Injecting secrets...
Deploy path: /opt/gitlab-runner/builds/motovaultpro
Secrets dir: /opt/gitlab-runner/builds/motovaultpro/secrets/app
Cleaning up any corrupted secret paths...
ERROR: Variable POSTGRES_PASSWORD is not set
Ensure it exists in GitLab CI/CD Variables
ERROR: Variable AUTH0_CLIENT_SECRET is not set
Ensure it exists in GitLab CI/CD Variables
ERROR: Variable AUTH0_MANAGEMENT_CLIENT_ID is not set
Ensure it exists in GitLab CI/CD Variables
ERROR: Variable AUTH0_MANAGEMENT_CLIENT_SECRET is not set
Ensure it exists in GitLab CI/CD Variables
ERROR: Variable GOOGLE_MAPS_API_KEY is not set
Ensure it exists in GitLab CI/CD Variables
ERROR: Variable GOOGLE_MAPS_MAP_ID is not set
Ensure it exists in GitLab CI/CD Variables
ERROR: Variable CF_DNS_API_TOKEN is not set
Ensure it exists in GitLab CI/CD Variables
ERROR: Variable RESEND_API_KEY is not set
Ensure it exists in GitLab CI/CD Variables
ERROR: One or more secrets failed to inject
Ensure all required CI/CD variables are configured as File type in GitLab
Running after_script
00:00
Running after script... Running after script...
@@ -89,6 +63,35 @@ Running after script...
*** ROLE ***
- You are a senior DBA with expert knowledge in Postgres SQL.
*** ACTION ***
- Make no assumptions.
- Ask clarifying questions.
- Ultrathink
- You will be implementing an ETL process that takes a export of the NHTSA vPIC database in Postgres and transforming it for use in this application.
*** CONTEXT ***
- This is a modern web app for managing a vehicle fleet. It has both a desktop and mobile versions of the site that both need to maintain feature parity. It's currently deployed via docker compose but in the future will be deployed via k8s.
- Read README.md CLAUDE.md and AI-INDEX.md and follow relevant instructions to understand this code repository in the context of this change.
- There is an existing database import process in this directory. This process works and should not be changed.
- The source database from the NHTSA vPIC dataset is located in the @vpic-source directory
- Deep research needs to be conducted on how to execute this ETL process.
- The source database is designed for VIN decoding only.
- Example VIN: 2025 Honda Civic Hybrid - 2HGFE4F88SH315466
- Example VIN: 2023 GMC Sierra 1500 AT4x - 3GTUUFEL6PG140748
- Example VIN: 2017 Chevrolet Corvette Z06 - 1G1YU3D64H5602799
*** CHANGES TO IMPLEMENT ***
- Research this code base and ask iterative questions to compile a complete plan.
- generate a project plan
- break into bite-sized tasks and milestones
*** ROLE *** *** ROLE ***
- You are a senior DBA with expert knowledge in Postgres SQL. - You are a senior DBA with expert knowledge in Postgres SQL.

View File

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

View File

@@ -49,38 +49,38 @@ export const SignupForm: React.FC<SignupFormProps> = ({ onSubmit, loading }) =>
return ( return (
<form onSubmit={handleSubmit(handleFormSubmit)} className="space-y-4"> <form onSubmit={handleSubmit(handleFormSubmit)} className="space-y-4">
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-avus mb-1">
Email Address <span className="text-red-500">*</span> Email Address <span className="text-red-400">*</span>
</label> </label>
<input <input
{...register('email')} {...register('email')}
type="email" type="email"
inputMode="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" placeholder="your.email@example.com"
style={{ fontSize: '16px' }} style={{ fontSize: '16px' }}
/> />
{errors.email && ( {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>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-avus mb-1">
Password <span className="text-red-500">*</span> Password <span className="text-red-400">*</span>
</label> </label>
<div className="relative"> <div className="relative">
<input <input
{...register('password')} {...register('password')}
type={showPassword ? 'text' : '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" placeholder="At least 8 characters"
style={{ fontSize: '16px' }} style={{ fontSize: '16px' }}
/> />
<button <button
type="button" type="button"
onClick={() => setShowPassword(!showPassword)} 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'} aria-label={showPassword ? 'Hide password' : 'Show password'}
> >
{showPassword ? ( {showPassword ? (
@@ -96,29 +96,29 @@ export const SignupForm: React.FC<SignupFormProps> = ({ onSubmit, loading }) =>
</button> </button>
</div> </div>
{errors.password && ( {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 Must be at least 8 characters with one uppercase letter and one number
</p> </p>
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-avus mb-1">
Confirm Password <span className="text-red-500">*</span> Confirm Password <span className="text-red-400">*</span>
</label> </label>
<div className="relative"> <div className="relative">
<input <input
{...register('confirmPassword')} {...register('confirmPassword')}
type={showConfirmPassword ? 'text' : 'password'} 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" placeholder="Re-enter your password"
style={{ fontSize: '16px' }} style={{ fontSize: '16px' }}
/> />
<button <button
type="button" type="button"
onClick={() => setShowConfirmPassword(!showConfirmPassword)} 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'} aria-label={showConfirmPassword ? 'Hide password' : 'Show password'}
> >
{showConfirmPassword ? ( {showConfirmPassword ? (
@@ -134,7 +134,7 @@ export const SignupForm: React.FC<SignupFormProps> = ({ onSubmit, loading }) =>
</button> </button>
</div> </div>
{errors.confirmPassword && ( {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> </div>

View File

@@ -25,24 +25,28 @@ export const SignupPage: React.FC = () => {
}; };
return ( 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="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"> <div className="text-center mb-8">
<h1 className="text-3xl font-bold text-primary-600 mb-2">MotoVaultPro</h1> <img
<h2 className="text-xl font-semibold text-gray-800">Create Your Account</h2> src="/images/logos/motovaultpro-title-slogan.png"
<p className="text-sm text-gray-600 mt-2"> 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 Start tracking your vehicle maintenance and fuel logs
</p> </p>
</div> </div>
<SignupForm onSubmit={handleSubmit} loading={isPending} /> <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?{' '} Already have an account?{' '}
<button <button
onClick={() => navigate('/login')} 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 Login
</button> </button>

View File

@@ -158,7 +158,7 @@ export const DocumentForm: React.FC<DocumentFormProps> = ({ onSuccess, onCancel
<form onSubmit={handleSubmit} className="w-full"> <form onSubmit={handleSubmit} className="w-full">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex flex-col"> <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 <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" 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} value={vehicleID}
@@ -173,7 +173,7 @@ export const DocumentForm: React.FC<DocumentFormProps> = ({ onSuccess, onCancel
</div> </div>
<div className="flex flex-col"> <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 <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" 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} value={documentType}
@@ -186,7 +186,7 @@ export const DocumentForm: React.FC<DocumentFormProps> = ({ onSuccess, onCancel
</div> </div>
<div className="flex flex-col md:col-span-2"> <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 <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" 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" type="text"
@@ -204,7 +204,7 @@ export const DocumentForm: React.FC<DocumentFormProps> = ({ onSuccess, onCancel
{documentType === 'insurance' && ( {documentType === 'insurance' && (
<> <>
<div className="flex flex-col"> <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 <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" 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" type="text"
@@ -213,7 +213,7 @@ export const DocumentForm: React.FC<DocumentFormProps> = ({ onSuccess, onCancel
/> />
</div> </div>
<div className="flex flex-col"> <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 <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" 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" type="text"
@@ -260,7 +260,7 @@ export const DocumentForm: React.FC<DocumentFormProps> = ({ onSuccess, onCancel
</div> </div>
<div className="flex flex-col"> <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 <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" 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" type="text"
@@ -270,7 +270,7 @@ export const DocumentForm: React.FC<DocumentFormProps> = ({ onSuccess, onCancel
/> />
</div> </div>
<div className="flex flex-col"> <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 <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" 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" type="text"
@@ -281,7 +281,7 @@ export const DocumentForm: React.FC<DocumentFormProps> = ({ onSuccess, onCancel
</div> </div>
<div className="flex flex-col"> <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 <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" 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" type="text"
@@ -291,7 +291,7 @@ export const DocumentForm: React.FC<DocumentFormProps> = ({ onSuccess, onCancel
/> />
</div> </div>
<div className="flex flex-col"> <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 <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" 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" type="number"
@@ -307,7 +307,7 @@ export const DocumentForm: React.FC<DocumentFormProps> = ({ onSuccess, onCancel
{documentType === 'registration' && ( {documentType === 'registration' && (
<> <>
<div className="flex flex-col"> <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 <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" 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" type="text"
@@ -334,7 +334,7 @@ export const DocumentForm: React.FC<DocumentFormProps> = ({ onSuccess, onCancel
/> />
</div> </div>
<div className="flex flex-col md:col-span-2"> <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 <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" 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" type="number"
@@ -356,16 +356,16 @@ export const DocumentForm: React.FC<DocumentFormProps> = ({ onSuccess, onCancel
onChange={(e) => setScanForMaintenance(e.target.checked)} 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" 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 Scan for Maintenance Schedule
</span> </span>
</label> </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>
)} )}
<div className="flex flex-col md:col-span-2"> <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 <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" 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} value={notes}
@@ -374,7 +374,7 @@ export const DocumentForm: React.FC<DocumentFormProps> = ({ onSuccess, onCancel
</div> </div>
<div className="flex flex-col md:col-span-2"> <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 <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" 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" type="file"
@@ -382,13 +382,13 @@ export const DocumentForm: React.FC<DocumentFormProps> = ({ onSuccess, onCancel
onChange={(e) => setFile(e.target.files?.[0] || null)} onChange={(e) => setFile(e.target.files?.[0] || null)}
/> />
{uploadProgress > 0 && uploadProgress < 100 && ( {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>
</div> </div>
{error && ( {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"> <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 fullWidth
InputProps={{ InputProps={{
readOnly: true, readOnly: true,
sx: { sx: (theme) => ({
backgroundColor: 'grey.50', backgroundColor: theme.palette.mode === 'dark' ? '#4C4E4D' : 'grey.50',
'& .MuiOutlinedInput-input': { '& .MuiOutlinedInput-input': {
cursor: 'default', cursor: 'default',
color: theme.palette.mode === 'dark' ? '#F2F3F6' : 'inherit',
}, },
}, }),
}} }}
helperText="Calculated from distance ÷ fuel amount" helperText="Calculated from distance ÷ fuel amount"
sx={{ sx={{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import { useRef } from 'react'; import React, { useRef } from 'react';
import Slider from 'react-slick'; import Slider from 'react-slick';
import 'slick-carousel/slick/slick.css'; import 'slick-carousel/slick/slick.css';
import 'slick-carousel/slick/slick-theme.css'; import 'slick-carousel/slick/slick-theme.css';
@@ -22,8 +22,8 @@ const heroSlides: HeroSlide[] = [
}, },
{ {
id: 3, id: 3,
imageSrc: 'https://images.unsplash.com/photo-1552519507-da3b142c6e3d?w=1920&h=1080&fit=crop', 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: 'Green Performance Car', imageAlt: 'Ferrari SF90',
}, },
{ {
id: 4, id: 4,
@@ -32,8 +32,8 @@ const heroSlides: HeroSlide[] = [
}, },
{ {
id: 5, id: 5,
imageSrc: 'https://images.unsplash.com/photo-1549317661-bd32c8ce0db2?w=1920&h=1080&fit=crop', 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: 'SUV on Road', imageAlt: 'Corvette Z06 - Black Rose',
}, },
{ {
id: 6, 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 sliderRef = useRef<Slider>(null);
const settings = { const settings = {
@@ -76,6 +80,19 @@ export const HeroCarousel = () => {
))} ))}
</Slider> </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>{` <style>{`
.hero-carousel .slick-dots { .hero-carousel .slick-dots {
bottom: 25px; bottom: 25px;

View File

@@ -1,10 +1,10 @@
/** /**
* @ai-summary Theme context for managing light/dark mode across the app * @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 * 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 { ThemeProvider as MuiThemeProvider, Theme } from '@mui/material/styles';
import { CssBaseline } from '@mui/material'; import { CssBaseline } from '@mui/material';
import { md3LightTheme, md3DarkTheme } from './md3Theme'; import { md3LightTheme, md3DarkTheme } from './md3Theme';
@@ -15,8 +15,10 @@ const SETTINGS_STORAGE_KEY = 'motovaultpro-mobile-settings';
interface ThemeContextValue { interface ThemeContextValue {
isDarkMode: boolean; isDarkMode: boolean;
toggleDarkMode: () => void; toggleDarkMode: () => void;
setDarkMode: (value: boolean) => void; setDarkMode: (value: boolean, syncToBackend?: boolean) => void;
theme: Theme; theme: Theme;
// Callback to register backend sync function
registerBackendSync: (syncFn: (darkMode: boolean) => void) => void;
} }
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined); const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
@@ -25,13 +27,21 @@ interface ThemeProviderProps {
children: ReactNode; children: ReactNode;
} }
// Read dark mode preference from localStorage (synced with useSettings) // Detect system dark mode preference
const getStoredDarkMode = (): boolean => { 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 { try {
const stored = localStorage.getItem(SETTINGS_STORAGE_KEY); const stored = localStorage.getItem(SETTINGS_STORAGE_KEY);
if (stored) { if (stored) {
const settings = JSON.parse(stored); const settings = JSON.parse(stored);
return settings.darkMode ?? false; return settings.darkMode !== undefined && settings.darkMode !== null;
} }
} catch { } catch {
// Ignore parse errors // Ignore parse errors
@@ -39,6 +49,31 @@ const getStoredDarkMode = (): boolean => {
return false; 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 // Update dark mode in localStorage while preserving other settings
const setStoredDarkMode = (darkMode: boolean): void => { const setStoredDarkMode = (darkMode: boolean): void => {
try { try {
@@ -52,15 +87,33 @@ const setStoredDarkMode = (darkMode: boolean): void => {
}; };
export const ThemeProvider = ({ children }: ThemeProviderProps) => { export const ThemeProvider = ({ children }: ThemeProviderProps) => {
const [isDarkMode, setIsDarkMode] = useState<boolean>(getStoredDarkMode); const [isDarkMode, setIsDarkMode] = useState<boolean>(getInitialDarkMode);
const [isInitialized, setIsInitialized] = useState(false); const [isInitialized, setIsInitialized] = useState(false);
const backendSyncRef = useRef<((darkMode: boolean) => void) | null>(null);
// Initialize on mount // Initialize on mount
useEffect(() => { useEffect(() => {
setIsDarkMode(getStoredDarkMode()); setIsDarkMode(getInitialDarkMode());
setIsInitialized(true); 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 // Apply dark class to document root for Tailwind
useEffect(() => { useEffect(() => {
const root = document.documentElement; const root = document.documentElement;
@@ -90,13 +143,23 @@ export const ThemeProvider = ({ children }: ThemeProviderProps) => {
return () => window.removeEventListener('storage', handleStorageChange); 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); setIsDarkMode(value);
setStoredDarkMode(value); setStoredDarkMode(value);
// Sync to backend if registered and requested
if (syncToBackend && backendSyncRef.current) {
backendSyncRef.current(value);
}
}, []); }, []);
const toggleDarkMode = useCallback(() => { const toggleDarkMode = useCallback(() => {
setDarkMode(!isDarkMode); setDarkMode(!isDarkMode, true);
}, [isDarkMode, setDarkMode]); }, [isDarkMode, setDarkMode]);
const theme = useMemo(() => { const theme = useMemo(() => {
@@ -109,8 +172,9 @@ export const ThemeProvider = ({ children }: ThemeProviderProps) => {
toggleDarkMode, toggleDarkMode,
setDarkMode, setDarkMode,
theme, theme,
registerBackendSync,
}), }),
[isDarkMode, toggleDarkMode, setDarkMode, theme] [isDarkMode, toggleDarkMode, setDarkMode, theme, registerBackendSync]
); );
// Prevent flash of wrong theme during SSR/initial load // 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,
};
};

Binary file not shown.