diff --git a/pwa/index.html b/pwa/index.html index 876bde3..94935d1 100644 --- a/pwa/index.html +++ b/pwa/index.html @@ -6,14 +6,11 @@ + content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://*.tile.openstreetmap.org; connect-src 'self' https://*.tile.openstreetmap.org; font-src 'self'; worker-src 'self'" /> Tilfluktsrom -
diff --git a/pwa/src/i18n/en.ts b/pwa/src/i18n/en.ts index 8120a8f..ef2bd57 100644 --- a/pwa/src/i18n/en.ts +++ b/pwa/src/i18n/en.ts @@ -71,4 +71,6 @@ export const en: Record = { about_open_source: 'Open source — kode.naiv.no/olemd/tilfluktsrom', action_about: 'About', action_close: 'Close', + action_clear_cache: 'Clear cached data', + cache_cleared: 'All cached data cleared', }; diff --git a/pwa/src/i18n/nb.ts b/pwa/src/i18n/nb.ts index 2cb9b40..ea43e2f 100644 --- a/pwa/src/i18n/nb.ts +++ b/pwa/src/i18n/nb.ts @@ -66,4 +66,6 @@ export const nb: Record = { about_open_source: 'Åpen kildekode — kode.naiv.no/olemd/tilfluktsrom', action_about: 'Om', action_close: 'Lukk', + action_clear_cache: 'Slett lagrede data', + cache_cleared: 'Alle lagrede data slettet', }; diff --git a/pwa/src/i18n/nn.ts b/pwa/src/i18n/nn.ts index b0cc328..55832e1 100644 --- a/pwa/src/i18n/nn.ts +++ b/pwa/src/i18n/nn.ts @@ -66,4 +66,6 @@ export const nn: Record = { about_open_source: 'Open kjeldekode — kode.naiv.no/olemd/tilfluktsrom', action_about: 'Om', action_close: 'Lukk', + action_clear_cache: 'Slett lagra data', + cache_cleared: 'Alle lagra data sletta', }; diff --git a/pwa/src/styles/main.css b/pwa/src/styles/main.css index 47d9768..17dd2f8 100644 --- a/pwa/src/styles/main.css +++ b/pwa/src/styles/main.css @@ -458,6 +458,24 @@ html, body { margin-bottom: 2px; } +.about-clear-btn { + display: block; + margin: 12px 0 0; + padding: 8px 16px; + background: transparent; + border: 1px solid #90A4AE; + border-radius: 6px; + color: #90A4AE; + font-size: 13px; + cursor: pointer; +} + +.about-clear-btn:disabled { + border-color: #4a6a5a; + color: #4a6a5a; + cursor: default; +} + .about-close-btn { display: block; margin: 16px auto 0; diff --git a/pwa/src/ui/about-dialog.ts b/pwa/src/ui/about-dialog.ts index 19c5d08..6788df3 100644 --- a/pwa/src/ui/about-dialog.ts +++ b/pwa/src/ui/about-dialog.ts @@ -1,5 +1,6 @@ /** * About dialog: app info, privacy statement, data sources, copyright. + * Includes a "Clear cached data" button that removes all local storage. * Opens as a modal overlay, same pattern as loading-overlay. */ @@ -35,6 +36,13 @@ export function showAbout(): void { content.appendChild(subheading(t('about_stored_title'))); content.appendChild(para(t('about_stored_body'))); + // Clear cache button + const clearBtn = document.createElement('button'); + clearBtn.className = 'about-clear-btn'; + clearBtn.textContent = t('action_clear_cache'); + clearBtn.addEventListener('click', () => clearAllData(clearBtn)); + content.appendChild(clearBtn); + const footer = document.createElement('div'); footer.className = 'about-footer'; footer.appendChild(small(t('about_copyright'))); @@ -63,6 +71,28 @@ export function hideAbout(): void { previousFocus = null; } +/** Clear all cached data: IndexedDB, localStorage, and service worker caches. */ +async function clearAllData(btn: HTMLButtonElement): Promise { + btn.disabled = true; + + // Clear localStorage (map cache metadata) + localStorage.clear(); + + // Clear IndexedDB (shelter database) + const dbs = await indexedDB.databases?.() ?? []; + for (const db of dbs) { + if (db.name) indexedDB.deleteDatabase(db.name); + } + + // Clear service worker caches (map tiles, precache) + const cacheNames = await caches.keys(); + for (const name of cacheNames) { + await caches.delete(name); + } + + btn.textContent = t('cache_cleared'); +} + function heading(text: string): HTMLElement { const el = document.createElement('h2'); el.textContent = text; diff --git a/pwa/src/ui/map-view.ts b/pwa/src/ui/map-view.ts index 52ee6ba..a7d99e1 100644 --- a/pwa/src/ui/map-view.ts +++ b/pwa/src/ui/map-view.ts @@ -6,16 +6,19 @@ */ import L from 'leaflet'; +import markerIcon from 'leaflet/dist/images/marker-icon.png'; +import markerIcon2x from 'leaflet/dist/images/marker-icon-2x.png'; +import markerShadow from 'leaflet/dist/images/marker-shadow.png'; import type { Shelter, ShelterWithDistance, LatLon } from '../types'; import { t } from '../i18n/i18n'; -// Fix Leaflet default icon paths (broken by bundlers) +// Fix Leaflet default icon paths (broken by bundlers) — use bundled assets // eslint-disable-next-line @typescript-eslint/no-explicit-any delete (L.Icon.Default.prototype as any)._getIconUrl; L.Icon.Default.mergeOptions({ - iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png', - iconRetinaUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png', - shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png', + iconUrl: markerIcon, + iconRetinaUrl: markerIcon2x, + shadowUrl: markerShadow, }); const DEFAULT_ZOOM = 14;