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;