fix: sync mobile routing with browser URL for direct navigation (refs #163)

URL-to-screen sync on mount and screen-to-URL sync via replaceState
enable direct URL navigation, page refresh, and bookmarks on mobile.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eric Gullickson
2026-02-13 19:35:53 -06:00
parent 325cf08df0
commit 0e8c6070ef
3 changed files with 57 additions and 3 deletions

View File

@@ -81,7 +81,7 @@ import { useOptimisticVehicles } from './features/vehicles/hooks/useOptimisticVe
import { CreateVehicleRequest } from './features/vehicles/types/vehicles.types'; import { CreateVehicleRequest } from './features/vehicles/types/vehicles.types';
import { MobileSettingsScreen } from './features/settings/mobile/MobileSettingsScreen'; import { MobileSettingsScreen } from './features/settings/mobile/MobileSettingsScreen';
import { SecurityMobileScreen } from './features/settings/mobile/SecurityMobileScreen'; import { SecurityMobileScreen } from './features/settings/mobile/SecurityMobileScreen';
import { useNavigationStore, useUserStore } from './core/store'; import { useNavigationStore, useUserStore, routeToScreen, screenToRoute } from './core/store';
import { useNeedsVehicleSelection, useDowngrade } from './features/subscription/hooks/useSubscription'; import { useNeedsVehicleSelection, useDowngrade } from './features/subscription/hooks/useSubscription';
import { useVehicles } from './features/vehicles/hooks/useVehicles'; import { useVehicles } from './features/vehicles/hooks/useVehicles';
import { VehicleSelectionDialog } from './features/subscription/components/VehicleSelectionDialog'; import { VehicleSelectionDialog } from './features/subscription/components/VehicleSelectionDialog';
@@ -364,6 +364,22 @@ function App() {
const [selectedVehicle, setSelectedVehicle] = useState<Vehicle | null>(null); const [selectedVehicle, setSelectedVehicle] = useState<Vehicle | null>(null);
const [showAddVehicle, setShowAddVehicle] = useState(false); const [showAddVehicle, setShowAddVehicle] = useState(false);
// Sync browser URL to Zustand screen state on mount (enables direct URL navigation on mobile)
useEffect(() => {
const screen = routeToScreen[window.location.pathname];
if (screen && screen !== activeScreen) {
navigateToScreen(screen, { source: 'url-sync' });
}
}, []); // eslint-disable-line react-hooks/exhaustive-deps -- intentionally runs once on mount
// Sync Zustand screen changes back to browser URL (enables bookmarks and URL sharing)
useEffect(() => {
const targetPath = screenToRoute[activeScreen];
if (targetPath && window.location.pathname !== targetPath) {
window.history.replaceState(null, '', targetPath);
}
}, [activeScreen]);
// Update mobile mode on window resize // Update mobile mode on window resize
useEffect(() => { useEffect(() => {
const checkMobileMode = () => { const checkMobileMode = () => {

View File

@@ -1,5 +1,5 @@
// Export navigation store // Export navigation store
export { useNavigationStore } from './navigation'; export { useNavigationStore, routeToScreen, screenToRoute } from './navigation';
export type { MobileScreen, VehicleSubScreen } from './navigation'; export type { MobileScreen, VehicleSubScreen } from './navigation';
// Export user store // Export user store

View File

@@ -5,6 +5,45 @@ import { safeStorage } from '../utils/safe-storage';
export type MobileScreen = 'Dashboard' | 'Vehicles' | 'Log Fuel' | 'Maintenance' | 'Stations' | 'Documents' | 'Settings' | 'Security' | 'Subscription' | 'AdminUsers' | 'AdminCatalog' | 'AdminCommunityStations' | 'AdminEmailTemplates' | 'AdminBackup' | 'AdminLogs'; export type MobileScreen = 'Dashboard' | 'Vehicles' | 'Log Fuel' | 'Maintenance' | 'Stations' | 'Documents' | 'Settings' | 'Security' | 'Subscription' | 'AdminUsers' | 'AdminCatalog' | 'AdminCommunityStations' | 'AdminEmailTemplates' | 'AdminBackup' | 'AdminLogs';
export type VehicleSubScreen = 'list' | 'detail' | 'add' | 'edit'; export type VehicleSubScreen = 'list' | 'detail' | 'add' | 'edit';
/** Maps browser URL paths to mobile screen names for direct URL navigation */
export const routeToScreen: Record<string, MobileScreen> = {
'/garage': 'Dashboard',
'/garage/dashboard': 'Dashboard',
'/garage/vehicles': 'Vehicles',
'/garage/fuel-logs': 'Log Fuel',
'/garage/maintenance': 'Maintenance',
'/garage/stations': 'Stations',
'/garage/documents': 'Documents',
'/garage/settings': 'Settings',
'/garage/settings/security': 'Security',
'/garage/settings/subscription': 'Subscription',
'/garage/settings/admin/users': 'AdminUsers',
'/garage/settings/admin/catalog': 'AdminCatalog',
'/garage/settings/admin/community-stations': 'AdminCommunityStations',
'/garage/settings/admin/email-templates': 'AdminEmailTemplates',
'/garage/settings/admin/backup': 'AdminBackup',
'/garage/settings/admin/logs': 'AdminLogs',
};
/** Reverse mapping: mobile screen name to canonical URL path */
export const screenToRoute: Record<MobileScreen, string> = {
'Dashboard': '/garage/dashboard',
'Vehicles': '/garage/vehicles',
'Log Fuel': '/garage/fuel-logs',
'Maintenance': '/garage/maintenance',
'Stations': '/garage/stations',
'Documents': '/garage/documents',
'Settings': '/garage/settings',
'Security': '/garage/settings/security',
'Subscription': '/garage/settings/subscription',
'AdminUsers': '/garage/settings/admin/users',
'AdminCatalog': '/garage/settings/admin/catalog',
'AdminCommunityStations': '/garage/settings/admin/community-stations',
'AdminEmailTemplates': '/garage/settings/admin/email-templates',
'AdminBackup': '/garage/settings/admin/backup',
'AdminLogs': '/garage/settings/admin/logs',
};
interface NavigationHistory { interface NavigationHistory {
screen: MobileScreen; screen: MobileScreen;
vehicleSubScreen?: VehicleSubScreen; vehicleSubScreen?: VehicleSubScreen;
@@ -196,7 +235,6 @@ export const useNavigationStore = create<NavigationState>()(
name: 'motovaultpro-mobile-navigation', name: 'motovaultpro-mobile-navigation',
storage: createJSONStorage(() => safeStorage), storage: createJSONStorage(() => safeStorage),
partialize: (state) => ({ partialize: (state) => ({
activeScreen: state.activeScreen,
vehicleSubScreen: state.vehicleSubScreen, vehicleSubScreen: state.vehicleSubScreen,
selectedVehicleId: state.selectedVehicleId, selectedVehicleId: state.selectedVehicleId,
formStates: state.formStates, formStates: state.formStates,