feat(pwa): installable app + fix research tree pip overflow
Research tree UI: - The level pips shared the header row with the upgrade name and fought for width; long names like "Festungsmauern" (5 pips) pushed the pips out of the upgrade card. Pips now sit in the bottom row next to the buy button (space-between), where width is plentiful, and carry a "level / max" tooltip - Tighter modal padding on phones (max-width 560px), scrollable at 92vh PWA (installable as app in mobile browsers): - public/manifest.webmanifest: standalone display, any orientation, TRXTD theme colors, German lang, start_url "/" - public/sw.js: network-first service worker with runtime caching. index.html is always fetched fresh so new deploys are picked up immediately; the cache only serves as offline fallback. API calls and cross-origin requests are never intercepted - Icon set generated by scripts/generate-icons.mjs (pure Node PNG encoder, zero image dependencies): gold tower on the game-themed dark gradient in 192px, 512px, maskable 512px (motif inside the safe zone) and 180px apple-touch-icon - index.html: manifest link, favicon, apple-touch-icon, theme-color, mobile-web-app-capable, apple-mobile-web-app meta tags - src/main.ts: service worker registration in production builds only (dev HMR stays untouched) - server.mjs: serve .webmanifest as application/manifest+json (Chrome requires the correct MIME type for installability) Verified against the production server: manifest (application/ manifest+json), icons (image/png) and sw.js (text/javascript) respond with 200, index.html references all PWA tags; npm test 48/48 green.
This commit is contained in:
parent
914af42210
commit
31cb594cb0
12 changed files with 296 additions and 13 deletions
BIN
public/icons/apple-touch-icon.png
Normal file
BIN
public/icons/apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
public/icons/icon-192.png
Normal file
BIN
public/icons/icon-192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
public/icons/icon-512.png
Normal file
BIN
public/icons/icon-512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
BIN
public/icons/icon-maskable-512.png
Normal file
BIN
public/icons/icon-maskable-512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
32
public/manifest.webmanifest
Normal file
32
public/manifest.webmanifest
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"name": "TRXTD – Tower Defense",
|
||||
"short_name": "TRXTD",
|
||||
"description": "Tower Defense im Browser – verteidige deine Basis gegen 20 Wellen!",
|
||||
"lang": "de",
|
||||
"start_url": "/",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"orientation": "any",
|
||||
"background_color": "#0e131a",
|
||||
"theme_color": "#0e131a",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icons/icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon-maskable-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
]
|
||||
}
|
||||
47
public/sw.js
Normal file
47
public/sw.js
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
* TRXTD service worker – makes the game installable as an app (PWA) and
|
||||
* keeps it playable offline.
|
||||
*
|
||||
* Strategy: network-first with runtime caching. The deployed build uses
|
||||
* hashed asset names, so index.html must always come from the network to
|
||||
* pick up new deploys; the cache is only a fallback when offline.
|
||||
* API calls and WebSocket upgrades are never intercepted.
|
||||
*/
|
||||
const CACHE = 'trxtd-v1'
|
||||
|
||||
self.addEventListener('install', () => {
|
||||
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 req = event.request
|
||||
if (req.method !== 'GET') return
|
||||
|
||||
const url = new URL(req.url)
|
||||
if (url.origin !== self.location.origin) return // CDN/external resources
|
||||
if (url.pathname.startsWith('/api/')) return // auth & game API: network only
|
||||
if (url.pathname.startsWith('/ws')) return
|
||||
|
||||
event.respondWith(
|
||||
fetch(req)
|
||||
.then((res) => {
|
||||
if (res && res.status === 200 && res.type === 'basic') {
|
||||
const copy = res.clone()
|
||||
caches.open(CACHE).then((c) => c.put(req, copy)).catch(() => {})
|
||||
}
|
||||
return res
|
||||
})
|
||||
.catch(() =>
|
||||
caches.match(req).then((hit) => hit || caches.match('/')),
|
||||
),
|
||||
)
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue