Files
motovaultpro/docs/changes/mobile-optimization-v1/06-CODE-EXAMPLES.md
Eric Gullickson a052040e3a Initial Commit
2025-09-17 16:09:15 -05:00

1341 lines
40 KiB
Markdown

# Code Examples & Implementation Snippets
## Overview
This document provides concrete code examples and implementation snippets for all mobile optimization improvements. These examples can be directly used during implementation with minimal modifications.
## Mobile Settings Implementation
### Complete Mobile Settings Screen
**File**: `frontend/src/features/settings/mobile/MobileSettingsScreen.tsx`
```tsx
import React, { useState } from 'react';
import { useAuth0 } from '@auth0/auth0-react';
import {
GlassCard,
MobileContainer,
MobilePill
} from '../../../shared-minimal/components/mobile';
import { useSettings } from '../hooks/useSettings';
interface ToggleSwitchProps {
enabled: boolean;
onChange: () => void;
label: string;
description?: string;
}
const ToggleSwitch: React.FC<ToggleSwitchProps> = ({
enabled,
onChange,
label,
description
}) => (
<div className="flex items-center justify-between py-2">
<div>
<p className="font-medium text-slate-800">{label}</p>
{description && (
<p className="text-sm text-slate-500">{description}</p>
)}
</div>
<button
onClick={onChange}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
enabled ? 'bg-blue-600' : 'bg-gray-200'
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
enabled ? 'translate-x-6' : 'translate-x-1'
}`}
/>
</button>
</div>
);
interface ModalProps {
isOpen: boolean;
onClose: () => void;
title: string;
children: React.ReactNode;
}
const Modal: React.FC<ModalProps> = ({ isOpen, onClose, title, children }) => {
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg p-6 max-w-sm w-full">
<h3 className="text-lg font-semibold text-slate-800 mb-4">{title}</h3>
{children}
<div className="flex justify-end mt-4">
<button
onClick={onClose}
className="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg font-medium"
>
Close
</button>
</div>
</div>
</div>
);
};
export const MobileSettingsScreen: React.FC = () => {
const { user, logout } = useAuth0();
const { settings, updateSetting } = useSettings();
const [showDataExport, setShowDataExport] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const handleLogout = () => {
logout({
logoutParams: {
returnTo: window.location.origin
}
});
};
const handleExportData = () => {
// Implement data export functionality
console.log('Exporting user data...');
setShowDataExport(false);
};
const handleDeleteAccount = () => {
// Implement account deletion
console.log('Deleting account...');
setShowDeleteConfirm(false);
};
return (
<MobileContainer>
<div className="space-y-4 pb-20">
{/* Header */}
<div className="text-center mb-6">
<h1 className="text-2xl font-bold text-slate-800">Settings</h1>
<p className="text-slate-500 mt-2">Manage your account and preferences</p>
</div>
{/* Account Section */}
<GlassCard>
<div className="p-4">
<h2 className="text-lg font-semibold text-slate-800 mb-4">Account</h2>
<div className="flex items-center space-x-3">
{user?.picture && (
<img
src={user.picture}
alt="Profile"
className="w-12 h-12 rounded-full"
/>
)}
<div>
<p className="font-medium text-slate-800">{user?.name}</p>
<p className="text-sm text-slate-500">{user?.email}</p>
</div>
</div>
<div className="pt-3 mt-3 border-t border-slate-200">
<p className="text-sm text-slate-600">
Member since {user?.updated_at ? new Date(user.updated_at).toLocaleDateString() : 'Unknown'}
</p>
</div>
</div>
</GlassCard>
{/* Notifications Section */}
<GlassCard>
<div className="p-4">
<h2 className="text-lg font-semibold text-slate-800 mb-4">Notifications</h2>
<div className="space-y-3">
<ToggleSwitch
enabled={settings.notifications.email}
onChange={() => updateSetting('notifications', {
...settings.notifications,
email: !settings.notifications.email
})}
label="Email Notifications"
description="Receive updates via email"
/>
<ToggleSwitch
enabled={settings.notifications.push}
onChange={() => updateSetting('notifications', {
...settings.notifications,
push: !settings.notifications.push
})}
label="Push Notifications"
description="Receive mobile push notifications"
/>
<ToggleSwitch
enabled={settings.notifications.maintenance}
onChange={() => updateSetting('notifications', {
...settings.notifications,
maintenance: !settings.notifications.maintenance
})}
label="Maintenance Reminders"
description="Get reminded about vehicle maintenance"
/>
</div>
</div>
</GlassCard>
{/* Appearance & Units Section */}
<GlassCard>
<div className="p-4">
<h2 className="text-lg font-semibold text-slate-800 mb-4">Appearance & Units</h2>
<div className="space-y-4">
<ToggleSwitch
enabled={settings.darkMode}
onChange={() => updateSetting('darkMode', !settings.darkMode)}
label="Dark Mode"
description="Switch to dark theme"
/>
<div className="flex items-center justify-between">
<div>
<p className="font-medium text-slate-800">Unit System</p>
<p className="text-sm text-slate-500">
Currently using {settings.unitSystem === 'imperial' ? 'Miles & Gallons' : 'Kilometers & Liters'}
</p>
</div>
<MobilePill
label={settings.unitSystem === 'imperial' ? 'Metric' : 'Imperial'}
onClick={() => updateSetting('unitSystem', settings.unitSystem === 'imperial' ? 'metric' : 'imperial')}
variant="secondary"
/>
</div>
</div>
</div>
</GlassCard>
{/* Data Management Section */}
<GlassCard>
<div className="p-4">
<h2 className="text-lg font-semibold text-slate-800 mb-4">Data Management</h2>
<div className="space-y-3">
<button
onClick={() => setShowDataExport(true)}
className="w-full text-left p-3 bg-blue-50 text-blue-700 rounded-lg font-medium"
>
Export My Data
</button>
<p className="text-sm text-slate-500">
Download a copy of all your vehicle and fuel data
</p>
</div>
</div>
</GlassCard>
{/* Account Actions Section */}
<GlassCard>
<div className="p-4">
<h2 className="text-lg font-semibold text-slate-800 mb-4">Account Actions</h2>
<div className="space-y-3">
<button
onClick={handleLogout}
className="w-full py-3 px-4 bg-gray-100 text-gray-700 rounded-lg text-left font-medium"
>
Sign Out
</button>
<button
onClick={() => setShowDeleteConfirm(true)}
className="w-full py-3 px-4 bg-red-50 text-red-600 rounded-lg text-left font-medium"
>
Delete Account
</button>
</div>
</div>
</GlassCard>
{/* Data Export Modal */}
<Modal
isOpen={showDataExport}
onClose={() => setShowDataExport(false)}
title="Export Data"
>
<p className="text-slate-600 mb-4">
This will create a downloadable file containing all your vehicle data, fuel logs, and preferences.
</p>
<div className="flex space-x-3">
<button
onClick={() => setShowDataExport(false)}
className="flex-1 py-2 px-4 bg-gray-200 text-gray-700 rounded-lg font-medium"
>
Cancel
</button>
<button
onClick={handleExportData}
className="flex-1 py-2 px-4 bg-blue-600 text-white rounded-lg font-medium"
>
Export
</button>
</div>
</Modal>
{/* Delete Account Confirmation */}
<Modal
isOpen={showDeleteConfirm}
onClose={() => setShowDeleteConfirm(false)}
title="Delete Account"
>
<p className="text-slate-600 mb-4">
This action cannot be undone. All your data will be permanently deleted.
</p>
<div className="flex space-x-3">
<button
onClick={() => setShowDeleteConfirm(false)}
className="flex-1 py-2 px-4 bg-gray-200 text-gray-700 rounded-lg font-medium"
>
Cancel
</button>
<button
onClick={handleDeleteAccount}
className="flex-1 py-2 px-4 bg-red-600 text-white rounded-lg font-medium"
>
Delete
</button>
</div>
</Modal>
</div>
</MobileContainer>
);
};
```
## State Management Examples
### Enhanced Navigation Store
**File**: `frontend/src/core/store/navigation.ts`
```tsx
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
export type MobileScreen = 'dashboard' | 'vehicles' | 'fuel' | 'settings';
export type VehicleSubScreen = 'list' | 'detail' | 'add' | 'edit';
interface NavigationHistory {
screen: MobileScreen;
vehicleSubScreen?: VehicleSubScreen;
selectedVehicleId?: string | null;
timestamp: number;
metadata?: Record<string, any>;
}
interface FormState {
data: Record<string, any>;
timestamp: number;
isDirty: boolean;
}
interface NavigationState {
// Current navigation state
activeScreen: MobileScreen;
vehicleSubScreen: VehicleSubScreen;
selectedVehicleId: string | null;
// Navigation history for back button
navigationHistory: NavigationHistory[];
// Form state preservation
formStates: Record<string, FormState>;
// Loading and error states
isNavigating: boolean;
navigationError: string | null;
// Actions
navigateToScreen: (screen: MobileScreen, metadata?: Record<string, any>) => void;
navigateToVehicleSubScreen: (subScreen: VehicleSubScreen, vehicleId?: string, metadata?: Record<string, any>) => void;
goBack: () => boolean;
canGoBack: () => boolean;
saveFormState: (formId: string, data: any, isDirty?: boolean) => void;
restoreFormState: (formId: string) => FormState | null;
clearFormState: (formId: string) => void;
clearAllFormStates: () => void;
setNavigationError: (error: string | null) => void;
}
export const useNavigationStore = create<NavigationState>()(
persist(
(set, get) => ({
// Initial state
activeScreen: 'vehicles',
vehicleSubScreen: 'list',
selectedVehicleId: null,
navigationHistory: [],
formStates: {},
isNavigating: false,
navigationError: null,
// Navigation actions
navigateToScreen: (screen, metadata = {}) => {
const currentState = get();
set({ isNavigating: true, navigationError: null });
try {
const historyEntry: NavigationHistory = {
screen: currentState.activeScreen,
vehicleSubScreen: currentState.vehicleSubScreen,
selectedVehicleId: currentState.selectedVehicleId,
timestamp: Date.now(),
metadata,
};
set({
activeScreen: screen,
vehicleSubScreen: screen === 'vehicles' ? 'list' : currentState.vehicleSubScreen,
selectedVehicleId: screen === 'vehicles' ? currentState.selectedVehicleId : null,
navigationHistory: [...currentState.navigationHistory, historyEntry].slice(-10),
isNavigating: false,
});
} catch (error) {
set({
navigationError: error instanceof Error ? error.message : 'Navigation failed',
isNavigating: false
});
}
},
navigateToVehicleSubScreen: (subScreen, vehicleId = null, metadata = {}) => {
const currentState = get();
set({ isNavigating: true, navigationError: null });
try {
const historyEntry: NavigationHistory = {
screen: currentState.activeScreen,
vehicleSubScreen: currentState.vehicleSubScreen,
selectedVehicleId: currentState.selectedVehicleId,
timestamp: Date.now(),
metadata,
};
set({
vehicleSubScreen: subScreen,
selectedVehicleId: vehicleId || currentState.selectedVehicleId,
navigationHistory: [...currentState.navigationHistory, historyEntry].slice(-10),
isNavigating: false,
});
} catch (error) {
set({
navigationError: error instanceof Error ? error.message : 'Navigation failed',
isNavigating: false
});
}
},
goBack: () => {
const currentState = get();
const lastEntry = currentState.navigationHistory[currentState.navigationHistory.length - 1];
if (lastEntry) {
set({
activeScreen: lastEntry.screen,
vehicleSubScreen: lastEntry.vehicleSubScreen || 'list',
selectedVehicleId: lastEntry.selectedVehicleId,
navigationHistory: currentState.navigationHistory.slice(0, -1),
isNavigating: false,
navigationError: null,
});
return true;
}
return false;
},
canGoBack: () => {
return get().navigationHistory.length > 0;
},
// Form state management
saveFormState: (formId, data, isDirty = true) => {
const currentState = get();
const formState: FormState = {
data,
timestamp: Date.now(),
isDirty,
};
set({
formStates: {
...currentState.formStates,
[formId]: formState,
},
});
},
restoreFormState: (formId) => {
const state = get().formStates[formId];
const maxAge = 2 * 60 * 60 * 1000; // 2 hours
if (state && Date.now() - state.timestamp < maxAge) {
return state;
}
// Clean up old state
if (state) {
get().clearFormState(formId);
}
return null;
},
clearFormState: (formId) => {
const currentState = get();
const newFormStates = { ...currentState.formStates };
delete newFormStates[formId];
set({ formStates: newFormStates });
},
clearAllFormStates: () => {
set({ formStates: {} });
},
setNavigationError: (error) => {
set({ navigationError: error });
},
}),
{
name: 'motovaultpro-mobile-navigation',
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({
activeScreen: state.activeScreen,
vehicleSubScreen: state.vehicleSubScreen,
selectedVehicleId: state.selectedVehicleId,
formStates: state.formStates,
}),
}
)
);
```
### Form State Hook Implementation
**File**: `frontend/src/core/hooks/useFormState.ts`
```tsx
import { useState, useEffect, useCallback, useRef } from 'react';
import { useNavigationStore } from '../store/navigation';
import { debounce } from 'lodash';
export interface UseFormStateOptions<T> {
formId: string;
defaultValues: T;
autoSave?: boolean;
saveDelay?: number;
onRestore?: (data: T) => void;
onSave?: (data: T) => void;
validate?: (data: T) => Record<string, string> | null;
}
export interface FormStateReturn<T> {
formData: T;
updateFormData: (updates: Partial<T>) => void;
setFormData: (data: T) => void;
resetForm: () => void;
submitForm: () => Promise<void>;
hasChanges: boolean;
isRestored: boolean;
isSaving: boolean;
errors: Record<string, string>;
isValid: boolean;
}
export const useFormState = <T extends Record<string, any>>({
formId,
defaultValues,
autoSave = true,
saveDelay = 1000,
onRestore,
onSave,
validate,
}: UseFormStateOptions<T>): FormStateReturn<T> => {
const { saveFormState, restoreFormState, clearFormState } = useNavigationStore();
const [formData, setFormDataState] = useState<T>(defaultValues);
const [hasChanges, setHasChanges] = useState(false);
const [isRestored, setIsRestored] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [errors, setErrors] = useState<Record<string, string>>({});
const initialDataRef = useRef<T>(defaultValues);
const formDataRef = useRef<T>(formData);
// Update ref when formData changes
useEffect(() => {
formDataRef.current = formData;
}, [formData]);
// Validation
const validateForm = useCallback((data: T) => {
if (!validate) return {};
const validationErrors = validate(data);
return validationErrors || {};
}, [validate]);
// Restore form state on mount
useEffect(() => {
const restoredState = restoreFormState(formId);
if (restoredState && !isRestored) {
const restoredData = { ...defaultValues, ...restoredState.data };
setFormDataState(restoredData);
setHasChanges(restoredState.isDirty);
setIsRestored(true);
if (onRestore) {
onRestore(restoredData);
}
}
}, [formId, restoreFormState, defaultValues, isRestored, onRestore]);
// Auto-save with debounce
const debouncedSave = useCallback(
debounce(async (data: T, isDirty: boolean) => {
if (!autoSave || !isDirty) return;
try {
setIsSaving(true);
saveFormState(formId, data, isDirty);
if (onSave) {
await onSave(data);
}
} catch (error) {
console.warn('Form auto-save failed:', error);
} finally {
setIsSaving(false);
}
}, saveDelay),
[autoSave, saveDelay, formId, saveFormState, onSave]
);
// Trigger auto-save when form data changes
useEffect(() => {
if (hasChanges) {
const validationErrors = validateForm(formData);
setErrors(validationErrors);
debouncedSave(formData, hasChanges);
}
}, [formData, hasChanges, validateForm, debouncedSave]);
const updateFormData = useCallback((updates: Partial<T>) => {
setFormDataState((current) => {
const updated = { ...current, ...updates };
const hasActualChanges = JSON.stringify(updated) !== JSON.stringify(initialDataRef.current);
setHasChanges(hasActualChanges);
return updated;
});
}, []);
const setFormData = useCallback((data: T) => {
setFormDataState(data);
const hasActualChanges = JSON.stringify(data) !== JSON.stringify(initialDataRef.current);
setHasChanges(hasActualChanges);
}, []);
const resetForm = useCallback(() => {
setFormDataState(defaultValues);
setHasChanges(false);
setErrors({});
clearFormState(formId);
initialDataRef.current = { ...defaultValues };
}, [defaultValues, formId, clearFormState]);
const submitForm = useCallback(async () => {
const validationErrors = validateForm(formDataRef.current);
setErrors(validationErrors);
if (Object.keys(validationErrors).length > 0) {
throw new Error('Form validation failed');
}
try {
setHasChanges(false);
clearFormState(formId);
initialDataRef.current = { ...formDataRef.current };
if (onSave) {
await onSave(formDataRef.current);
}
} catch (error) {
setHasChanges(true); // Restore changes state on error
throw error;
}
}, [validateForm, formId, clearFormState, onSave]);
const isValid = Object.keys(errors).length === 0;
return {
formData,
updateFormData,
setFormData,
resetForm,
submitForm,
hasChanges,
isRestored,
isSaving,
errors,
isValid,
};
};
```
## Token Management Examples
### Enhanced API Client with 401 Retry
**File**: `frontend/src/core/api/enhancedClient.ts`
```tsx
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
interface TokenManager {
refreshToken(): Promise<string>;
isRefreshing(): boolean;
addFailedRequest(request: () => Promise<any>): void;
}
class AuthTokenManager implements TokenManager {
private static instance: AuthTokenManager;
private _isRefreshing = false;
private failedQueue: Array<{
resolve: (token: string) => void;
reject: (error: Error) => void;
}> = [];
private getAccessTokenSilently: any;
constructor(getAccessTokenSilently: any) {
this.getAccessTokenSilently = getAccessTokenSilently;
}
static getInstance(getAccessTokenSilently?: any): AuthTokenManager {
if (!AuthTokenManager.instance && getAccessTokenSilently) {
AuthTokenManager.instance = new AuthTokenManager(getAccessTokenSilently);
}
return AuthTokenManager.instance;
}
async refreshToken(): Promise<string> {
if (this._isRefreshing) {
return new Promise((resolve, reject) => {
this.failedQueue.push({ resolve, reject });
});
}
this._isRefreshing = true;
try {
const token = await this.getAccessTokenSilently({
cacheMode: 'off',
timeoutInSeconds: 20,
});
// Process queued requests
this.failedQueue.forEach(({ resolve }) => resolve(token));
this.failedQueue = [];
return token;
} catch (error) {
// Reject queued requests
this.failedQueue.forEach(({ reject }) => reject(error as Error));
this.failedQueue = [];
throw error;
} finally {
this._isRefreshing = false;
}
}
isRefreshing(): boolean {
return this._isRefreshing;
}
addFailedRequest(callback: () => Promise<any>): void {
this.failedQueue.push({
resolve: () => callback(),
reject: (error) => Promise.reject(error),
});
}
}
export const createEnhancedApiClient = (getAccessTokenSilently: any): AxiosInstance => {
const tokenManager = AuthTokenManager.getInstance(getAccessTokenSilently);
const client = axios.create({
baseURL: process.env.REACT_APP_API_URL || '/api',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
// Request interceptor - inject tokens
client.interceptors.request.use(
async (config) => {
try {
// Don't add token if already refreshing or if this is a retry
if (!config._retry && !tokenManager.isRefreshing()) {
const token = await getAccessTokenSilently({
cacheMode: 'on',
timeoutInSeconds: 15,
});
if (token) {
config.headers = config.headers || {};
config.headers.Authorization = `Bearer ${token}`;
}
}
} catch (error) {
console.warn('Token acquisition failed, proceeding without token:', error);
}
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor - handle 401s with token refresh retry
client.interceptors.response.use(
(response: AxiosResponse) => response,
async (error) => {
const originalRequest = error.config;
// Handle 401 responses with token refresh
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
console.log('401 detected, attempting token refresh...');
const newToken = await tokenManager.refreshToken();
// Update the failed request with new token
originalRequest.headers = originalRequest.headers || {};
originalRequest.headers.Authorization = `Bearer ${newToken}`;
// Retry the original request
return client(originalRequest);
} catch (refreshError) {
console.error('Token refresh failed:', refreshError);
// Clear any stored tokens and redirect to login
localStorage.clear();
window.location.href = '/';
return Promise.reject(refreshError);
}
}
// Enhanced mobile error handling
if (error.code === 'ECONNABORTED' || error.message.includes('timeout')) {
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent
);
if (isMobile) {
error.message = 'Connection timeout. Please check your network and try again.';
}
}
return Promise.reject(error);
}
);
return client;
};
// Usage example
export const useApiClient = () => {
const { getAccessTokenSilently } = useAuth0();
return useMemo(() => {
return createEnhancedApiClient(getAccessTokenSilently);
}, [getAccessTokenSilently]);
};
```
## Mobile Component Examples
### Enhanced Add Vehicle Form with State Persistence
**File**: `frontend/src/features/vehicles/mobile/EnhancedAddVehicleScreen.tsx`
```tsx
import React, { useCallback } from 'react';
import { ArrowLeft, Save } from 'lucide-react';
import { useFormState } from '../../../core/hooks/useFormState';
import { useNavigationStore } from '../../../core/store/navigation';
import { GlassCard, MobileContainer, MobilePill } from '../../../shared-minimal/components/mobile';
interface VehicleFormData {
year: string;
make: string;
model: string;
trim: string;
vin: string;
licensePlate: string;
nickname: string;
color: string;
}
interface EnhancedAddVehicleScreenProps {
onVehicleAdded: () => void;
}
export const EnhancedAddVehicleScreen: React.FC<EnhancedAddVehicleScreenProps> = ({
onVehicleAdded,
}) => {
const { goBack } = useNavigationStore();
const validateForm = useCallback((data: VehicleFormData) => {
const errors: Record<string, string> = {};
if (!data.year || parseInt(data.year) < 1900 || parseInt(data.year) > new Date().getFullYear() + 1) {
errors.year = 'Please enter a valid year';
}
if (!data.make.trim()) {
errors.make = 'Make is required';
}
if (!data.model.trim()) {
errors.model = 'Model is required';
}
if (data.vin && data.vin.length !== 17) {
errors.vin = 'VIN must be 17 characters';
}
if (!data.vin.trim() && !data.licensePlate.trim()) {
errors.licensePlate = 'Either VIN or License Plate is required';
}
return Object.keys(errors).length > 0 ? errors : null;
}, []);
const {
formData,
updateFormData,
resetForm,
submitForm,
hasChanges,
isRestored,
isSaving,
errors,
isValid,
} = useFormState<VehicleFormData>({
formId: 'add-vehicle',
defaultValues: {
year: '',
make: '',
model: '',
trim: '',
vin: '',
licensePlate: '',
nickname: '',
color: '',
},
validate: validateForm,
onRestore: (data) => {
console.log('Form data restored:', data);
},
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!isValid) {
return;
}
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
await submitForm();
onVehicleAdded();
} catch (error) {
console.error('Error adding vehicle:', error);
// Form state is preserved on error
}
};
const handleBack = () => {
if (hasChanges) {
const confirmLeave = window.confirm(
'You have unsaved changes. Are you sure you want to leave? Your changes will be saved as a draft.'
);
if (!confirmLeave) return;
}
goBack();
};
return (
<MobileContainer>
<div className="space-y-4 pb-20">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center">
<button onClick={handleBack} className="mr-4">
<ArrowLeft className="w-6 h-6 text-slate-700" />
</button>
<div>
<h1 className="text-xl font-bold text-slate-800">Add Vehicle</h1>
{isRestored && (
<p className="text-sm text-blue-600">Draft restored</p>
)}
</div>
</div>
{isSaving && (
<div className="flex items-center text-sm text-blue-600">
<Save className="w-4 h-4 mr-1" />
Saving...
</div>
)}
</div>
<form onSubmit={handleSubmit} className="space-y-4">
{/* Basic Information */}
<GlassCard>
<div className="p-4">
<h2 className="text-lg font-semibold text-slate-800 mb-4">Basic Information</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Year *
</label>
<input
type="number"
placeholder="2023"
value={formData.year}
onChange={(e) => updateFormData({ year: e.target.value })}
className={`w-full p-3 border rounded-lg ${
errors.year ? 'border-red-300' : 'border-slate-300'
}`}
/>
{errors.year && (
<p className="text-sm text-red-600 mt-1">{errors.year}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Make *
</label>
<input
type="text"
placeholder="Toyota"
value={formData.make}
onChange={(e) => updateFormData({ make: e.target.value })}
className={`w-full p-3 border rounded-lg ${
errors.make ? 'border-red-300' : 'border-slate-300'
}`}
/>
{errors.make && (
<p className="text-sm text-red-600 mt-1">{errors.make}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Model *
</label>
<input
type="text"
placeholder="Camry"
value={formData.model}
onChange={(e) => updateFormData({ model: e.target.value })}
className={`w-full p-3 border rounded-lg ${
errors.model ? 'border-red-300' : 'border-slate-300'
}`}
/>
{errors.model && (
<p className="text-sm text-red-600 mt-1">{errors.model}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Trim
</label>
<input
type="text"
placeholder="LE, XLE, etc."
value={formData.trim}
onChange={(e) => updateFormData({ trim: e.target.value })}
className="w-full p-3 border border-slate-300 rounded-lg"
/>
</div>
</div>
</div>
</GlassCard>
{/* Identification */}
<GlassCard>
<div className="p-4">
<h2 className="text-lg font-semibold text-slate-800 mb-4">Identification</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
VIN
</label>
<input
type="text"
placeholder="17-character VIN"
value={formData.vin}
onChange={(e) => updateFormData({ vin: e.target.value.toUpperCase() })}
maxLength={17}
className={`w-full p-3 border rounded-lg font-mono text-sm ${
errors.vin ? 'border-red-300' : 'border-slate-300'
}`}
/>
{errors.vin && (
<p className="text-sm text-red-600 mt-1">{errors.vin}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
License Plate
</label>
<input
type="text"
placeholder="ABC-123"
value={formData.licensePlate}
onChange={(e) => updateFormData({ licensePlate: e.target.value.toUpperCase() })}
className={`w-full p-3 border rounded-lg ${
errors.licensePlate ? 'border-red-300' : 'border-slate-300'
}`}
/>
{errors.licensePlate && (
<p className="text-sm text-red-600 mt-1">{errors.licensePlate}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Nickname
</label>
<input
type="text"
placeholder="My Daily Driver"
value={formData.nickname}
onChange={(e) => updateFormData({ nickname: e.target.value })}
className="w-full p-3 border border-slate-300 rounded-lg"
/>
</div>
</div>
</div>
</GlassCard>
{/* Form Actions */}
<div className="flex space-x-3 pt-4">
<button
type="button"
onClick={resetForm}
className="flex-1 py-3 bg-gray-200 text-gray-700 rounded-lg font-medium"
>
Clear All
</button>
<button
type="submit"
disabled={!isValid}
className={`flex-1 py-3 rounded-lg font-medium ${
isValid
? 'bg-blue-600 text-white'
: 'bg-gray-300 text-gray-500 cursor-not-allowed'
}`}
>
Add Vehicle
</button>
</div>
{hasChanges && (
<p className="text-sm text-blue-600 text-center bg-blue-50 p-2 rounded-lg">
Changes are being saved automatically
</p>
)}
</form>
</div>
</MobileContainer>
);
};
```
## App Integration Examples
### Updated App.tsx with Enhanced Navigation
**File**: `frontend/src/App.tsx` (key sections)
```tsx
import React, { useEffect } from 'react';
import { useAuth0 } from '@auth0/auth0-react';
import { useNavigationStore } from './core/store/navigation';
import { useUserStore } from './core/store/user';
import { MobileSettingsScreen } from './features/settings/mobile/MobileSettingsScreen';
import { EnhancedAddVehicleScreen } from './features/vehicles/mobile/EnhancedAddVehicleScreen';
const MobileApp: React.FC = () => {
const { user, isAuthenticated, isLoading } = useAuth0();
const {
activeScreen,
vehicleSubScreen,
selectedVehicleId,
navigateToScreen,
navigateToVehicleSubScreen,
goBack,
canGoBack,
navigationError,
} = useNavigationStore();
const { setUserProfile } = useUserStore();
// Update user profile when authenticated
useEffect(() => {
if (isAuthenticated && user) {
setUserProfile(user);
}
}, [isAuthenticated, user, setUserProfile]);
// Handle mobile back button and navigation errors
useEffect(() => {
const handlePopState = (event: PopStateEvent) => {
event.preventDefault();
if (canGoBack()) {
goBack();
}
};
const handleNavigationError = () => {
if (navigationError) {
console.error('Navigation error:', navigationError);
// Could show toast notification here
}
};
window.addEventListener('popstate', handlePopState);
handleNavigationError();
return () => {
window.removeEventListener('popstate', handlePopState);
};
}, [goBack, canGoBack, navigationError]);
const handleVehicleSelect = useCallback((vehicleId: string) => {
navigateToVehicleSubScreen('detail', vehicleId, { source: 'vehicle-list' });
}, [navigateToVehicleSubScreen]);
const handleAddVehicle = useCallback(() => {
navigateToVehicleSubScreen('add', null, { source: 'vehicle-list' });
}, [navigateToVehicleSubScreen]);
const handleBackToList = useCallback(() => {
navigateToVehicleSubScreen('list', null, { source: 'back-navigation' });
}, [navigateToVehicleSubScreen]);
const handleVehicleAdded = useCallback(() => {
navigateToVehicleSubScreen('list', null, { source: 'vehicle-added' });
}, [navigateToVehicleSubScreen]);
// Show loading screen while Auth0 initializes
if (isLoading) {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-blue-50 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-slate-600">Loading MotoVaultPro...</p>
</div>
</div>
);
}
// Show login screen if not authenticated
if (!isAuthenticated) {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-blue-50 flex items-center justify-center p-4">
<div className="max-w-md w-full text-center">
<h1 className="text-3xl font-bold text-slate-800 mb-2">MotoVaultPro</h1>
<p className="text-slate-600 mb-8">Track your vehicles and fuel efficiency</p>
<button
onClick={() => loginWithRedirect()}
className="w-full py-3 px-6 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors"
>
Sign In
</button>
</div>
</div>
);
}
// Main mobile app interface
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-blue-50">
{/* Navigation Error Banner */}
{navigationError && (
<div className="bg-red-100 border-l-4 border-red-500 text-red-700 p-4">
<p className="font-medium">Navigation Error</p>
<p className="text-sm">{navigationError}</p>
</div>
)}
{/* Screen Content */}
{renderActiveScreen()}
{/* Bottom Navigation */}
<BottomNavigation
activeScreen={activeScreen}
onScreenChange={navigateToScreen}
hasChanges={hasUnsavedChanges()} // Could implement this to show unsaved indicators
/>
</div>
);
// Screen rendering logic
function renderActiveScreen() {
switch (activeScreen) {
case 'vehicles':
return renderVehiclesScreen();
case 'fuel':
return <FuelScreen />;
case 'dashboard':
return <DashboardScreen />;
case 'settings':
return <MobileSettingsScreen />;
default:
return renderVehiclesScreen();
}
}
function renderVehiclesScreen() {
switch (vehicleSubScreen) {
case 'list':
return (
<VehiclesMobileScreen
onVehicleSelect={handleVehicleSelect}
onAddVehicle={handleAddVehicle}
/>
);
case 'detail':
return (
<VehicleDetailMobile
vehicleId={selectedVehicleId!}
onBack={handleBackToList}
/>
);
case 'add':
return (
<EnhancedAddVehicleScreen
onVehicleAdded={handleVehicleAdded}
/>
);
default:
return (
<VehiclesMobileScreen
onVehicleSelect={handleVehicleSelect}
onAddVehicle={handleAddVehicle}
/>
);
}
}
};
```
These code examples provide concrete, implementable solutions for all aspects of the mobile optimization plan. Each example includes proper error handling, TypeScript types, and integration with the existing architecture.