40 KiB
40 KiB
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
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
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
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
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
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)
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.