Files
MMM-NextcloudCookbook/control/app.js
T

115 lines
3.7 KiB
JavaScript

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