An in-browser wisp.place site explorer
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}