From f56931859976b035bba3804c7355f6d1c27b9641 Mon Sep 17 00:00:00 2001 From: YASSERRMD Date: Tue, 19 May 2026 04:01:07 +0400 Subject: [PATCH] pwa: add service worker, manifest.json, install prompt component --- frontend/index.html | 10 ++++++ frontend/public/icon-192.svg | 6 ++++ frontend/public/manifest.json | 22 ++++++++++++ frontend/public/sw.js | 40 +++++++++++++++++++++ frontend/src/App.tsx | 3 ++ frontend/src/components/InstallPrompt.tsx | 43 +++++++++++++++++++++++ 6 files changed, 124 insertions(+) create mode 100644 frontend/public/icon-192.svg create mode 100644 frontend/public/manifest.json create mode 100644 frontend/public/sw.js create mode 100644 frontend/src/components/InstallPrompt.tsx diff --git a/frontend/index.html b/frontend/index.html index cf8d41a..70ecb3a 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -3,10 +3,20 @@ + + + + + Sunpath
+ diff --git a/frontend/public/icon-192.svg b/frontend/public/icon-192.svg new file mode 100644 index 0000000..7dea2e7 --- /dev/null +++ b/frontend/public/icon-192.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/public/manifest.json b/frontend/public/manifest.json new file mode 100644 index 0000000..a081fbc --- /dev/null +++ b/frontend/public/manifest.json @@ -0,0 +1,22 @@ +{ + "name": "Sunpath — Solar Exposure Analyser", + "short_name": "Sunpath", + "description": "Analyse solar exposure and shadows for any point on Earth.", + "start_url": "/", + "display": "standalone", + "background_color": "#1a1a2e", + "theme_color": "#f39c12", + "icons": [ + { + "src": "/icon-192.svg", + "sizes": "192x192", + "type": "image/svg+xml" + }, + { + "src": "/icon-192.svg", + "sizes": "512x512", + "type": "image/svg+xml", + "purpose": "any maskable" + } + ] +} diff --git a/frontend/public/sw.js b/frontend/public/sw.js new file mode 100644 index 0000000..0bc3c66 --- /dev/null +++ b/frontend/public/sw.js @@ -0,0 +1,40 @@ +const CACHE = 'sunpath-v1' + +const PRECACHE_URLS = [ + '/', + '/index.html', + '/manifest.json', +] + +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(CACHE).then((cache) => cache.addAll(PRECACHE_URLS)) + ) +}) + +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then((names) => + Promise.all(names.filter((n) => n !== CACHE).map((n) => caches.delete(n))) + ) + ) +}) + +self.addEventListener('fetch', (event) => { + if (event.request.url.includes('/api/')) { + return + } + + event.respondWith( + caches.match(event.request).then((cached) => { + const fetched = fetch(event.request).then((response) => { + if (response.ok && response.type === 'basic') { + const copy = response.clone() + caches.open(CACHE).then((cache) => cache.put(event.request, copy)) + } + return response + }).catch(() => cached) + return cached || fetched + }) + ) +}) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8db2cce..4c3dc66 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -10,6 +10,7 @@ import EmbedPanel from './components/EmbedPanel' import ProjectsPanel from './components/ProjectsPanel' import ComparisonPanel from './components/ComparisonPanel' import TimeSlider from './components/TimeSlider' +import InstallPrompt from './components/InstallPrompt' import SunIndicator from './components/SunIndicator' import { fetchHorizon, fetchBuildings, fetchGrid } from './lib/api' import type { GridCell, HorizonProfile } from './lib/api' @@ -225,6 +226,7 @@ function App() { {summary} )} + ) } @@ -417,6 +419,7 @@ function App() { + ) } diff --git a/frontend/src/components/InstallPrompt.tsx b/frontend/src/components/InstallPrompt.tsx new file mode 100644 index 0000000..34813b0 --- /dev/null +++ b/frontend/src/components/InstallPrompt.tsx @@ -0,0 +1,43 @@ +import { useState, useEffect } from 'react' + +export default function InstallPrompt() { + const [deferredPrompt, setDeferredPrompt] = useState(null) + const [show, setShow] = useState(false) + + useEffect(() => { + const handler = (e: Event) => { + e.preventDefault() + setDeferredPrompt(e) + setShow(true) + } + window.addEventListener('beforeinstallprompt', handler) + return () => window.removeEventListener('beforeinstallprompt', handler) + }, []) + + const install = () => { + if (!deferredPrompt) return + ;(deferredPrompt as any).prompt() + ;(deferredPrompt as any).userChoice.then(() => { + setDeferredPrompt(null) + setShow(false) + }) + } + + if (!show) return null + + return ( +
+
+
+ Install Sunpath for offline access +
+ + +
+
+ ) +}