Initial Commit

This commit is contained in:
Eric Gullickson
2025-09-17 16:09:15 -05:00
parent 0cdb9803de
commit a052040e3a
373 changed files with 437090 additions and 6773 deletions

View File

@@ -0,0 +1,671 @@
# State Management & Navigation Consistency Solutions
## Overview
This document addresses critical state management issues in mobile navigation, including context loss during screen transitions, form state persistence, and navigation consistency between mobile and desktop platforms.
## Issues Identified
### 1. Mobile State Reset Issues
**Location**: `frontend/src/App.tsx` mobile navigation logic
**Problem**: Navigation between screens resets critical state:
- `selectedVehicle` resets when switching screens
- `showAddVehicle` form state lost during navigation
- User context not maintained across screen transitions
- Mobile navigation doesn't preserve history
### 2. Navigation Paradigm Split
**Mobile**: State-based navigation without URLs (`activeScreen` state)
**Desktop**: URL-based routing with React Router
**Impact**: Inconsistent user experience and different development patterns
### 3. State Persistence Gaps
- User context not persisted (requires re-authentication overhead)
- Form data lost when navigating away
- Mobile navigation state not preserved across app restarts
- Settings changes not immediately reflected across screens
## Solution Architecture
### Enhanced Mobile State Management
#### 1. Navigation State Persistence
**File**: `frontend/src/core/store/navigation.ts` (new)
```tsx
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
export type MobileScreen = 'dashboard' | 'vehicles' | 'fuel' | 'settings';
export type VehicleSubScreen = 'list' | 'detail' | 'add' | 'edit';
interface NavigationState {
// Current navigation state
activeScreen: MobileScreen;
vehicleSubScreen: VehicleSubScreen;
selectedVehicleId: string | null;
// Navigation history for back button
navigationHistory: {
screen: MobileScreen;
vehicleSubScreen?: VehicleSubScreen;
selectedVehicleId?: string | null;
timestamp: number;
}[];
// Form state preservation
formStates: Record<string, any>;
// Actions
navigateToScreen: (screen: MobileScreen) => void;
navigateToVehicleSubScreen: (subScreen: VehicleSubScreen, vehicleId?: string) => void;
goBack: () => void;
saveFormState: (formId: string, state: any) => void;
restoreFormState: (formId: string) => any;
clearFormState: (formId: string) => void;
}
export const useNavigationStore = create<NavigationState>()(
persist(
(set, get) => ({
// Initial state
activeScreen: 'vehicles',
vehicleSubScreen: 'list',
selectedVehicleId: null,
navigationHistory: [],
formStates: {},
// Navigation actions
navigateToScreen: (screen) => {
const currentState = get();
const historyEntry = {
screen: currentState.activeScreen,
vehicleSubScreen: currentState.vehicleSubScreen,
selectedVehicleId: currentState.selectedVehicleId,
timestamp: Date.now(),
};
set({
activeScreen: screen,
vehicleSubScreen: screen === 'vehicles' ? 'list' : currentState.vehicleSubScreen,
selectedVehicleId: screen === 'vehicles' ? currentState.selectedVehicleId : null,
navigationHistory: [...currentState.navigationHistory, historyEntry].slice(-10), // Keep last 10
});
},
navigateToVehicleSubScreen: (subScreen, vehicleId = null) => {
const currentState = get();
const historyEntry = {
screen: currentState.activeScreen,
vehicleSubScreen: currentState.vehicleSubScreen,
selectedVehicleId: currentState.selectedVehicleId,
timestamp: Date.now(),
};
set({
vehicleSubScreen: subScreen,
selectedVehicleId: vehicleId || currentState.selectedVehicleId,
navigationHistory: [...currentState.navigationHistory, historyEntry].slice(-10),
});
},
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),
});
}
},
// Form state management
saveFormState: (formId, state) => {
set((current) => ({
formStates: {
...current.formStates,
[formId]: { ...state, timestamp: Date.now() },
},
}));
},
restoreFormState: (formId) => {
const state = get().formStates[formId];
// Return state if it's less than 1 hour old
if (state && Date.now() - state.timestamp < 3600000) {
return state;
}
return null;
},
clearFormState: (formId) => {
set((current) => {
const newFormStates = { ...current.formStates };
delete newFormStates[formId];
return { formStates: newFormStates };
});
},
}),
{
name: 'motovaultpro-mobile-navigation',
partialize: (state) => ({
activeScreen: state.activeScreen,
vehicleSubScreen: state.vehicleSubScreen,
selectedVehicleId: state.selectedVehicleId,
formStates: state.formStates,
// Don't persist navigation history - rebuild on app start
}),
}
)
);
```
#### 2. Enhanced User Context Persistence
**File**: `frontend/src/core/store/user.ts` (new)
```tsx
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface UserPreferences {
unitSystem: 'imperial' | 'metric';
darkMode: boolean;
notifications: {
email: boolean;
push: boolean;
maintenance: boolean;
};
}
interface UserState {
// User data (persisted subset)
userProfile: {
id: string;
name: string;
email: string;
picture: string;
} | null;
preferences: UserPreferences;
// Session data (not persisted)
isOnline: boolean;
lastSyncTimestamp: number;
// Actions
setUserProfile: (profile: any) => void;
updatePreferences: (preferences: Partial<UserPreferences>) => void;
setOnlineStatus: (isOnline: boolean) => void;
updateLastSync: () => void;
clearUserData: () => void;
}
export const useUserStore = create<UserState>()(
persist(
(set) => ({
// Initial state
userProfile: null,
preferences: {
unitSystem: 'imperial',
darkMode: false,
notifications: {
email: true,
push: true,
maintenance: true,
},
},
isOnline: true,
lastSyncTimestamp: 0,
// Actions
setUserProfile: (profile) => {
if (profile) {
set({
userProfile: {
id: profile.sub,
name: profile.name,
email: profile.email,
picture: profile.picture,
},
});
}
},
updatePreferences: (newPreferences) => {
set((state) => ({
preferences: { ...state.preferences, ...newPreferences },
}));
},
setOnlineStatus: (isOnline) => set({ isOnline }),
updateLastSync: () => set({ lastSyncTimestamp: Date.now() }),
clearUserData: () => set({
userProfile: null,
preferences: {
unitSystem: 'imperial',
darkMode: false,
notifications: {
email: true,
push: true,
maintenance: true,
},
},
}),
}),
{
name: 'motovaultpro-user-context',
partialize: (state) => ({
userProfile: state.userProfile,
preferences: state.preferences,
// Don't persist session data
}),
}
)
);
```
#### 3. Smart Form State Hook
**File**: `frontend/src/core/hooks/useFormState.ts` (new)
```tsx
import { useState, useEffect, useCallback } from 'react';
import { useNavigationStore } from '../store/navigation';
export interface UseFormStateOptions {
formId: string;
defaultValues: Record<string, any>;
autoSave?: boolean;
saveDelay?: number;
}
export const useFormState = <T extends Record<string, any>>({
formId,
defaultValues,
autoSave = true,
saveDelay = 1000,
}: UseFormStateOptions) => {
const { saveFormState, restoreFormState, clearFormState } = useNavigationStore();
const [formData, setFormData] = useState<T>(defaultValues as T);
const [hasChanges, setHasChanges] = useState(false);
const [isRestored, setIsRestored] = useState(false);
// Restore form state on mount
useEffect(() => {
const restoredState = restoreFormState(formId);
if (restoredState && !isRestored) {
setFormData({ ...defaultValues, ...restoredState });
setHasChanges(true);
setIsRestored(true);
}
}, [formId, restoreFormState, defaultValues, isRestored]);
// Auto-save with debounce
useEffect(() => {
if (!autoSave || !hasChanges) return;
const timer = setTimeout(() => {
saveFormState(formId, formData);
}, saveDelay);
return () => clearTimeout(timer);
}, [formData, hasChanges, autoSave, saveDelay, formId, saveFormState]);
const updateFormData = useCallback((updates: Partial<T>) => {
setFormData((current) => ({ ...current, ...updates }));
setHasChanges(true);
}, []);
const resetForm = useCallback(() => {
setFormData(defaultValues as T);
setHasChanges(false);
clearFormState(formId);
}, [defaultValues, formId, clearFormState]);
const submitForm = useCallback(() => {
setHasChanges(false);
clearFormState(formId);
}, [formId, clearFormState]);
return {
formData,
updateFormData,
resetForm,
submitForm,
hasChanges,
isRestored,
};
};
```
### Implementation in App.tsx
#### Updated Mobile Navigation Logic
**File**: `frontend/src/App.tsx` (modifications)
```tsx
import { useNavigationStore } from './core/store/navigation';
import { useUserStore } from './core/store/user';
// Replace existing mobile detection and state management
const MobileApp: React.FC = () => {
const { user, isAuthenticated } = useAuth0();
const {
activeScreen,
vehicleSubScreen,
selectedVehicleId,
navigateToScreen,
navigateToVehicleSubScreen,
goBack,
} = useNavigationStore();
const { setUserProfile } = useUserStore();
// Update user profile when authenticated
useEffect(() => {
if (isAuthenticated && user) {
setUserProfile(user);
}
}, [isAuthenticated, user, setUserProfile]);
// Handle mobile back button
useEffect(() => {
const handlePopState = (event: PopStateEvent) => {
event.preventDefault();
goBack();
};
window.addEventListener('popstate', handlePopState);
return () => window.removeEventListener('popstate', handlePopState);
}, [goBack]);
const handleVehicleSelect = (vehicleId: string) => {
navigateToVehicleSubScreen('detail', vehicleId);
};
const handleAddVehicle = () => {
navigateToVehicleSubScreen('add');
};
const handleBackToList = () => {
navigateToVehicleSubScreen('list');
};
// Render screens based on navigation state
const renderActiveScreen = () => {
switch (activeScreen) {
case 'vehicles':
return renderVehiclesScreen();
case 'fuel':
return <FuelScreen />;
case 'dashboard':
return <DashboardScreen />;
case 'settings':
return <MobileSettingsScreen />;
default:
return renderVehiclesScreen();
}
};
const renderVehiclesScreen = () => {
switch (vehicleSubScreen) {
case 'list':
return (
<VehiclesMobileScreen
onVehicleSelect={handleVehicleSelect}
onAddVehicle={handleAddVehicle}
/>
);
case 'detail':
return (
<VehicleDetailMobile
vehicleId={selectedVehicleId!}
onBack={handleBackToList}
/>
);
case 'add':
return (
<AddVehicleScreen
onBack={handleBackToList}
onVehicleAdded={handleBackToList}
/>
);
default:
return (
<VehiclesMobileScreen
onVehicleSelect={handleVehicleSelect}
onAddVehicle={handleAddVehicle}
/>
);
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-blue-50">
{renderActiveScreen()}
<BottomNavigation
activeScreen={activeScreen}
onScreenChange={navigateToScreen}
/>
</div>
);
};
```
#### Enhanced Add Vehicle Form with State Persistence
**File**: `frontend/src/features/vehicles/mobile/AddVehicleScreen.tsx` (example usage)
```tsx
import React from 'react';
import { useFormState } from '../../../core/hooks/useFormState';
interface AddVehicleScreenProps {
onBack: () => void;
onVehicleAdded: () => void;
}
export const AddVehicleScreen: React.FC<AddVehicleScreenProps> = ({
onBack,
onVehicleAdded,
}) => {
const {
formData,
updateFormData,
resetForm,
submitForm,
hasChanges,
isRestored,
} = useFormState({
formId: 'add-vehicle',
defaultValues: {
year: '',
make: '',
model: '',
trim: '',
vin: '',
licensePlate: '',
nickname: '',
},
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
// Submit vehicle data
await submitVehicle(formData);
submitForm(); // Clear saved state
onVehicleAdded();
} catch (error) {
// Handle error, form state is preserved
console.error('Error adding vehicle:', error);
}
};
return (
<div className="p-4">
<div className="flex items-center mb-6">
<button onClick={onBack} className="mr-4">
<ArrowLeft className="w-6 h-6" />
</button>
<h1 className="text-xl font-bold">Add Vehicle</h1>
{isRestored && (
<span className="ml-auto text-sm text-blue-600">Draft restored</span>
)}
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<input
type="text"
placeholder="Year"
value={formData.year}
onChange={(e) => updateFormData({ year: e.target.value })}
className="w-full p-3 border rounded-lg"
/>
{/* More form fields... */}
<div className="flex space-x-3">
<button
type="button"
onClick={resetForm}
className="flex-1 py-3 bg-gray-200 text-gray-700 rounded-lg"
>
Clear
</button>
<button
type="submit"
className="flex-1 py-3 bg-blue-600 text-white rounded-lg"
>
Add Vehicle
</button>
</div>
{hasChanges && (
<p className="text-sm text-blue-600 text-center">
Changes are being saved automatically
</p>
)}
</form>
</div>
);
};
```
## Integration with Existing Systems
### 1. Zustand Store Integration
**File**: `frontend/src/core/store/index.ts` (existing file modifications)
```tsx
// Export new stores alongside existing ones
export { useNavigationStore } from './navigation';
export { useUserStore } from './user';
// Keep existing store exports
export { useAppStore } from './app';
```
### 2. Auth0 Integration Enhancement
**File**: `frontend/src/core/auth/Auth0Provider.tsx` (modifications)
```tsx
import { useUserStore } from '../store/user';
// Inside the Auth0Provider component
const { setUserProfile, clearUserData } = useUserStore();
// Update user profile on authentication
useEffect(() => {
if (isAuthenticated && user) {
setUserProfile(user);
} else if (!isAuthenticated) {
clearUserData();
}
}, [isAuthenticated, user, setUserProfile, clearUserData]);
```
### 3. Unit System Integration
**File**: `frontend/src/shared-minimal/utils/units.ts` (modifications)
```tsx
import { useUserStore } from '../../core/store/user';
// Update existing unit hooks to use new store
export const useUnitSystem = () => {
const { preferences, updatePreferences } = useUserStore();
const toggleUnitSystem = () => {
const newSystem = preferences.unitSystem === 'imperial' ? 'metric' : 'imperial';
updatePreferences({ unitSystem: newSystem });
};
return {
unitSystem: preferences.unitSystem,
toggleUnitSystem,
};
};
```
## Testing Requirements
### State Persistence Tests
- ✅ Navigation state persists across app restarts
- ✅ Selected vehicle context maintained during navigation
- ✅ Form state preserved when navigating away and returning
- ✅ User preferences persist and sync across screens
- ✅ Navigation history works correctly with back button
### Mobile Navigation Tests
- ✅ Screen transitions maintain context
- ✅ Bottom navigation reflects current state accurately
- ✅ Add vehicle form preserves data during interruptions
- ✅ Settings changes reflect immediately across screens
- ✅ Authentication state managed correctly
### Integration Tests
- ✅ New stores integrate properly with existing components
- ✅ Auth0 integration works with enhanced user persistence
- ✅ Unit system changes sync between old and new systems
- ✅ No conflicts with existing Zustand store patterns
## Migration Strategy
### Phase 1: Store Creation
1. Create new navigation and user stores
2. Implement form state management hook
3. Test stores in isolation
### Phase 2: Mobile App Integration
1. Update App.tsx to use new navigation store
2. Modify mobile screens to use form state hook
3. Test mobile navigation and persistence
### Phase 3: System Integration
1. Integrate with existing Auth0 provider
2. Update unit system to use new user store
3. Ensure backward compatibility
### Phase 4: Enhancement & Optimization
1. Add advanced features like offline persistence
2. Optimize performance and storage usage
3. Add error handling and recovery mechanisms
## Success Criteria
Upon completion:
1. **Navigation Consistency**: Mobile navigation maintains context across all transitions
2. **State Persistence**: All user data, preferences, and form states persist appropriately
3. **Form Recovery**: Users can navigate away from forms and return without data loss
4. **User Context**: User preferences and settings sync immediately across all screens
5. **Back Navigation**: Mobile back button works correctly with navigation history
6. **Integration**: New state management integrates seamlessly with existing systems
This enhanced state management system will provide a robust foundation for consistent mobile and desktop experiences while maintaining all existing functionality.