diff --git a/assets/app.js b/assets/app.js index a55085e..086499e 100644 --- a/assets/app.js +++ b/assets/app.js @@ -5,6 +5,7 @@ import './bootstrap.js'; * This file will be included onto the page via the importmap() Twig function, * which should already be in your base.html.twig. */ +import './styles/fonts.css'; import './styles/theme.css'; import './styles/app.css'; import './styles/layout.css'; @@ -12,5 +13,8 @@ import './styles/button.css'; import './styles/card.css'; import './styles/article.css'; import './styles/form.css'; +import './styles/spinner.css'; +import './styles/a2hs.css'; + console.log('This log comes from assets/app.js - welcome to AssetMapper! 🎉'); diff --git a/assets/controllers/install-prompt_controller.js b/assets/controllers/install-prompt_controller.js new file mode 100644 index 0000000..c79d44f --- /dev/null +++ b/assets/controllers/install-prompt_controller.js @@ -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 + } +} diff --git a/assets/controllers/service-worker_controller.js b/assets/controllers/service-worker_controller.js new file mode 100644 index 0000000..b2bba42 --- /dev/null +++ b/assets/controllers/service-worker_controller.js @@ -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)); + } + } +} diff --git a/assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-500.woff2 b/assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-500.woff2 new file mode 100644 index 0000000..a78c9a2 Binary files /dev/null and b/assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-500.woff2 differ diff --git a/assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-500italic.woff2 b/assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-500italic.woff2 new file mode 100644 index 0000000..d73d053 Binary files /dev/null and b/assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-500italic.woff2 differ diff --git a/assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-600.woff2 b/assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-600.woff2 new file mode 100644 index 0000000..510b2e2 Binary files /dev/null and b/assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-600.woff2 differ diff --git a/assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-600italic.woff2 b/assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-600italic.woff2 new file mode 100644 index 0000000..52a4f1e Binary files /dev/null and b/assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-600italic.woff2 differ diff --git a/assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-700.woff2 b/assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-700.woff2 new file mode 100644 index 0000000..056737b Binary files /dev/null and b/assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-700.woff2 differ diff --git a/assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-700italic.woff2 b/assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-700italic.woff2 new file mode 100644 index 0000000..d10d6a5 Binary files /dev/null and b/assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-700italic.woff2 differ diff --git a/assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-800.woff2 b/assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-800.woff2 new file mode 100644 index 0000000..e95b361 Binary files /dev/null and b/assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-800.woff2 differ diff --git a/assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-800italic.woff2 b/assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-800italic.woff2 new file mode 100644 index 0000000..8c7183f Binary files /dev/null and b/assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-800italic.woff2 differ diff --git a/assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-italic.woff2 b/assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-italic.woff2 new file mode 100644 index 0000000..3a1846a Binary files /dev/null and b/assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-italic.woff2 differ diff --git a/assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-regular.woff2 b/assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-regular.woff2 new file mode 100644 index 0000000..af11da6 Binary files /dev/null and b/assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-regular.woff2 differ diff --git a/assets/icons/apple-touch-icon.png b/assets/icons/apple-touch-icon.png new file mode 100644 index 0000000..643c779 Binary files /dev/null and b/assets/icons/apple-touch-icon.png differ diff --git a/assets/icons/favicon-96x96.png b/assets/icons/favicon-96x96.png new file mode 100644 index 0000000..6540452 Binary files /dev/null and b/assets/icons/favicon-96x96.png differ diff --git a/assets/icons/favicon.ico b/assets/icons/favicon.ico new file mode 100644 index 0000000..6372cb5 Binary files /dev/null and b/assets/icons/favicon.ico differ diff --git a/assets/icons/web-app-manifest-192x192.png b/assets/icons/web-app-manifest-192x192.png new file mode 100644 index 0000000..00bff6a Binary files /dev/null and b/assets/icons/web-app-manifest-192x192.png differ diff --git a/assets/icons/web-app-manifest-512x512.png b/assets/icons/web-app-manifest-512x512.png new file mode 100644 index 0000000..790f692 Binary files /dev/null and b/assets/icons/web-app-manifest-512x512.png differ diff --git a/assets/site.webmanifest b/assets/site.webmanifest new file mode 100644 index 0000000..2482fae --- /dev/null +++ b/assets/site.webmanifest @@ -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" +} diff --git a/assets/styles/a2hs.css b/assets/styles/a2hs.css new file mode 100644 index 0000000..a6246fb --- /dev/null +++ b/assets/styles/a2hs.css @@ -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; +} diff --git a/assets/styles/app.css b/assets/styles/app.css index 7ba46f0..4a31829 100644 --- a/assets/styles/app.css +++ b/assets/styles/app.css @@ -142,9 +142,6 @@ svg.icon { flex-wrap: wrap; } -div:nth-child(odd) .featured-list { - flex-direction: row-reverse; -} .featured-list > * { box-sizing: border-box; /* so padding/border don't break the layout */ @@ -152,6 +149,28 @@ div:nth-child(odd) .featured-list { padding: 10px; } +@media (max-width: 1024px) { + .featured-list { + flex-direction: column !important; + } + + .featured-list .card-header { + margin-top: 20px; + } + + .featured-list .card { + border-bottom: 1px solid var(--color-border) !important; + } + + .featured-list > * { + margin-bottom: 0; + padding: 0; + } +} +div:nth-child(odd) .featured-list { + flex-direction: row-reverse; +} + .featured-list div:first-child { flex: 0 0 66%; /* each item takes up 50% width = 2 columns */ } @@ -161,7 +180,7 @@ div:nth-child(odd) .featured-list { } .featured-list h2.card-title { - font-size: 1.75rem; + font-size: 1.5rem; } .featured-list p.lede { @@ -172,14 +191,20 @@ div:nth-child(odd) .featured-list { margin-bottom: 0; } +.featured-list .card:not(:last-child) { + border-bottom: 1px solid var(--color-border); +} + .featured-list .card-header img { - max-height: 100px; + max-height: 500px; + aspect-ratio: 1; } .article-list .metadata { display: flex; flex-direction: row; justify-content: space-between; + align-items: baseline; } .article-list .metadata p { @@ -199,7 +224,7 @@ div:nth-child(odd) .featured-list { } .card-header { - font-size: 1.5rem; + margin: 10px 0; } .header__image { @@ -246,6 +271,7 @@ div:nth-child(odd) .featured-list { .header__categories ul { display: flex; + flex-wrap: wrap; justify-content: center; gap: 2em; padding: 0; @@ -474,4 +500,5 @@ footer a { label.search { width: 100%; justify-content: center; + margin-bottom: 15px; } diff --git a/assets/styles/fonts.css b/assets/styles/fonts.css new file mode 100644 index 0000000..df52e2d --- /dev/null +++ b/assets/styles/fonts.css @@ -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+ */ +} diff --git a/assets/styles/spinner.css b/assets/styles/spinner.css new file mode 100644 index 0000000..5246806 --- /dev/null +++ b/assets/styles/spinner.css @@ -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); } +} diff --git a/assets/styles/theme.css b/assets/styles/theme.css index 4db861d..9dfa056 100644 --- a/assets/styles/theme.css +++ b/assets/styles/theme.css @@ -1,6 +1,5 @@ @import url('https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,200..800;1,6..72,200..800&family=Playfair+Display:ital,wght@0,400..900;1,400..900&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Montserrat&display=swap'); -@import url('https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,100..700;1,100..700&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Lobster&display=swap'); diff --git a/importmap.php b/importmap.php index 0dd06cc..1d060b8 100644 --- a/importmap.php +++ b/importmap.php @@ -57,4 +57,7 @@ return [ 'version' => '2.0.3', 'type' => 'css', ], + 'es-module-shims' => [ + 'version' => '2.0.10', + ], ]; diff --git a/public/icons/favicon.svg b/public/icons/favicon.svg new file mode 100644 index 0000000..44084ac --- /dev/null +++ b/public/icons/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/offline.html b/public/offline.html new file mode 100644 index 0000000..889220d --- /dev/null +++ b/public/offline.html @@ -0,0 +1,11 @@ + + +
+ +Please reconnect to use the app.
+ + diff --git a/public/service-worker.js b/public/service-worker.js new file mode 100644 index 0000000..e48980b --- /dev/null +++ b/public/service-worker.js @@ -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')); + }) + ); +}); diff --git a/templates/base.html.twig b/templates/base.html.twig index 9228111..8b041b6 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -1,17 +1,24 @@ - +