An in-browser wisp.place site explorer
at main 154 lines 4.9 kB view raw
1/** 2 * SiteRendererSW - Service worker-based site rendering component 3 * 4 * This component uses a service worker to render wisp sites with: 5 * - Clean URLs (/wisp/{did}/{siteName}/path) 6 * - Native CSS and script loading 7 * - Working relative paths 8 * - Proper browser back button 9 */ 10 11import { useState, useEffect } from 'react'; 12import { LoadingState } from './LoadingState'; 13import { ErrorDisplay } from './ErrorDisplay'; 14import { getSWManager } from '../utils/serviceWorker'; 15import type { WispDirectory } from '../types/lexicon'; 16 17export interface SiteRendererSWProps { 18 pdsUrl: string; 19 did: string; 20 handle: string; 21 siteName: string; 22 manifest: WispDirectory; 23 onBack: () => void; 24} 25 26export function SiteRendererSW({ 27 pdsUrl, 28 did, 29 handle, 30 siteName, 31 manifest, 32 onBack, 33}: SiteRendererSWProps) { 34 const [status, setStatus] = useState<'loading' | 'navigating' | 'error'>('loading'); 35 const [error, setError] = useState<string | null>(null); 36 37 useEffect(() => { 38 async function loadSite() { 39 const swManager = getSWManager(); 40 41 try { 42 // Ensure service worker is registered 43 if (!swManager.isReady()) { 44 setStatus('loading'); 45 const registered = await swManager.register(); 46 47 if (!registered) { 48 throw new Error('Failed to register service worker'); 49 } 50 } 51 52 setStatus('navigating'); 53 54 // Wait for the service worker to claim this client 55 const registration = swManager.getRegistration(); 56 if (registration) { 57 await new Promise<void>((resolve) => { 58 if (registration.active && navigator.serviceWorker.controller === registration.active) { 59 console.log('[SiteRendererSW] Service worker is active and controlling'); 60 resolve(); 61 } else { 62 const handler = () => { 63 console.log('[SiteRendererSW] Service worker claimed client'); 64 registration.removeEventListener('controllerchange', handler); 65 resolve(); 66 }; 67 registration.addEventListener('controllerchange', handler); 68 // Timeout after 1 second 69 setTimeout(() => { 70 registration.removeEventListener('controllerchange', handler); 71 resolve(); 72 }, 1000); 73 } 74 }); 75 } 76 77 // Set the manifest in the service worker 78 const manifestSet = await swManager.setManifest(manifest, pdsUrl, did, handle, siteName); 79 80 if (!manifestSet) { 81 throw new Error('Failed to set manifest in service worker'); 82 } 83 84 // Store resolver state for back button 85 const resolverState = { 86 handle, 87 did, 88 pdsUrl, 89 siteName, 90 }; 91 sessionStorage.setItem('wisp_resolver_state', JSON.stringify(resolverState)); 92 93 // Wait for service worker to be controlling the page 94 console.log('[SiteRendererSW] Waiting for service worker control...'); 95 await new Promise<void>((resolve) => { 96 let checks = 0; 97 const maxChecks = 20; // Wait up to 2 seconds 98 99 const checkControl = () => { 100 if (navigator.serviceWorker.controller) { 101 console.log('[SiteRendererSW] Service worker is controlling the page'); 102 resolve(); 103 } else if (checks >= maxChecks) { 104 console.warn('[SiteRendererSW] Service worker not controlling after timeout'); 105 resolve(); 106 } else { 107 checks++; 108 setTimeout(checkControl, 100); 109 } 110 }; 111 112 checkControl(); 113 }); 114 115 // Small additional delay to ensure SW is fully ready 116 await new Promise(resolve => setTimeout(resolve, 200)); 117 118 // Navigate to the wisp site 119 // Don't URL encode - DID and siteName are valid in URL paths 120 const wispPath = `/wisp/${did}/${siteName}/`; 121 console.log('[SiteRendererSW] Navigating to:', wispPath); 122 window.location.href = wispPath; 123 124 // Note: The component will unmount as we navigate away 125 } catch (err) { 126 const errorMessage = err instanceof Error ? err.message : 'Failed to load site'; 127 setError(errorMessage); 128 setStatus('error'); 129 } 130 } 131 132 loadSite(); 133 }, [manifest, pdsUrl, did, handle, siteName]); 134 135 if (status === 'error') { 136 return ( 137 <div className="min-h-screen bg-gray-50 flex items-center justify-center p-4"> 138 <div className="max-w-2xl w-full"> 139 <ErrorDisplay 140 error={error || 'Failed to load site'} 141 onRetry={() => window.location.reload()} 142 onBack={onBack} 143 /> 144 </div> 145 </div> 146 ); 147 } 148 149 const message = status === 'loading' 150 ? 'Initializing service worker...' 151 : 'Loading site...'; 152 153 return <LoadingState stage="loading-site" message={message} />; 154}