Skip to content

Commit 9096191

Browse files
committed
feat: implement official PWA Service Worker for offline support and app badging
1 parent 2dd11a5 commit 9096191

3 files changed

Lines changed: 78 additions & 0 deletions

File tree

web/public/sw.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
const CACHE_NAME = 'slr-cache-v1';
2+
3+
// Assets to cache immediately
4+
const PRECACHE_ASSETS = [
5+
'/',
6+
'/manifest.webmanifest',
7+
'/icon-192.png',
8+
'/icon-512.png',
9+
];
10+
11+
self.addEventListener('install', (event) => {
12+
event.waitUntil(
13+
caches.open(CACHE_NAME).then((cache) => {
14+
return cache.addAll(PRECACHE_ASSETS);
15+
})
16+
);
17+
self.skipWaiting();
18+
});
19+
20+
self.addEventListener('activate', (event) => {
21+
event.waitUntil(
22+
caches.keys().then((cacheNames) => {
23+
return Promise.all(
24+
cacheNames.map((cacheName) => {
25+
if (cacheName !== CACHE_NAME) {
26+
return caches.delete(cacheName);
27+
}
28+
})
29+
);
30+
})
31+
);
32+
self.clients.claim();
33+
});
34+
35+
self.addEventListener('fetch', (event) => {
36+
// Skip cross-origin requests (like Clerk or Neon)
37+
if (!event.request.url.startsWith(self.location.origin)) return;
38+
39+
// Stale-while-revalidate strategy
40+
event.respondWith(
41+
caches.open(CACHE_NAME).then((cache) => {
42+
return cache.match(event.request).then((response) => {
43+
const fetchPromise = fetch(event.request).then((networkResponse) => {
44+
// Cache the new response if it's a valid GET request
45+
if (event.request.method === 'GET' && networkResponse.status === 200) {
46+
cache.put(event.request, networkResponse.clone());
47+
}
48+
return networkResponse;
49+
});
50+
return response || fetchPromise;
51+
});
52+
})
53+
);
54+
});

web/src/app/layout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import Layout from "@/components/layout/Layout";
44
import { LanguageProvider } from "@/context/LanguageContext";
55
import "./globals.css";
66
import UpdateDetector from "@/components/common/UpdateDetector";
7+
import ServiceWorkerRegistration from "@/components/common/ServiceWorkerRegistration";
78
import { Analytics } from "@vercel/analytics/next";
89
import { SpeedInsights } from "@vercel/speed-insights/next";
910

@@ -122,6 +123,7 @@ export default function RootLayout({
122123
<LanguageProvider>
123124
{children}
124125
<UpdateDetector />
126+
<ServiceWorkerRegistration />
125127
<Analytics />
126128
<SpeedInsights />
127129
</LanguageProvider>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
'use client';
2+
3+
import { useEffect } from 'react';
4+
5+
export default function ServiceWorkerRegistration() {
6+
useEffect(() => {
7+
if ('serviceWorker' in navigator && window.location.hostname !== 'localhost') {
8+
window.addEventListener('load', () => {
9+
navigator.serviceWorker
10+
.register('/sw.js')
11+
.then((registration) => {
12+
console.log('SW registered: ', registration);
13+
})
14+
.catch((registrationError) => {
15+
console.log('SW registration failed: ', registrationError);
16+
});
17+
});
18+
}
19+
}, []);
20+
21+
return null;
22+
}

0 commit comments

Comments
 (0)