Initial Commit
This commit is contained in:
24
frontend/src/core/store/app.ts
Normal file
24
frontend/src/core/store/app.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { create } from 'zustand';
|
||||
import { Vehicle } from '../../features/vehicles/types/vehicles.types';
|
||||
|
||||
interface AppState {
|
||||
// UI state
|
||||
sidebarOpen: boolean;
|
||||
selectedVehicle: Vehicle | null;
|
||||
|
||||
// Actions
|
||||
toggleSidebar: () => void;
|
||||
setSidebarOpen: (open: boolean) => void;
|
||||
setSelectedVehicle: (vehicle: Vehicle | null) => void;
|
||||
}
|
||||
|
||||
export const useAppStore = create<AppState>((set) => ({
|
||||
// Initial state
|
||||
sidebarOpen: false,
|
||||
selectedVehicle: null,
|
||||
|
||||
// Actions
|
||||
toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
|
||||
setSidebarOpen: (open: boolean) => set({ sidebarOpen: open }),
|
||||
setSelectedVehicle: (vehicle: Vehicle | null) => set({ selectedVehicle: vehicle }),
|
||||
}));
|
||||
@@ -1,54 +1,12 @@
|
||||
/**
|
||||
* @ai-summary Global state management with Zustand
|
||||
* @ai-context Minimal global state, features manage their own state
|
||||
*/
|
||||
// Export navigation store
|
||||
export { useNavigationStore } from './navigation';
|
||||
export type { MobileScreen, VehicleSubScreen } from './navigation';
|
||||
|
||||
import { create } from 'zustand';
|
||||
import { devtools, persist } from 'zustand/middleware';
|
||||
// Export user store
|
||||
export { useUserStore } from './user';
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
name?: string;
|
||||
}
|
||||
// Export app store (compatibility)
|
||||
export { useAppStore } from './app';
|
||||
|
||||
interface AppState {
|
||||
// User state
|
||||
user: User | null;
|
||||
setUser: (user: User | null) => void;
|
||||
|
||||
// UI state
|
||||
sidebarOpen: boolean;
|
||||
toggleSidebar: () => void;
|
||||
|
||||
// Selected vehicle (for context)
|
||||
selectedVehicleId: string | null;
|
||||
setSelectedVehicle: (id: string | null) => void;
|
||||
}
|
||||
|
||||
export const useAppStore = create<AppState>()(
|
||||
devtools(
|
||||
persist(
|
||||
(set) => ({
|
||||
// User state
|
||||
user: null,
|
||||
setUser: (user) => set({ user }),
|
||||
|
||||
// UI state
|
||||
sidebarOpen: true,
|
||||
toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
|
||||
|
||||
// Selected vehicle
|
||||
selectedVehicleId: null,
|
||||
setSelectedVehicle: (vehicleId) => set({ selectedVehicleId: vehicleId }),
|
||||
}),
|
||||
{
|
||||
name: 'motovaultpro-storage',
|
||||
partialize: (state) => ({
|
||||
selectedVehicleId: state.selectedVehicleId,
|
||||
sidebarOpen: state.sidebarOpen,
|
||||
}),
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
// Note: This replaces any existing store exports and provides
|
||||
// centralized access to all Zustand stores in the application
|
||||
205
frontend/src/core/store/navigation.ts
Normal file
205
frontend/src/core/store/navigation.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist, createJSONStorage } from 'zustand/middleware';
|
||||
|
||||
export type MobileScreen = 'Dashboard' | 'Vehicles' | 'Log 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();
|
||||
|
||||
// Skip navigation if already on the same screen
|
||||
if (currentState.activeScreen === screen && !currentState.isNavigating) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const historyEntry: NavigationHistory = {
|
||||
screen: currentState.activeScreen,
|
||||
vehicleSubScreen: currentState.vehicleSubScreen,
|
||||
selectedVehicleId: currentState.selectedVehicleId,
|
||||
timestamp: Date.now(),
|
||||
metadata,
|
||||
};
|
||||
|
||||
// Update state atomically to prevent blank screens
|
||||
set({
|
||||
activeScreen: screen,
|
||||
vehicleSubScreen: screen === 'Vehicles' ? currentState.vehicleSubScreen : 'list',
|
||||
selectedVehicleId: screen === 'Vehicles' ? currentState.selectedVehicleId : null,
|
||||
navigationHistory: [...currentState.navigationHistory, historyEntry].slice(-10),
|
||||
isNavigating: false,
|
||||
navigationError: null,
|
||||
});
|
||||
} catch (error) {
|
||||
set({
|
||||
navigationError: error instanceof Error ? error.message : 'Navigation failed',
|
||||
isNavigating: false
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
navigateToVehicleSubScreen: (subScreen, vehicleId, 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 !== null ? 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,
|
||||
}),
|
||||
}
|
||||
)
|
||||
);
|
||||
101
frontend/src/core/store/user.ts
Normal file
101
frontend/src/core/store/user.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist, createJSONStorage } 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',
|
||||
storage: createJSONStorage(() => localStorage),
|
||||
partialize: (state) => ({
|
||||
userProfile: state.userProfile,
|
||||
preferences: state.preferences,
|
||||
// Don't persist session data
|
||||
}),
|
||||
}
|
||||
)
|
||||
);
|
||||
Reference in New Issue
Block a user