release nouri 0.5.0 shopping rhythm pwa and reminders

This commit is contained in:
2026-04-12 16:39:04 +02:00
parent d8b56e6b67
commit 96ab52e1ba
37 changed files with 2199 additions and 285 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

+174 -9
View File
@@ -128,6 +128,29 @@ button.secondary:hover,
margin: 1rem auto 2rem;
}
.site-footer {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 0.75rem;
padding: 1rem 0.35rem 0;
color: var(--muted);
font-size: 0.92rem;
}
.footer-copy {
display: inline-flex;
align-items: center;
gap: 0.55rem;
flex-wrap: wrap;
}
.footer-copy .ui-icon {
width: 0.95rem;
height: 0.95rem;
color: var(--accent-strong);
}
.site-header {
position: sticky;
top: 1rem;
@@ -445,6 +468,7 @@ h3 {
justify-content: space-between;
gap: 1rem;
align-items: center;
padding: 1rem 1.1rem;
}
.stacked-mobile {
@@ -616,6 +640,7 @@ input[type="text"],
input[type="email"],
input[type="password"],
input[type="date"],
input[type="time"],
input[type="file"],
select,
textarea {
@@ -862,6 +887,111 @@ legend {
margin-bottom: 1rem;
}
.planner-subsection {
display: grid;
gap: 0.8rem;
margin-bottom: 1rem;
}
.planner-subsection h3 {
font-size: 1rem;
}
.planner-search {
display: grid;
gap: 0.45rem;
color: var(--muted);
}
.compact-picker-list {
display: grid;
gap: 0.55rem;
}
.compact-picker-list form[hidden] {
display: none;
}
.picker-row {
width: 100%;
justify-content: space-between;
padding: 0.85rem 1rem;
border-radius: 16px;
border: 1px solid var(--line);
background: color-mix(in srgb, var(--surface-strong) 86%, #fff 14%);
color: var(--text);
}
.picker-row small {
color: var(--muted);
}
.compact-quick-row {
margin-bottom: 0;
}
.compact-button {
min-width: 150px;
padding: 0.78rem 0.9rem;
}
.settings-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
}
.pwa-card {
padding: 1rem;
border-radius: 18px;
border: 1px solid var(--line);
background: color-mix(in srgb, var(--surface-strong) 84%, #fff 16%);
}
.card-link-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.75rem;
}
.menu-card {
display: grid;
justify-items: start;
gap: 0.5rem;
padding: 1rem;
border-radius: 18px;
border: 1px solid var(--line);
background: color-mix(in srgb, var(--surface-strong) 86%, #fff 14%);
}
.menu-card .ui-icon {
width: 1.15rem;
height: 1.15rem;
color: var(--accent-strong);
}
.roomy-row {
padding: 1rem 1.2rem;
}
.inline-form-tight {
grid-template-columns: 1fr auto;
}
.inline-form-tight > :first-child {
grid-column: auto;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
.planner-entry-top {
align-items: flex-start;
}
@@ -1018,6 +1148,41 @@ legend {
mask-image: url("../icons/fa/ellipsis.svg");
}
.icon-heart {
-webkit-mask-image: url("../icons/fa/heart.svg");
mask-image: url("../icons/fa/heart.svg");
}
.icon-sliders {
-webkit-mask-image: url("../icons/fa/sliders.svg");
mask-image: url("../icons/fa/sliders.svg");
}
.icon-seedling {
-webkit-mask-image: url("../icons/fa/seedling.svg");
mask-image: url("../icons/fa/seedling.svg");
}
.icon-bell {
-webkit-mask-image: url("../icons/fa/bell.svg");
mask-image: url("../icons/fa/bell.svg");
}
.icon-mobile-screen-button {
-webkit-mask-image: url("../icons/fa/mobile-screen-button.svg");
mask-image: url("../icons/fa/mobile-screen-button.svg");
}
.icon-apple-whole {
-webkit-mask-image: url("../icons/fa/apple-whole.svg");
mask-image: url("../icons/fa/apple-whole.svg");
}
.icon-leaf {
-webkit-mask-image: url("../icons/fa/leaf.svg");
mask-image: url("../icons/fa/leaf.svg");
}
.mobile-sheet-backdrop {
position: fixed;
inset: 0;
@@ -1054,17 +1219,10 @@ legend {
.mobile-sheet-links {
display: grid;
gap: 0.45rem;
gap: 0.75rem;
margin: 1rem 0;
}
.mobile-sheet-links a {
padding: 0.9rem 1rem;
border-radius: 16px;
border: 1px solid var(--line);
background: color-mix(in srgb, var(--surface-strong) 86%, #fff 14%);
}
.mobile-sheet-actions {
flex-wrap: wrap;
}
@@ -1091,6 +1249,7 @@ legend {
.stats-grid,
.two-column,
.template-library-grid,
.settings-grid,
.inline-form,
.planner-entry-form,
.planner-entry-form-wide,
@@ -1158,6 +1317,10 @@ legend {
padding: 1rem;
}
.site-footer {
padding-bottom: 5.6rem;
}
h1 {
font-size: clamp(1.6rem, 7vw, 2rem);
}
@@ -1174,7 +1337,9 @@ legend {
.week-mini-grid,
.week-overview-grid,
.more-link-grid,
.template-library-grid {
.template-library-grid,
.settings-grid,
.card-link-grid {
grid-template-columns: 1fr;
}
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Pro 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2025 Fonticons, Inc. --><path fill="currentColor" d="M224 112c-8.8 0-16-7.2-16-16l0-16c0-44.2 35.8-80 80-80l16 0c8.8 0 16 7.2 16 16l0 16c0 44.2-35.8 80-80 80l-16 0zM0 288c0-76.3 35.7-160 112-160 27.3 0 59.7 10.3 82.7 19.3 18.8 7.3 39.9 7.3 58.7 0 22.9-8.9 55.4-19.3 82.7-19.3 76.3 0 112 83.7 112 160 0 128-80 224-160 224-16.5 0-38.1-6.6-51.5-11.3-8.1-2.8-16.9-2.8-25 0-13.4 4.7-35 11.3-51.5 11.3-80 0-160-96-160-224z"/></svg>

After

Width:  |  Height:  |  Size: 631 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Pro 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2025 Fonticons, Inc. --><path fill="currentColor" d="M224 0c-17.7 0-32 14.3-32 32l0 3.2C119 50 64 114.6 64 192l0 21.7c0 48.1-16.4 94.8-46.4 132.4L7.8 358.3C2.7 364.6 0 372.4 0 380.5 0 400.1 15.9 416 35.5 416l376.9 0c19.6 0 35.5-15.9 35.5-35.5 0-8.1-2.7-15.9-7.8-22.2l-9.8-12.2C400.4 308.5 384 261.8 384 213.7l0-21.7c0-77.4-55-142-128-156.8l0-3.2c0-17.7-14.3-32-32-32zM162 464c7.1 27.6 32.2 48 62 48s54.9-20.4 62-48l-124 0z"/></svg>

After

Width:  |  Height:  |  Size: 637 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2025 Fonticons, Inc. --><path fill="currentColor" d="M241 87.1l15 20.7 15-20.7C296 52.5 336.2 32 378.9 32 452.4 32 512 91.6 512 165.1l0 2.6c0 112.2-139.9 242.5-212.9 298.2-12.4 9.4-27.6 14.1-43.1 14.1s-30.8-4.6-43.1-14.1C139.9 410.2 0 279.9 0 167.7l0-2.6C0 91.6 59.6 32 133.1 32 175.8 32 216 52.5 241 87.1z"/></svg>

After

Width:  |  Height:  |  Size: 521 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2025 Fonticons, Inc. --><path fill="currentColor" d="M471.3 6.7C477.7 .6 487-1.6 495.6 1.2 505.4 4.5 512 13.7 512 24l0 186.9c0 131.2-108.1 237.1-238.8 237.1-77 0-143.4-49.5-167.5-118.7-35.4 30.8-57.7 76.1-57.7 126.7 0 13.3-10.7 24-24 24S0 469.3 0 456C0 381.1 38.2 315.1 96.1 276.3 131.4 252.7 173.5 240 216 240l80 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-80 0c-39.7 0-77.3 8.8-111 24.5 23.3-70 89.2-120.5 167-120.5 66.4 0 115.8-22.1 148.7-44 19.2-12.8 35.5-28.1 50.7-45.3z"/></svg>

After

Width:  |  Height:  |  Size: 685 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--! Font Awesome Pro 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2025 Fonticons, Inc. --><path fill="currentColor" d="M16 64C16 28.7 44.7 0 80 0L304 0c35.3 0 64 28.7 64 64l0 384c0 35.3-28.7 64-64 64L80 512c-35.3 0-64-28.7-64-64L16 64zm64 0l0 304 224 0 0-304-224 0zM192 472c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32z"/></svg>

After

Width:  |  Height:  |  Size: 487 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2025 Fonticons, Inc. --><path fill="currentColor" d="M512 32C512 140.1 435.4 230.3 333.6 251.4 325.7 193.3 299.6 141 261.1 100.5 301.2 40 369.9 0 448 0l32 0c17.7 0 32 14.3 32 32zM0 96C0 78.3 14.3 64 32 64l32 0c123.7 0 224 100.3 224 224l0 192c0 17.7-14.3 32-32 32s-32-14.3-32-32l0-160C100.3 320 0 219.7 0 96z"/></svg>

After

Width:  |  Height:  |  Size: 522 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2025 Fonticons, Inc. --><path fill="currentColor" d="M32 64C14.3 64 0 78.3 0 96s14.3 32 32 32l86.7 0c12.3 28.3 40.5 48 73.3 48s61-19.7 73.3-48L480 128c17.7 0 32-14.3 32-32s-14.3-32-32-32L265.3 64C253 35.7 224.8 16 192 16s-61 19.7-73.3 48L32 64zm0 160c-17.7 0-32 14.3-32 32s14.3 32 32 32l246.7 0c12.3 28.3 40.5 48 73.3 48s61-19.7 73.3-48l54.7 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-54.7 0c-12.3-28.3-40.5-48-73.3-48s-61 19.7-73.3 48L32 224zm0 160c-17.7 0-32 14.3-32 32s14.3 32 32 32l54.7 0c12.3 28.3 40.5 48 73.3 48s61-19.7 73.3-48L480 448c17.7 0 32-14.3 32-32s-14.3-32-32-32l-246.7 0c-12.3-28.3-40.5-48-73.3-48s-61 19.7-73.3 48L32 384z"/></svg>

After

Width:  |  Height:  |  Size: 850 B

+96
View File
@@ -0,0 +1,96 @@
(() => {
const getCsrfToken = () => {
const meta = document.querySelector('meta[name="csrf-token"]');
return meta ? meta.getAttribute("content") : "";
};
const getPushPublicKey = () => {
const meta = document.querySelector('meta[name="nouri-push-public-key"]');
return meta ? meta.getAttribute("content") : "";
};
const urlBase64ToUint8Array = (base64String) => {
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
const rawData = window.atob(base64);
return Uint8Array.from([...rawData].map((character) => character.charCodeAt(0)));
};
const registerServiceWorker = async () => {
if (!("serviceWorker" in navigator)) return null;
return navigator.serviceWorker.register("/service-worker.js");
};
const subscribeToPush = async () => {
const publicKey = getPushPublicKey();
if (!publicKey || !("serviceWorker" in navigator) || !("PushManager" in window)) return;
const registration = await navigator.serviceWorker.ready;
const permission = await Notification.requestPermission();
if (permission !== "granted") return;
const existing = await registration.pushManager.getSubscription();
const subscription = existing || await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(publicKey),
});
const subscriptionJson = subscription.toJSON();
const payload = new URLSearchParams({
csrf_token: getCsrfToken(),
endpoint: subscription.endpoint,
p256dh: subscriptionJson.keys && subscriptionJson.keys.p256dh ? subscriptionJson.keys.p256dh : "",
auth: subscriptionJson.keys && subscriptionJson.keys.auth ? subscriptionJson.keys.auth : "",
});
await fetch("/push/subscribe", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
"X-Requested-With": "XMLHttpRequest",
},
body: payload.toString(),
});
window.location.reload();
};
const unsubscribeFromPush = async () => {
if (!("serviceWorker" in navigator)) return;
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.getSubscription();
const payload = new URLSearchParams({ csrf_token: getCsrfToken() });
if (subscription) {
payload.set("endpoint", subscription.endpoint);
await subscription.unsubscribe();
}
await fetch("/push/unsubscribe", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
"X-Requested-With": "XMLHttpRequest",
},
body: payload.toString(),
});
window.location.reload();
};
document.addEventListener("DOMContentLoaded", () => {
registerServiceWorker();
const enableButton = document.querySelector("[data-push-enable]");
const disableButton = document.querySelector("[data-push-disable]");
if (enableButton) {
enableButton.addEventListener("click", () => {
subscribeToPush().catch(() => {
window.location.reload();
});
});
}
if (disableButton) {
disableButton.addEventListener("click", () => {
unsubscribeFromPush().catch(() => {
window.location.reload();
});
});
}
});
})();
+23
View File
@@ -0,0 +1,23 @@
{
"name": "Nouri",
"short_name": "Nouri",
"description": "einfach essen planen",
"lang": "de",
"start_url": "/",
"scope": "/",
"display": "standalone",
"background_color": "#fff6ef",
"theme_color": "#efab72",
"icons": [
{
"src": "/static/brand/pwa-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/static/brand/pwa-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
+74
View File
@@ -0,0 +1,74 @@
const CACHE_NAME = "nouri-v0-5-0";
const APP_SHELL = [
"/",
"/static/css/styles.css",
"/static/js/theme.js",
"/static/js/ui.js",
"/static/js/planner.js",
"/static/js/pwa.js",
"/static/brand/pwa-192.png",
"/static/brand/pwa-512.png",
"/static/brand/favicon.svg",
];
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.addAll(APP_SHELL)).then(() => self.skipWaiting())
);
});
self.addEventListener("activate", (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key)))
).then(() => self.clients.claim())
);
});
self.addEventListener("fetch", (event) => {
if (event.request.method !== "GET") return;
event.respondWith(
caches.match(event.request).then((cached) => {
if (cached) return cached;
return fetch(event.request).then((response) => {
if (!response || response.status !== 200 || response.type !== "basic") {
return response;
}
const clone = response.clone();
caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
return response;
});
})
);
});
self.addEventListener("push", (event) => {
if (!event.data) return;
const data = event.data.json();
event.waitUntil(
self.registration.showNotification(data.title || "Nouri", {
body: data.body || "",
icon: data.icon || "/static/brand/pwa-192.png",
badge: data.badge || "/static/brand/pwa-badge.png",
data: { url: data.url || "/" },
})
);
});
self.addEventListener("notificationclick", (event) => {
event.notification.close();
const targetUrl = event.notification.data && event.notification.data.url ? event.notification.data.url : "/";
event.waitUntil(
clients.matchAll({ type: "window", includeUncontrolled: true }).then((clientList) => {
for (const client of clientList) {
if (client.url.includes(targetUrl) && "focus" in client) {
return client.focus();
}
}
if (clients.openWindow) {
return clients.openWindow(targetUrl);
}
return null;
})
);
});