const apiBase = "/MMM-NextcloudCookbook/api"; const status = document.querySelector("#status"); const list = document.querySelector("#recipes"); const search = document.querySelector("#search"); const exit = document.querySelector("#exit"); const portionControls = document.querySelector("#portions"); const portionCount = document.querySelector("#portion-count"); const fewerPortions = document.querySelector("#fewer-portions"); const morePortions = document.querySelector("#more-portions"); let recipes = []; let activeRecipeId = null; let activeRecipeName = null; let portions = null; async function request(path, options) { const response = await fetch(`${apiBase}${path}`, { headers: { "Content-Type": "application/json" }, ...options }); if (!response.ok) { const body = await response.json().catch(() => ({})); throw new Error(body.error || `Request failed with HTTP ${response.status}`); } return response.json(); } function render() { const query = search.value.trim().toLocaleLowerCase(); const matches = recipes.filter((recipe) => recipe.name.toLocaleLowerCase().includes(query)); list.replaceChildren(...matches.map((recipe) => { const item = document.createElement("li"); const button = document.createElement("button"); button.type = "button"; button.className = recipe.id === activeRecipeId ? "recipe active" : "recipe"; const title = document.createElement("strong"); title.textContent = recipe.name; button.appendChild(title); if (recipe.description) { const description = document.createElement("span"); description.textContent = recipe.description; button.appendChild(description); } button.addEventListener("click", () => activate(recipe)); item.appendChild(button); return item; })); if (!matches.length) status.textContent = "No matching recipes."; portionControls.hidden = !activeRecipeId; portionCount.textContent = portions ?? ""; fewerPortions.disabled = portions <= 1; } async function activate(recipe) { status.textContent = `Opening ${recipe.name}…`; try { const state = await request("/activate", { method: "POST", body: JSON.stringify({ recipeId: recipe.id }) }); activeRecipeId = state.recipeId; activeRecipeName = recipe.name; portions = state.portions; status.textContent = `${recipe.name} is now on the mirror.`; render(); } catch (error) { status.textContent = error.message; } } async function adjustPortions(delta) { try { const state = await request("/portions", { method: "POST", body: JSON.stringify({ delta }) }); portions = state.portions; status.textContent = `${activeRecipeName} is now on the mirror.`; render(); } catch (error) { status.textContent = error.message; } } fewerPortions.addEventListener("click", () => adjustPortions(-1)); morePortions.addEventListener("click", () => adjustPortions(1)); exit.addEventListener("click", async () => { status.textContent = "Leaving recipe mode…"; try { await request("/exit", { method: "POST", body: "{}" }); activeRecipeId = null; activeRecipeName = null; portions = null; status.textContent = "The mirror is back to its normal display."; render(); } catch (error) { status.textContent = error.message; } }); search.addEventListener("input", render); Promise.all([request("/state"), request("/recipes")]) .then(([state, loadedRecipes]) => { activeRecipeId = state.recipeId; activeRecipeName = state.recipe?.name || null; portions = state.portions; recipes = loadedRecipes; status.textContent = `${recipes.length} recipes available.`; render(); }) .catch((error) => { status.textContent = error.message; });