Add Equipment page: owned brewers and grinders filter the Brews UI
Test and deploy / test-and-deploy (push) Successful in 59s
Test and deploy / test-and-deploy (push) Successful in 59s
- user_gear table (migration 008) + GET/PUT /api/gear (brewer keys validated against the taxonomy, grinders deduped/trimmed) - /gear page: silhouette multi-select for owned brewers (autosaves) and a grinder list; nav gains Brewing → Equipment - Brews page: with gear configured, the picker shows only owned brewers (plus the edited brew's method) with a show-all toggle; grinder field suggests your grinders; empty-gear state links to /gear - Included in full backups, per-user export, and the OpenAPI spec Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Fable 5
parent
3bceb44ae3
commit
e62b9601a2
+54
-1
@@ -14,6 +14,10 @@ let brews = [];
|
||||
let editingId = null;
|
||||
let selectedMethod = null;
|
||||
let beanFilter = null;
|
||||
// Owned equipment from /gear: when brewers are configured, the picker shows only those
|
||||
// (with a show-all escape hatch), and grinders become input suggestions.
|
||||
let gear = { brewers: [], grinders: [] };
|
||||
let showAllBrewers = false;
|
||||
|
||||
const fmtDate = (v) => (v ? new Date(v).toLocaleDateString() : "—");
|
||||
const fmtTime = (s) =>
|
||||
@@ -45,17 +49,29 @@ function silhouetteSvg(methodKey, size = 42) {
|
||||
|
||||
const isEspresso = () => findBrewMethod(selectedMethod)?.category === "espresso";
|
||||
|
||||
function visibleMethods() {
|
||||
if (!gear.brewers.length || showAllBrewers) return BREW_METHODS;
|
||||
// An old brew being edited may use a brewer that's since been removed from the gear list —
|
||||
// keep its tile visible so the selection isn't silently unrepresentable.
|
||||
return BREW_METHODS.filter(
|
||||
(m) => gear.brewers.includes(m.key) || m.key === selectedMethod,
|
||||
);
|
||||
}
|
||||
|
||||
function renderBrewerPicker() {
|
||||
const mount = document.getElementById("brewer-picker");
|
||||
mount.replaceChildren();
|
||||
const methods = visibleMethods();
|
||||
for (const category of BREW_CATEGORIES) {
|
||||
const inCategory = methods.filter((m) => m.category === category.key);
|
||||
if (!inCategory.length) continue;
|
||||
const title = document.createElement("div");
|
||||
title.className = "brewer-cat-title";
|
||||
title.textContent = category.name;
|
||||
mount.append(title);
|
||||
const grid = document.createElement("div");
|
||||
grid.className = "brewer-grid";
|
||||
for (const method of BREW_METHODS.filter((m) => m.category === category.key)) {
|
||||
for (const method of inCategory) {
|
||||
const tile = document.createElement("button");
|
||||
tile.type = "button";
|
||||
tile.className = "brewer-tile";
|
||||
@@ -72,6 +88,29 @@ function renderBrewerPicker() {
|
||||
}
|
||||
mount.append(grid);
|
||||
}
|
||||
if (gear.brewers.length) {
|
||||
const toggle = document.createElement("button");
|
||||
toggle.type = "button";
|
||||
toggle.className = "ghost-btn small";
|
||||
toggle.style.marginTop = "10px";
|
||||
toggle.textContent = showAllBrewers
|
||||
? "Show only my brewers"
|
||||
: "Show all brewers…";
|
||||
toggle.addEventListener("click", () => {
|
||||
showAllBrewers = !showAllBrewers;
|
||||
renderBrewerPicker();
|
||||
});
|
||||
mount.append(toggle);
|
||||
} else {
|
||||
const hint = document.createElement("p");
|
||||
hint.className = "field-note";
|
||||
hint.style.marginTop = "10px";
|
||||
const link = document.createElement("a");
|
||||
link.href = "/gear";
|
||||
link.textContent = "Set up your equipment";
|
||||
hint.append(link, " to only see the brewers you own here.");
|
||||
mount.append(hint);
|
||||
}
|
||||
}
|
||||
|
||||
function selectMethod(key) {
|
||||
@@ -269,6 +308,8 @@ function startEdit(brew) {
|
||||
editingId = brew.id;
|
||||
const form = document.getElementById("brew-form");
|
||||
form.beanId.value = brew.beanId ?? "";
|
||||
selectedMethod = brew.method;
|
||||
renderBrewerPicker(); // the edited brew's method may be outside the owned-gear filter
|
||||
selectMethod(brew.method);
|
||||
form.doseG.value = brew.doseG ?? "";
|
||||
form.waterG.value = brew.waterG ?? "";
|
||||
@@ -368,7 +409,19 @@ async function init() {
|
||||
initSideNav();
|
||||
const user = await loadNavUser();
|
||||
if (!user) return;
|
||||
try {
|
||||
gear = (await api("/api/gear")).gear;
|
||||
} catch {
|
||||
/* no gear configured — picker shows everything */
|
||||
}
|
||||
renderBrewerPicker();
|
||||
const grinderOptions = document.getElementById("grinder-options");
|
||||
if (grinderOptions)
|
||||
grinderOptions.replaceChildren(
|
||||
...gear.grinders.map((g) =>
|
||||
Object.assign(document.createElement("option"), { value: g }),
|
||||
),
|
||||
);
|
||||
updateRatingPill();
|
||||
await Promise.all([loadBeans(), loadBrews()]);
|
||||
const params = new URLSearchParams(location.search);
|
||||
|
||||
Reference in New Issue
Block a user