/** * Main App component * * Entry point for the Wisp Client application. * * Routes: * - /: Resolver UI (landing page) * - /wisp/{did}/{siteName}/{path}: Handled by service worker - React renders minimal component */ import { useState, useEffect } from 'react'; import { Routes, Route, Navigate, useLocation } from 'react-router-dom'; import { ResolverUI, SiteRendererSW, ServiceWorkerDebug } from './components'; import { useATProtoResolver } from './hooks/useATProtoResolver'; import { useSitesFetcher, useManifestFetcherManual } from './hooks/useManifestFetcher'; function ResolverWrapper() { const location = useLocation(); const [handle, setHandle] = useState(''); const [siteRkey, setSiteRkey] = useState(''); const [siteName, setSiteName] = useState(''); const [loading, setLoading] = useState<'idle' | 'resolving' | 'fetching'>('idle'); const [error, setError] = useState(null); // Check for handle in query params useEffect(() => { const params = new URLSearchParams(location.search); const handleParam = params.get('handle'); if (handleParam) setHandle(handleParam); }, [location.search]); // Resolve handle when provided const resolver = useATProtoResolver(handle || null); // Fetch sites list when resolved (for validation) useSitesFetcher( resolver.data?.pdsUrl || null, resolver.data?.did || null ); // Fetch manifest when site is selected const manifestState = useManifestFetcherManual( resolver.data?.pdsUrl || null, resolver.data?.did || null, siteRkey || undefined ); // Handle loading a site const handleLoad = async (loadedHandle: string, loadedSiteRkey: string, loadedSiteName: string) => { setHandle(loadedHandle); setSiteRkey(loadedSiteRkey); setSiteName(loadedSiteName); setLoading('resolving'); // The resolver and manifest fetchers will automatically trigger // We'll wait for them and then navigate }; // Handle navigate to wisp useEffect(() => { if (loading === 'resolving' && resolver.data && !resolver.loading) { if (resolver.error) { setError(resolver.error); setLoading('idle'); return; } setLoading('fetching'); } if (loading === 'fetching' && manifestState.data && !manifestState.loading) { if (manifestState.error) { setError(manifestState.error); setLoading('idle'); return; } // Success - trigger navigation via SiteRendererSW } }, [loading, resolver, manifestState]); // Handle back/cancel const handleBack = () => { setHandle(''); setSiteRkey(''); setSiteName(''); setLoading('idle'); setError(null); }; // Show resolver UI if (!handle || loading === 'idle') { return ( ); } // Show loading state if (resolver.loading || manifestState.loading) { const stage = resolver.loading ? 'resolving' : 'fetchingManifest'; const message = resolver.loading ? `Resolving ${handle}...` : manifestState.loading ? 'Fetching site manifest...' : 'Loading...'; return (
{stage === 'resolving' && (

{message}

)} {stage === 'fetchingManifest' && (

{message}

{manifestState.recordCount !== undefined && (

Found {manifestState.recordCount} files

)}
)}
); } // Show error state if (error || resolver.error || manifestState.error) { const errorMessage = error || resolver.error || manifestState.error; return (

Error

{errorMessage}

); } // Render the site using service worker if (resolver.data && manifestState.data) { return ( ); } return null; } /** * SiteRouteWrapper - Minimal wrapper for /wisp/* routes * * The service worker handles the actual content serving. * This component renders nothing (or minimal UI) so the service worker * can intercept and serve the site content. */ function SiteRouteWrapper() { const location = useLocation(); useEffect(() => { console.log('[App] Site route:', location.pathname); // Check if service worker has a manifest loaded // If not, the user might have bookmarked a /wisp/ URL // We should redirect to the resolver const checkSW = async () => { if (navigator.serviceWorker && navigator.serviceWorker.controller) { // Send a message to check status const channel = new MessageChannel(); const timeout = setTimeout(() => { channel.port1.close(); // No response, redirect to resolver window.location.href = '/'; }, 1000); channel.port1.onmessage = (event) => { clearTimeout(timeout); channel.port1.close(); if (!event.data.hasManifest) { // No manifest loaded, redirect to resolver console.log('[App] No manifest in SW, redirecting to resolver'); window.location.href = '/'; } }; navigator.serviceWorker.controller.postMessage( { type: 'GET_STATUS' }, [channel.port2] ); } }; checkSW(); }, [location.pathname]); // Render nothing - service worker serves the content return null; } function App() { return ( <> {/* Resolver UI - handles / */} } /> {/* Wisp routes - handled by service worker */} {/* Pattern: /wisp/{did}/{siteName}/* */} } /> {/* Catch-all - redirect to resolver */} } /> {/* Debug component - only shows in development */} ); } export default App;