implement PWA support

Hunter
Friday, July 11th, 2025 at 3:57 PM

images/rollerskate.pngnew
index.html+14 
manifest.jsonnew
sw.jsnew

images/rollerskate.png
Binary files differ.
index.html
  5  	<meta name="viewport" content="width=device-width, initial-scale=1.0">
  6  	<title>Web Workshop</title>
  7  	<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🛼</text></svg>">
  8 +	<link rel="manifest" href="manifest.json">
  9  	<script type="module">
 10  		import {EditorView, keymap, placeholder} from "https://esm.sh/@codemirror/view@6"
 11  		import {EditorState} from "https://esm.sh/@codemirror/state@6"

290  
291  		// Initialize when page loads
292  		initializeCodeMirror();
293 +
294 +		// Register service worker
295 +		if ('serviceWorker' in navigator) {
296 +			window.addEventListener('load', () => {
297 +				navigator.serviceWorker.register('/sw.js')
298 +					.then(registration => {
299 +						console.log('SW registered: ', registration);
300 +					})
301 +					.catch(registrationError => {
302 +						console.log('SW registration failed: ', registrationError);
303 +					});
304 +			});
305 +		}
306  	</script>
307  </body>
308  </html>
manifest.json
 1 +{
 2 +  "name": "Web Workshop",
 3 +  "short_name": "WW",
 4 +  "description": "HTML editor and live preview tool",
 5 +  "start_url": "/",
 6 +  "display": "standalone",
 7 +  "background_color": "#16181b",
 8 +  "theme_color": "#16181b",
 9 +  "orientation": "any",
10 +  "icons": [
11 +    {
12 +      "src": "images/rollerskate.png",
13 +      "sizes": "160x160",
14 +      "type": "image/png"
15 +    },
16 +    {
17 +      "src": "images/rollerskate.png",
18 +      "sizes": "192x192",
19 +      "type": "image/png"
20 +    }
21 +  ]
22 +}
sw.js
 1 +const CACHE_NAME = 'web-workshop-v1';
 2 +const urlsToCache = [
 3 +  '/',
 4 +  '/index.html',
 5 +  '/manifest.json'
 6 +];
 7 +
 8 +// Function to discover and cache all files in directories
 9 +async function cacheDirectoryFiles(cache, directories) {
10 +  for (const dir of directories) {
11 +    try {
12 +      // Try to fetch directory listing
13 +      const response = await fetch(dir);
14 +      if (response.ok) {
15 +        const html = await response.text();
16 +        // Extract file links from directory listing
17 +        const links = html.match(/href="([^"]*\.(png|jpg|jpeg|gif|svg|webp|html|css|js|json|txt|md))"/gi);
18 +        if (links) {
19 +          const files = links.map(link => {
20 +            const match = link.match(/href="([^"]*)"/);
21 +            return match ? dir + match[1] : null;
22 +          }).filter(Boolean);
23 +          
24 +          // Cache each file
25 +          for (const file of files) {
26 +            try {
27 +              await cache.add(file);
28 +            } catch (e) {
29 +              console.log(`Failed to cache ${file}:`, e);
30 +            }
31 +          }
32 +        }
33 +      }
34 +    } catch (e) {
35 +      console.log(`Failed to cache directory ${dir}:`, e);
36 +    }
37 +  }
38 +}
39 +
40 +// Install service worker and cache resources
41 +self.addEventListener('install', event => {
42 +  event.waitUntil(
43 +    caches.open(CACHE_NAME)
44 +      .then(async cache => {
45 +        // Cache core files
46 +        await cache.addAll(urlsToCache);
47 +        // Cache directory files
48 +        await cacheDirectoryFiles(cache, ['/images/', '/pages/']);
49 +      })
50 +      .then(() => self.skipWaiting())
51 +  );
52 +});
53 +
54 +// Activate service worker and clean up old caches
55 +self.addEventListener('activate', event => {
56 +  event.waitUntil(
57 +    caches.keys().then(cacheNames => {
58 +      return Promise.all(
59 +        cacheNames.map(cacheName => {
60 +          if (cacheName !== CACHE_NAME) {
61 +            return caches.delete(cacheName);
62 +          }
63 +        })
64 +      );
65 +    }).then(() => self.clients.claim())
66 +  );
67 +});
68 +
69 +// Fetch strategy: Network first, then cache (for updates when online)
70 +self.addEventListener('fetch', event => {
71 +  event.respondWith(
72 +    fetch(event.request)
73 +      .then(response => {
74 +        // If we got a response, add it to the cache
75 +        if (response.status === 200) {
76 +          const responseClone = response.clone();
77 +          caches.open(CACHE_NAME)
78 +            .then(cache => cache.put(event.request, responseClone));
79 +        }
80 +        return response;
81 +      })
82 +      .catch(() => {
83 +        // Network failed, try cache
84 +        return caches.match(event.request)
85 +          .then(response => {
86 +            if (response) {
87 +              return response;
88 +            }
89 +            // If not in cache and it's a navigation request, serve index.html
90 +            if (event.request.mode === 'navigate') {
91 +              return caches.match('/index.html');
92 +            }
93 +            throw new Error('No cached version available');
94 +          });
95 +      })
96 +  );
97 +});