Add PWA support (manifest, service worker, icons, safe-area CSS)
This commit is contained in:
1 parent
0a2616c5d6
commit
02f49e55f8
5 files changed
+145
-2
No files matched your search
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<!-- Solid background (fills corners for maskable safe zone) -->
|
||||
<rect width="512" height="512" fill="#1a1a1a"/>
|
||||
<!-- Sun -->
|
||||
<circle cx="205" cy="205" r="70" fill="#e07848"/>
|
||||
<!-- Cloud bumps (front, overlapping sun) -->
|
||||
<circle cx="215" cy="325" r="72" fill="#ece7dc"/>
|
||||
<circle cx="305" cy="297" r="88" fill="#ece7dc"/>
|
||||
<circle cx="390" cy="328" r="62" fill="#ece7dc"/>
|
||||
<!-- Cloud base (flat bottom) -->
|
||||
<rect x="147" y="335" width="305" height="68" fill="#ece7dc"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 559 B |
+14
-1
@@ -2,8 +2,16 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||
<title>Weather</title>
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<link rel="apple-touch-icon" href="/icons/icon.svg">
|
||||
<meta name="theme-color" content="#f5f2ec" media="(prefers-color-scheme: light)">
|
||||
<meta name="theme-color" content="#191510" media="(prefers-color-scheme: dark)">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="apple-mobile-web-app-title" content="Weather">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@300;400;500&family=Playfair+Display:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
|
||||
@@ -134,5 +142,10 @@
|
||||
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script src="app.js"></script>
|
||||
<script>
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('/sw.js');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "Weather",
|
||||
"short_name": "Weather",
|
||||
"description": "Self-hosted weather app with animated radar. No API keys required.",
|
||||
"start_url": "/",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#191510",
|
||||
"theme_color": "#1a1a1a",
|
||||
"orientation": "portrait-primary",
|
||||
"categories": ["weather", "utilities"],
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icons/icon.svg",
|
||||
"sizes": "any",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
]
|
||||
}
|
||||
+33
-1
@@ -454,6 +454,38 @@ a:hover { text-decoration: underline; }
|
||||
padding-top: 0.75rem;
|
||||
}
|
||||
|
||||
/* ── PWA: safe area insets (iPhone notch / home bar) ── */
|
||||
@supports (padding: env(safe-area-inset-bottom)) {
|
||||
.app {
|
||||
padding-left: max(1.5rem, calc(1rem + env(safe-area-inset-left)));
|
||||
padding-right: max(1.5rem, calc(1rem + env(safe-area-inset-right)));
|
||||
padding-bottom: max(4rem, calc(2rem + env(safe-area-inset-bottom)));
|
||||
}
|
||||
.radar-map-full {
|
||||
height: calc(100vh - 280px - env(safe-area-inset-top) - env(safe-area-inset-bottom));
|
||||
}
|
||||
}
|
||||
|
||||
/* ── PWA: status bar area when running standalone ── */
|
||||
@media (display-mode: standalone) {
|
||||
.masthead { padding-top: env(safe-area-inset-top); }
|
||||
}
|
||||
|
||||
/* ── Touch-friendly tap targets (min 44 × 44 px) ── */
|
||||
.tab {
|
||||
min-height: 44px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.search-row input,
|
||||
.search-row button { min-height: 44px; }
|
||||
.radar-btn { width: 44px; height: 44px; font-size: 0.8rem; }
|
||||
.tl-tick { min-height: 36px; }
|
||||
|
||||
/* iOS momentum scrolling */
|
||||
.hourly-scroll { -webkit-overflow-scrolling: touch; }
|
||||
|
||||
/* ── Leaflet overrides ── */
|
||||
.leaflet-container { background: #1a1f2e; }
|
||||
.leaflet-control-zoom a {
|
||||
@@ -464,7 +496,7 @@ a:hover { text-decoration: underline; }
|
||||
|
||||
/* ── Responsive ── */
|
||||
@media (max-width: 640px) {
|
||||
.app { padding: 1.25rem 1rem 3rem; }
|
||||
.app { padding: 1.25rem 1rem max(3rem, calc(1.5rem + env(safe-area-inset-bottom, 0px))); }
|
||||
.masthead { grid-template-columns: 1fr 1fr; grid-template-rows: auto auto; }
|
||||
.masthead h1 { font-size: 1.6rem; }
|
||||
.tagline { display: none; }
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
'use strict';
|
||||
|
||||
const CACHE = 'weather-v1';
|
||||
|
||||
// App shell — cached on install for offline use
|
||||
const PRECACHE = [
|
||||
'/',
|
||||
'/app.js',
|
||||
'/style.css',
|
||||
'/manifest.json',
|
||||
'/icons/icon.svg'
|
||||
];
|
||||
|
||||
self.addEventListener('install', event => {
|
||||
event.waitUntil(
|
||||
caches.open(CACHE)
|
||||
.then(cache => cache.addAll(PRECACHE))
|
||||
.then(() => self.skipWaiting())
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('activate', event => {
|
||||
event.waitUntil(
|
||||
caches.keys()
|
||||
.then(keys => Promise.all(
|
||||
keys.filter(k => k !== CACHE).map(k => caches.delete(k))
|
||||
))
|
||||
.then(() => self.clients.claim())
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', event => {
|
||||
const { request } = event;
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Skip non-GET, API calls, and map/radar tile requests
|
||||
if (
|
||||
request.method !== 'GET' ||
|
||||
url.pathname.startsWith('/api/') ||
|
||||
url.hostname.includes('rainviewer') ||
|
||||
url.hostname.includes('openstreetmap') ||
|
||||
url.hostname.includes('cartodb') ||
|
||||
url.hostname.includes('basemaps')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.respondWith(
|
||||
caches.match(request).then(cached => {
|
||||
if (cached) return cached;
|
||||
return fetch(request).then(response => {
|
||||
// Only cache successful same-origin and trusted CDN responses
|
||||
if (response.ok && (
|
||||
url.origin === self.location.origin ||
|
||||
url.hostname.includes('googleapis') ||
|
||||
url.hostname.includes('gstatic') ||
|
||||
url.hostname.includes('unpkg.com')
|
||||
)) {
|
||||
const clone = response.clone();
|
||||
caches.open(CACHE).then(cache => cache.put(request, clone));
|
||||
}
|
||||
return response;
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user