Persist navigation state in URL for browser refresh support

- Add URL utility functions for parsing and building URL state
- Initialize app state from URL parameters on page load
- Sync navigation changes to URL via browser history API
- Handle browser back/forward navigation with popstate listener
- Support deep linking to campaigns and proofs

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
michael 2026-01-24 09:04:04 -06:00
parent e2fd9549f7
commit f1776df710
2 changed files with 100 additions and 2 deletions

View file

@ -4,6 +4,7 @@ import { useIsAuthenticated, useMsal } from '@azure/msal-react';
import { InteractionStatus } from '@azure/msal-browser';
import { Hero } from './components/Hero';
import { analyzeProof } from './services/geminiService';
import { parseUrlState, pushUrlState } from './utils/urlState';
import { getUserInfo } from './services/authService';
import apiService from './services/apiService';
import type { AgentReview, AgentName, FlaggedItem, ResolvedItem, ErrorItem } from './types';
@ -34,9 +35,13 @@ const App: React.FC = () => {
const isAuthenticated = useIsAuthenticated();
const { instance: msalInstance, inProgress } = useMsal();
const [currentView, setCurrentView] = useState<View>('Home');
const [selectedCampaign, setSelectedCampaign] = useState<string | null>(null);
// Get initial state from URL
const initialUrlState = parseUrlState();
const [currentView, setCurrentView] = useState<View>(initialUrlState.view);
const [selectedCampaign, setSelectedCampaign] = useState<string | null>(initialUrlState.campaignName);
const [selectedProof, setSelectedProof] = useState<any | null>(null);
const [pendingProofId, setPendingProofId] = useState<string | null>(initialUrlState.proofId);
const [error, setError] = useState<string | null>(null);
const [isLoadingData, setIsLoadingData] = useState(true);
@ -138,6 +143,63 @@ const App: React.FC = () => {
loadAuditItems();
}, [isAuthenticated]);
// Sync state changes to URL
useEffect(() => {
pushUrlState({
view: currentView,
campaignName: selectedCampaign,
proofId: selectedProof?._id || null,
});
}, [currentView, selectedCampaign, selectedProof]);
// Handle browser back/forward
useEffect(() => {
const handlePopState = () => {
const state = parseUrlState();
setCurrentView(state.view);
setSelectedCampaign(state.campaignName);
if (!state.proofId) setSelectedProof(null);
setPendingProofId(state.proofId);
};
window.addEventListener('popstate', handlePopState);
return () => window.removeEventListener('popstate', handlePopState);
}, []);
// Load proofs for campaign selected from URL (deep linking support)
useEffect(() => {
const loadProofsForUrlCampaign = async () => {
if (!selectedCampaign || !isAuthenticated || campaigns.length === 0) return;
// Skip if proofs already loaded for this campaign
if (campaignProofs[selectedCampaign]) return;
const campaign = campaigns.find(c => c.name === selectedCampaign);
if (!campaign?._id) return;
try {
const proofs = await apiService.getProofs(campaign._id);
setCampaignProofs(prev => ({
...prev,
[selectedCampaign]: proofs.map(p => apiService.convertProofToFrontend(p))
}));
} catch (error) {
console.error('Failed to load proofs for URL campaign:', selectedCampaign, error);
}
};
loadProofsForUrlCampaign();
}, [selectedCampaign, isAuthenticated, campaigns, campaignProofs]);
// Restore proof selection from URL after proofs are loaded
useEffect(() => {
if (pendingProofId && selectedCampaign && campaignProofs[selectedCampaign]) {
const proof = campaignProofs[selectedCampaign].find(p => p._id === pendingProofId);
if (proof) {
setSelectedProof(proof);
}
setPendingProofId(null);
}
}, [pendingProofId, selectedCampaign, campaignProofs]);
useEffect(() => {
// Keep selectedProof in sync with the master list in campaignProofs.
// This ensures that when a new version is added, the detail view refreshes.

View file

@ -0,0 +1,36 @@
type View = 'Home' | 'Analytics' | 'Campaigns' | 'WIP Reviewer' | 'CopyGenAI' | 'Settings' | 'Profile' | 'Auditing';
export interface UrlNavigationState {
view: View;
campaignName: string | null;
proofId: string | null;
}
const VALID_VIEWS: View[] = ['Home', 'Analytics', 'Campaigns', 'WIP Reviewer', 'CopyGenAI', 'Settings', 'Profile', 'Auditing'];
export function parseUrlState(): UrlNavigationState {
const params = new URLSearchParams(window.location.search);
const view = params.get('view') as View;
return {
view: VALID_VIEWS.includes(view) ? view : 'Home',
campaignName: params.get('campaign'),
proofId: params.get('proof'),
};
}
export function buildUrl(state: Partial<UrlNavigationState>): string {
const params = new URLSearchParams();
if (state.view && state.view !== 'Home') params.set('view', state.view);
if (state.campaignName) params.set('campaign', state.campaignName);
if (state.proofId) params.set('proof', state.proofId);
const query = params.toString();
return query ? `?${query}` : '/';
}
export function pushUrlState(state: Partial<UrlNavigationState>): void {
const newUrl = buildUrl(state);
// Only push if URL actually changed to avoid duplicate history entries
if (newUrl !== window.location.pathname + window.location.search) {
window.history.pushState(state, '', newUrl);
}
}