feat(ui): build responsive cookbook, editor and persistent cooking assistant
This commit is contained in:
1 parent
5d71d32e20
commit
a58516e424
29 files changed
+6430
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 4.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><rect width="512" height="512" rx="110" fill="#365747"/><path d="M157 265c-56-5-57-88-4-100 14-72 110-79 132-22 52-21 102 20 93 70-4 27-25 45-49 49v87H157z" fill="none" stroke="#faf8f4" stroke-width="18" stroke-linejoin="round"/><path d="M159 311h169M217 208v54m57-54v54" fill="none" stroke="#faf8f4" stroke-width="15" stroke-linecap="round"/><circle cx="363" cy="351" r="17" fill="#d38968"/></svg>
|
||||
|
After Width: | Height: | Size: 486 B |
Binary file not shown.
|
After Width: | Height: | Size: 179 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 217 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 165 KiB |
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"id": "/",
|
||||
"name": "HamHam – Dein persönliches Kochbuch",
|
||||
"short_name": "HamHam",
|
||||
"description": "Lieblingsrezepte sammeln und entspannt kochen.",
|
||||
"lang": "de",
|
||||
"start_url": "/",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#faf8f4",
|
||||
"theme_color": "#365747",
|
||||
"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 maskable"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1" /><meta
|
||||
name="theme-color"
|
||||
content="#365747"
|
||||
/><title>HamHam · Offline</title
|
||||
><style>
|
||||
body {
|
||||
background: #faf8f4;
|
||||
color: #253b32;
|
||||
font: 16px/1.7 system-ui;
|
||||
margin: 0;
|
||||
padding: 50px 25px;
|
||||
}
|
||||
main {
|
||||
max-width: 600px;
|
||||
margin: auto;
|
||||
}
|
||||
h1 {
|
||||
font: 44px Georgia;
|
||||
}
|
||||
a {
|
||||
color: #365747;
|
||||
}
|
||||
li {
|
||||
margin: 12px 0;
|
||||
}
|
||||
button {
|
||||
background: #365747;
|
||||
color: white;
|
||||
border: 0;
|
||||
padding: 14px 22px;
|
||||
border-radius: 9px;
|
||||
font: inherit;
|
||||
}
|
||||
</style>
|
||||
<main>
|
||||
<p>HAMHAM · DEIN KOCHBUCH</p>
|
||||
<h1>Ein Moment ohne Netz.</h1>
|
||||
<p>
|
||||
Diese Seite ist noch nicht auf deinem Gerät gespeichert. Bereits online geöffnete Rezept- und
|
||||
Kochseiten findest du hier. Für neue Rezepte und die Verwaltung brauchst du eine Verbindung.
|
||||
</p>
|
||||
<ul id="recipes"></ul>
|
||||
<button onclick="location.reload()">Verbindung erneut prüfen</button>
|
||||
</main>
|
||||
<script>
|
||||
caches
|
||||
.open('hamham-public-v1')
|
||||
.then((c) => c.keys())
|
||||
.then((keys) => {
|
||||
const list = document.querySelector('#recipes');
|
||||
keys
|
||||
.filter((r) => new URL(r.url).pathname.startsWith('/rezepte/'))
|
||||
.forEach((r) => {
|
||||
let li = document.createElement('li'),
|
||||
a = document.createElement('a'),
|
||||
url = new URL(r.url);
|
||||
a.href = r.url;
|
||||
let cook = url.pathname.endsWith('/kochen');
|
||||
a.textContent =
|
||||
(cook ? 'Kochvorgang fortsetzen' : 'Gespeichertes Rezept öffnen') +
|
||||
' · ' +
|
||||
url.pathname.split('/')[2].slice(0, 8);
|
||||
li.append(a);
|
||||
list.append(li);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</html>
|
||||
@@ -0,0 +1,94 @@
|
||||
const CACHE = 'hamham-public-v1';
|
||||
async function savePage(address) {
|
||||
const url = new URL(address, self.location.origin);
|
||||
if (url.origin !== self.location.origin) return;
|
||||
const match = /^\/rezepte\/([a-f0-9-]{36})(?:\/kochen)?$/.exec(url.pathname);
|
||||
if (match) {
|
||||
const recipe = await fetch(`/api/recipes/${match[1]}`).then((r) => (r.ok ? r.json() : null));
|
||||
if (!recipe?.published) return;
|
||||
} else if (!['/', '/favoriten', '/einkaufsliste'].includes(url.pathname)) return;
|
||||
const response = await fetch(url.href, { headers: { Accept: 'text/html' } });
|
||||
if (!response.ok || !response.headers.get('content-type')?.includes('text/html')) return;
|
||||
const cache = await caches.open(CACHE),
|
||||
html = await response.clone().text();
|
||||
await cache.put(url.href, response);
|
||||
const paths = [
|
||||
...new Set(
|
||||
Array.from(
|
||||
html.matchAll(/(?:src|href)="(\/(?:_next\/static\/|images\/|icons\/|media\/)[^"<>]+)"/g),
|
||||
(m) => m[1].replaceAll('&', '&'),
|
||||
),
|
||||
),
|
||||
];
|
||||
await Promise.all(
|
||||
paths.map(async (path) => {
|
||||
try {
|
||||
const r = await fetch(path);
|
||||
if (r.ok) await cache.put(path, r);
|
||||
} catch {}
|
||||
}),
|
||||
);
|
||||
}
|
||||
self.addEventListener('message', (event) => {
|
||||
if (event.data?.type === 'CACHE_PAGE' && typeof event.data.url === 'string')
|
||||
event.waitUntil(savePage(event.data.url).catch(() => {}));
|
||||
});
|
||||
self.addEventListener('install', (event) => {
|
||||
event.waitUntil(
|
||||
caches.open(CACHE).then((cache) => cache.addAll(['/offline.html', '/icons/icon-192.png'])),
|
||||
);
|
||||
self.skipWaiting();
|
||||
});
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(
|
||||
caches
|
||||
.keys()
|
||||
.then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)))),
|
||||
);
|
||||
self.clients.claim();
|
||||
});
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const req = event.request,
|
||||
url = new URL(req.url);
|
||||
if (
|
||||
req.method !== 'GET' ||
|
||||
url.origin !== self.location.origin ||
|
||||
url.pathname.startsWith('/admin') ||
|
||||
url.pathname.startsWith('/api') ||
|
||||
url.searchParams.has('_rsc') ||
|
||||
req.headers.get('rsc')
|
||||
)
|
||||
return;
|
||||
const asset = /^\/(?:_next\/static\/|images\/|icons\/|media\/)/.test(url.pathname);
|
||||
const recipe = /^\/rezepte\/([a-f0-9-]{36})(?:\/kochen)?$/.exec(url.pathname);
|
||||
if (!asset && req.mode !== 'navigate') return;
|
||||
event.respondWith(
|
||||
(async () => {
|
||||
const cache = await caches.open(CACHE);
|
||||
if (asset) {
|
||||
const cached = await cache.match(req);
|
||||
if (cached) return cached;
|
||||
}
|
||||
try {
|
||||
const response = await fetch(req);
|
||||
if (response.ok && asset) await cache.put(req, response.clone());
|
||||
if (response.ok && recipe) {
|
||||
// Never persist a draft or an administrator page. Only published recipes.
|
||||
const data = await fetch(`/api/recipes/${recipe[1]}`)
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.catch(() => null);
|
||||
if (data?.published) await cache.put(req, response.clone());
|
||||
else await cache.delete(req);
|
||||
}
|
||||
return response;
|
||||
} catch {
|
||||
const saved =
|
||||
(await cache.match(req)) ??
|
||||
(req.mode === 'navigate' ? await cache.match(req, { ignoreSearch: true }) : null);
|
||||
if (saved) return saved;
|
||||
if (req.mode === 'navigate') return await cache.match('/offline.html');
|
||||
return new Response('', { status: 503 });
|
||||
}
|
||||
})(),
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user