29 changed files with 369 additions and 9 deletions
@ -0,0 +1,59 @@ |
|||||||
|
import { Controller } from '@hotwired/stimulus' |
||||||
|
|
||||||
|
export default class extends Controller { |
||||||
|
static targets = ['promptBox'] |
||||||
|
|
||||||
|
connect() { |
||||||
|
this.checkInstallEligibility() |
||||||
|
} |
||||||
|
|
||||||
|
checkInstallEligibility() { |
||||||
|
// Skip if already installed or dismissed
|
||||||
|
if ( |
||||||
|
localStorage.getItem('a2hs_installed') === '1' || |
||||||
|
localStorage.getItem('a2hs_dismissed') === '1' || |
||||||
|
window.matchMedia('(display-mode: standalone)').matches |
||||||
|
) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Track page loads
|
||||||
|
let loadCount = parseInt(localStorage.getItem('a2hs_pageloads') || '0', 10) |
||||||
|
loadCount++ |
||||||
|
localStorage.setItem('a2hs_pageloads', loadCount) |
||||||
|
|
||||||
|
// Listen for install prompt only after threshold
|
||||||
|
if (loadCount >= 5) { |
||||||
|
window.addEventListener('beforeinstallprompt', (e) => { |
||||||
|
e.preventDefault() |
||||||
|
this.deferredPrompt = e |
||||||
|
this.promptBoxTarget.classList.remove('hidden') |
||||||
|
}, { once: true }) // Listen once only
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
install() { |
||||||
|
if (this.deferredPrompt) { |
||||||
|
this.deferredPrompt.prompt() |
||||||
|
this.deferredPrompt.userChoice.then((choiceResult) => { |
||||||
|
if (choiceResult.outcome === 'accepted') { |
||||||
|
localStorage.setItem('a2hs_installed', '1') |
||||||
|
console.log('User accepted the A2HS prompt') |
||||||
|
} else { |
||||||
|
console.log('User dismissed the A2HS prompt') |
||||||
|
} |
||||||
|
this.cleanupPrompt() |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
dismiss() { |
||||||
|
localStorage.setItem('a2hs_dismissed', '1') |
||||||
|
this.cleanupPrompt() |
||||||
|
} |
||||||
|
|
||||||
|
cleanupPrompt() { |
||||||
|
this.promptBoxTarget.classList.add('hidden') |
||||||
|
this.deferredPrompt = null |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,11 @@ |
|||||||
|
import { Controller } from '@hotwired/stimulus' |
||||||
|
|
||||||
|
export default class extends Controller { |
||||||
|
connect() { |
||||||
|
if ('serviceWorker' in navigator) { |
||||||
|
navigator.serviceWorker.register('/service-worker.js') |
||||||
|
.then(reg => console.log('SW registered:', reg)) |
||||||
|
.catch(err => console.error('SW failed:', err)); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 23 KiB |
@ -0,0 +1,21 @@ |
|||||||
|
{ |
||||||
|
"name": "Decent Newsroom", |
||||||
|
"short_name": "Newsroom", |
||||||
|
"icons": [ |
||||||
|
{ |
||||||
|
"src": "/icons/web-app-manifest-192x192.png", |
||||||
|
"sizes": "192x192", |
||||||
|
"type": "image/png", |
||||||
|
"purpose": "maskable" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"src": "/icons/web-app-manifest-512x512.png", |
||||||
|
"sizes": "512x512", |
||||||
|
"type": "image/png", |
||||||
|
"purpose": "maskable" |
||||||
|
} |
||||||
|
], |
||||||
|
"theme_color": "#ffffff", |
||||||
|
"background_color": "#ffffff", |
||||||
|
"display": "standalone" |
||||||
|
} |
||||||
@ -0,0 +1,14 @@ |
|||||||
|
.install-prompt-box { |
||||||
|
position: fixed; |
||||||
|
bottom: 20px; |
||||||
|
left: 20px; |
||||||
|
background: #ffffff; |
||||||
|
border: 1px solid var(--color-border); |
||||||
|
padding: 1rem; |
||||||
|
border-radius: 8px; |
||||||
|
z-index: 1000; |
||||||
|
} |
||||||
|
|
||||||
|
.install-prompt-box.hidden { |
||||||
|
display: none; |
||||||
|
} |
||||||
@ -0,0 +1,80 @@ |
|||||||
|
/* eb-garamond-regular - latin_latin-ext */ |
||||||
|
@font-face { |
||||||
|
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ |
||||||
|
font-family: 'EB Garamond'; |
||||||
|
font-style: normal; |
||||||
|
font-weight: 400; |
||||||
|
src: url('../fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ |
||||||
|
} |
||||||
|
/* eb-garamond-italic - latin_latin-ext */ |
||||||
|
@font-face { |
||||||
|
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ |
||||||
|
font-family: 'EB Garamond'; |
||||||
|
font-style: italic; |
||||||
|
font-weight: 400; |
||||||
|
src: url('../fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ |
||||||
|
} |
||||||
|
/* eb-garamond-500 - latin_latin-ext */ |
||||||
|
@font-face { |
||||||
|
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ |
||||||
|
font-family: 'EB Garamond'; |
||||||
|
font-style: normal; |
||||||
|
font-weight: 500; |
||||||
|
src: url('../fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-500.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ |
||||||
|
} |
||||||
|
/* eb-garamond-500italic - latin_latin-ext */ |
||||||
|
@font-face { |
||||||
|
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ |
||||||
|
font-family: 'EB Garamond'; |
||||||
|
font-style: italic; |
||||||
|
font-weight: 500; |
||||||
|
src: url('../fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-500italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ |
||||||
|
} |
||||||
|
/* eb-garamond-600 - latin_latin-ext */ |
||||||
|
@font-face { |
||||||
|
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ |
||||||
|
font-family: 'EB Garamond'; |
||||||
|
font-style: normal; |
||||||
|
font-weight: 600; |
||||||
|
src: url('../fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-600.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ |
||||||
|
} |
||||||
|
/* eb-garamond-600italic - latin_latin-ext */ |
||||||
|
@font-face { |
||||||
|
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ |
||||||
|
font-family: 'EB Garamond'; |
||||||
|
font-style: italic; |
||||||
|
font-weight: 600; |
||||||
|
src: url('../fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-600italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ |
||||||
|
} |
||||||
|
/* eb-garamond-700 - latin_latin-ext */ |
||||||
|
@font-face { |
||||||
|
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ |
||||||
|
font-family: 'EB Garamond'; |
||||||
|
font-style: normal; |
||||||
|
font-weight: 700; |
||||||
|
src: url('../fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-700.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ |
||||||
|
} |
||||||
|
/* eb-garamond-700italic - latin_latin-ext */ |
||||||
|
@font-face { |
||||||
|
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ |
||||||
|
font-family: 'EB Garamond'; |
||||||
|
font-style: italic; |
||||||
|
font-weight: 700; |
||||||
|
src: url('../fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-700italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ |
||||||
|
} |
||||||
|
/* eb-garamond-800 - latin_latin-ext */ |
||||||
|
@font-face { |
||||||
|
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ |
||||||
|
font-family: 'EB Garamond'; |
||||||
|
font-style: normal; |
||||||
|
font-weight: 800; |
||||||
|
src: url('../fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-800.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ |
||||||
|
} |
||||||
|
/* eb-garamond-800italic - latin_latin-ext */ |
||||||
|
@font-face { |
||||||
|
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ |
||||||
|
font-family: 'EB Garamond'; |
||||||
|
font-style: italic; |
||||||
|
font-weight: 800; |
||||||
|
src: url('../fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-800italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ |
||||||
|
} |
||||||
@ -0,0 +1,28 @@ |
|||||||
|
.spinner { |
||||||
|
display: flex; |
||||||
|
justify-content: center; |
||||||
|
align-items: center; |
||||||
|
margin: 1em 0; |
||||||
|
} |
||||||
|
|
||||||
|
.lds-dual-ring { |
||||||
|
display: inline-block; |
||||||
|
width: 40px; |
||||||
|
height: 40px; |
||||||
|
} |
||||||
|
.lds-dual-ring:after { |
||||||
|
content: " "; |
||||||
|
display: block; |
||||||
|
width: 32px; |
||||||
|
height: 32px; |
||||||
|
margin: 4px; |
||||||
|
border-radius: 50%; |
||||||
|
border: 4px solid var(--color-primary); |
||||||
|
border-color: var(--color-primary) transparent var(--color-primary) transparent; |
||||||
|
animation: lds-dual-ring 1.2s linear infinite; |
||||||
|
} |
||||||
|
|
||||||
|
@keyframes lds-dual-ring { |
||||||
|
0% { transform: rotate(0deg); } |
||||||
|
100% { transform: rotate(360deg); } |
||||||
|
} |
||||||
|
After Width: | Height: | Size: 29 KiB |
@ -0,0 +1,11 @@ |
|||||||
|
<!DOCTYPE html> |
||||||
|
<html lang="en"> |
||||||
|
<head> |
||||||
|
<meta charset="UTF-8" /> |
||||||
|
<title>Decent Newsroom is offline</title> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<h1>You are offline</h1> |
||||||
|
<p>Please reconnect to use the app.</p> |
||||||
|
</body> |
||||||
|
</html> |
||||||
@ -0,0 +1,83 @@ |
|||||||
|
const CACHE_NAME = 'newsroom-pwa-v0.0.1'; |
||||||
|
const URLS_TO_CACHE = [ |
||||||
|
'/offline.html' |
||||||
|
]; |
||||||
|
|
||||||
|
// Install: cache initial assets
|
||||||
|
self.addEventListener('install', (event) => { |
||||||
|
event.waitUntil( |
||||||
|
caches.open(CACHE_NAME).then(async (cache) => { |
||||||
|
const urls = URLS_TO_CACHE.map(async (url) => { |
||||||
|
try { |
||||||
|
const response = await fetch(url); |
||||||
|
if (response.ok && response.type === 'basic') { |
||||||
|
await cache.put(url, response.clone()); |
||||||
|
} else { |
||||||
|
console.warn(`[SW] Skipped caching ${url}: invalid response`); |
||||||
|
} |
||||||
|
} catch (err) { |
||||||
|
console.warn(`[SW] Failed to fetch ${url}:`, err); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
await Promise.all(urls); |
||||||
|
}) |
||||||
|
); |
||||||
|
self.skipWaiting(); |
||||||
|
}); |
||||||
|
|
||||||
|
// Activate: clean up old caches
|
||||||
|
self.addEventListener('activate', (event) => { |
||||||
|
event.waitUntil( |
||||||
|
caches.keys().then((cacheNames) => |
||||||
|
Promise.all( |
||||||
|
cacheNames |
||||||
|
.filter((name) => name !== CACHE_NAME) |
||||||
|
.map((name) => caches.delete(name)) |
||||||
|
) |
||||||
|
) |
||||||
|
); |
||||||
|
self.clients.claim(); |
||||||
|
}); |
||||||
|
|
||||||
|
// Fetch: serve from cache, fallback to network, then offline
|
||||||
|
self.addEventListener('fetch', (event) => { |
||||||
|
const request = event.request; |
||||||
|
|
||||||
|
// Only handle HTTP GET requests
|
||||||
|
if ( |
||||||
|
request.method !== 'GET' || |
||||||
|
!request.url.startsWith('http') |
||||||
|
) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
// Skip cache for dynamic routes
|
||||||
|
const isDynamic = request.url.includes('/cat/') ; |
||||||
|
if (isDynamic) { |
||||||
|
return; // Don't intercept
|
||||||
|
} |
||||||
|
|
||||||
|
event.respondWith( |
||||||
|
caches.match(request).then((cached) => { |
||||||
|
if (cached) return cached; |
||||||
|
|
||||||
|
return fetch(request) |
||||||
|
.then((response) => { |
||||||
|
// Optionally cache fetched responses
|
||||||
|
if ( |
||||||
|
response && |
||||||
|
response.status === 200 && |
||||||
|
response.type === 'basic' |
||||||
|
) { |
||||||
|
const responseClone = response.clone(); |
||||||
|
caches.open(CACHE_NAME).then((cache) => |
||||||
|
cache.put(request, responseClone) |
||||||
|
); |
||||||
|
} |
||||||
|
return response; |
||||||
|
}) |
||||||
|
.catch(() => caches.match('/offline.html')); |
||||||
|
}) |
||||||
|
); |
||||||
|
}); |
||||||
Loading…
Reference in new issue