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:
parent
e2fd9549f7
commit
f1776df710
2 changed files with 100 additions and 2 deletions
|
|
@ -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.
|
||||
|
|
|
|||
36
frontend/utils/urlState.ts
Normal file
36
frontend/utils/urlState.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue