diff --git a/frontend/App.tsx b/frontend/App.tsx index 8862eaa..13b0a35 100755 --- a/frontend/App.tsx +++ b/frontend/App.tsx @@ -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('Home'); - const [selectedCampaign, setSelectedCampaign] = useState(null); + // Get initial state from URL + const initialUrlState = parseUrlState(); + + const [currentView, setCurrentView] = useState(initialUrlState.view); + const [selectedCampaign, setSelectedCampaign] = useState(initialUrlState.campaignName); const [selectedProof, setSelectedProof] = useState(null); + const [pendingProofId, setPendingProofId] = useState(initialUrlState.proofId); const [error, setError] = useState(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. diff --git a/frontend/utils/urlState.ts b/frontend/utils/urlState.ts new file mode 100644 index 0000000..f86a7cf --- /dev/null +++ b/frontend/utils/urlState.ts @@ -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): 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): 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); + } +}