Files
Kochbuch/static/js/app.js
T
Kyle DammannandClaude Opus 5.5 60785e2437 Add phase 5: link import, Instagram fallback and share target
- SSRF-safe fetcher: public IPs only, connects to the checked address,
  redirects re-validated, size/time limits, honest user agent
- schema.org/Recipe JSON-LD extraction (graphs, HowToSections, ISO
  durations, yields, keywords, images) without extra dependencies;
  AI fallback on plain page text when no structured recipe exists
- Instagram: one login-free request to the public embed page; when
  Instagram withholds the caption, the import page explains how to paste
  the text or import a screenshot instead
- Suggested source photo can be downloaded and attached on save
- Text import works without AI (text becomes the description)
- Installable PWA with manifest, icons and Android share target
- Offline tests for SSRF rules, parsers, Instagram and import views

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 21:53:28 +01:00

69 lines
2.5 KiB
JavaScript

// Small progressive enhancements. Everything works without JavaScript.
document.addEventListener("click", async (event) => {
const button = event.target.closest("[data-copy]");
if (!button) return;
const input = document.querySelector(button.dataset.copy);
if (!input) return;
input.select();
try {
await navigator.clipboard.writeText(input.value);
button.textContent = "Kopiert ✓";
} catch {
button.textContent = "Bitte manuell kopieren";
}
});
// Serving calculator: recalculate as soon as the number changes.
document.addEventListener("change", (event) => {
const input = event.target.closest(".servings input[name=portionen]");
if (input && input.value) input.form.requestSubmit();
});
// Cooking mode: keep the screen on while the page is visible (Screen Wake Lock API).
(() => {
const page = document.querySelector("[data-wakelock]");
if (!page || !("wakeLock" in navigator)) return;
const status = page.querySelector(".wakelock-status");
let lock = null;
const acquire = async () => {
try {
lock = await navigator.wakeLock.request("screen");
if (status) status.hidden = false;
lock.addEventListener("release", () => { if (status) status.hidden = true; });
} catch {
// Not allowed right now (e.g. battery saver); the page still works.
}
};
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") acquire();
});
acquire();
})();
// Long-running submits (AI): show progress on the clicked button and prevent double submits.
// Disabling happens after the browser has captured the form data (incl. the clicked button).
document.addEventListener("submit", (event) => {
const button = event.submitter;
if (!button || !button.dataset.busyText) return;
setTimeout(() => {
event.target.querySelectorAll("button[type=submit]").forEach((b) => {
b.dataset.label = b.dataset.label || b.textContent;
b.disabled = true;
});
button.textContent = button.dataset.busyText;
}, 0);
});
// Back/forward cache: restore buttons disabled by the handler above.
window.addEventListener("pageshow", () => {
document.querySelectorAll("button[data-label]").forEach((b) => {
b.disabled = false;
b.textContent = b.dataset.label;
});
});
// Register the (no-cache) service worker so the app can be installed and used as share target.
if ("serviceWorker" in navigator) {
window.addEventListener("load", () => navigator.serviceWorker.register("/sw.js").catch(() => {}));
}