Nuša Pukšič 8 months ago
parent
commit
06490272e8
  1. 4
      assets/app.js
  2. 59
      assets/controllers/install-prompt_controller.js
  3. 11
      assets/controllers/service-worker_controller.js
  4. BIN
      assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-500.woff2
  5. BIN
      assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-500italic.woff2
  6. BIN
      assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-600.woff2
  7. BIN
      assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-600italic.woff2
  8. BIN
      assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-700.woff2
  9. BIN
      assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-700italic.woff2
  10. BIN
      assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-800.woff2
  11. BIN
      assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-800italic.woff2
  12. BIN
      assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-italic.woff2
  13. BIN
      assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-regular.woff2
  14. BIN
      assets/icons/apple-touch-icon.png
  15. BIN
      assets/icons/favicon-96x96.png
  16. BIN
      assets/icons/favicon.ico
  17. BIN
      assets/icons/web-app-manifest-192x192.png
  18. BIN
      assets/icons/web-app-manifest-512x512.png
  19. 21
      assets/site.webmanifest
  20. 14
      assets/styles/a2hs.css
  21. 39
      assets/styles/app.css
  22. 80
      assets/styles/fonts.css
  23. 28
      assets/styles/spinner.css
  24. 1
      assets/styles/theme.css
  25. 3
      importmap.php
  26. 1
      public/icons/favicon.svg
  27. 11
      public/offline.html
  28. 83
      public/service-worker.js
  29. 23
      templates/base.html.twig

4
assets/app.js

@ -5,6 +5,7 @@ import './bootstrap.js';
* This file will be included onto the page via the importmap() Twig function, * This file will be included onto the page via the importmap() Twig function,
* which should already be in your base.html.twig. * which should already be in your base.html.twig.
*/ */
import './styles/fonts.css';
import './styles/theme.css'; import './styles/theme.css';
import './styles/app.css'; import './styles/app.css';
import './styles/layout.css'; import './styles/layout.css';
@ -12,5 +13,8 @@ import './styles/button.css';
import './styles/card.css'; import './styles/card.css';
import './styles/article.css'; import './styles/article.css';
import './styles/form.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! 🎉'); console.log('This log comes from assets/app.js - welcome to AssetMapper! 🎉');

59
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
}
}

11
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));
}
}
}

BIN
assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-500.woff2

Binary file not shown.

BIN
assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-500italic.woff2

Binary file not shown.

BIN
assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-600.woff2

Binary file not shown.

BIN
assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-600italic.woff2

Binary file not shown.

BIN
assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-700.woff2

Binary file not shown.

BIN
assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-700italic.woff2

Binary file not shown.

BIN
assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-800.woff2

Binary file not shown.

BIN
assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-800italic.woff2

Binary file not shown.

BIN
assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-italic.woff2

Binary file not shown.

BIN
assets/fonts/eb-garamond/eb-garamond-v30-latin_latin-ext-regular.woff2

Binary file not shown.

BIN
assets/icons/apple-touch-icon.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

BIN
assets/icons/favicon-96x96.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
assets/icons/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
assets/icons/web-app-manifest-192x192.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

BIN
assets/icons/web-app-manifest-512x512.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

21
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"
}

14
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;
}

39
assets/styles/app.css

@ -142,9 +142,6 @@ svg.icon {
flex-wrap: wrap; flex-wrap: wrap;
} }
div:nth-child(odd) .featured-list {
flex-direction: row-reverse;
}
.featured-list > * { .featured-list > * {
box-sizing: border-box; /* so padding/border don't break the layout */ box-sizing: border-box; /* so padding/border don't break the layout */
@ -152,6 +149,28 @@ div:nth-child(odd) .featured-list {
padding: 10px; 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 { .featured-list div:first-child {
flex: 0 0 66%; /* each item takes up 50% width = 2 columns */ 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 { .featured-list h2.card-title {
font-size: 1.75rem; font-size: 1.5rem;
} }
.featured-list p.lede { .featured-list p.lede {
@ -172,14 +191,20 @@ div:nth-child(odd) .featured-list {
margin-bottom: 0; margin-bottom: 0;
} }
.featured-list .card:not(:last-child) {
border-bottom: 1px solid var(--color-border);
}
.featured-list .card-header img { .featured-list .card-header img {
max-height: 100px; max-height: 500px;
aspect-ratio: 1;
} }
.article-list .metadata { .article-list .metadata {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
align-items: baseline;
} }
.article-list .metadata p { .article-list .metadata p {
@ -199,7 +224,7 @@ div:nth-child(odd) .featured-list {
} }
.card-header { .card-header {
font-size: 1.5rem; margin: 10px 0;
} }
.header__image { .header__image {
@ -246,6 +271,7 @@ div:nth-child(odd) .featured-list {
.header__categories ul { .header__categories ul {
display: flex; display: flex;
flex-wrap: wrap;
justify-content: center; justify-content: center;
gap: 2em; gap: 2em;
padding: 0; padding: 0;
@ -474,4 +500,5 @@ footer a {
label.search { label.search {
width: 100%; width: 100%;
justify-content: center; justify-content: center;
margin-bottom: 15px;
} }

80
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+ */
}

28
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); }
}

1
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=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=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'); @import url('https://fonts.googleapis.com/css2?family=Lobster&display=swap');

3
importmap.php

@ -57,4 +57,7 @@ return [
'version' => '2.0.3', 'version' => '2.0.3',
'type' => 'css', 'type' => 'css',
], ],
'es-module-shims' => [
'version' => '2.0.10',
],
]; ];

1
public/icons/favicon.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 29 KiB

11
public/offline.html

@ -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>

83
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'));
})
);
});

23
templates/base.html.twig

@ -1,17 +1,24 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" data-theme="{{ app.session.get('theme', 'dark') }}"> <html lang="en" data-theme="{{ app.session.get('theme', 'light') }}">
<head> <head>
<meta charset="UTF-8"> <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">
<title>{% block title %}Newsroom{% endblock %}</title> <title>{% block title %}Newsroom{% endblock %}</title>
<meta name="description" content="Decentralised Newsroom - the future of journalism"> <meta name="description" content="Decentralised Newsroom - the future of journalism">
<link rel="icon" type="image/png" href="{{ asset('icons/favicon-96x96.png') }}" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="{{ asset('icons/favicon.svg') }}" />
<link rel="shortcut icon" href="{{ asset('icons/favicon.ico') }}" />
<link rel="apple-touch-icon" href="{{ asset('icons/apple-touch-icon.png') }}">
<meta name="apple-mobile-web-app-title" content="Newsroom" />
<link rel="manifest" href="{{ asset('site.webmanifest') }}">
{% block stylesheets %} {% block stylesheets %}
{% endblock %} {% endblock %}
{% block javascripts %} {% block javascripts %}
{% block importmap %}{{ importmap('app') }}{% endblock %} {% block importmap %}{{ importmap('app') }}{% endblock %}
{% endblock %} {% endblock %}
</head> </head>
<body> <body data-controller="service-worker">
<header> <header>
<twig:Header /> <twig:Header />
</header> </header>
@ -29,6 +36,18 @@
</aside> </aside>
</div> </div>
<div data-controller="install-prompt">
<div
class="install-prompt-box hidden"
data-install-prompt-target="promptBox"
>
<p>Install this app on your device for quick access?</p>
<button data-action="click->install-prompt#install">Yes, install</button>
<button data-action="click->install-prompt#dismiss">No thanks</button>
</div>
</div>
<footer> <footer>
<twig:Footer /> <twig:Footer />
</footer> </footer>

Loading…
Cancel
Save