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
@@ -48,6 +48,7 @@ const PUBLIC_SHELL_FILES = new Set([
|
||||
"/roasts.html",
|
||||
"/beans.html",
|
||||
"/brews.html",
|
||||
"/gear.html",
|
||||
"/api-docs.html",
|
||||
]);
|
||||
|
||||
@@ -142,6 +143,7 @@ export function createApp({
|
||||
req.path === "/roasts" ||
|
||||
req.path === "/beans" ||
|
||||
req.path === "/brews" ||
|
||||
req.path === "/gear" ||
|
||||
req.path === "/api-docs"
|
||||
)
|
||||
res.set("Cache-Control", "no-store, private");
|
||||
@@ -422,6 +424,7 @@ export function createApp({
|
||||
for (const [route, file] of [
|
||||
["/beans", "beans.html"],
|
||||
["/brews", "brews.html"],
|
||||
["/gear", "gear.html"],
|
||||
["/api-docs", "api-docs.html"],
|
||||
]) {
|
||||
app.get(route, async (req, res, next) => {
|
||||
@@ -2390,6 +2393,7 @@ export function createApp({
|
||||
],
|
||||
],
|
||||
["api_tokens", ["token_hash", "user_id", "name", "created_at", "last_used_at"]],
|
||||
["user_gear", ["user_id", "brewers", "grinders", "updated_at"]],
|
||||
["audit_events", ["id", "actor_user_id", "action", "target", "created_at"]],
|
||||
];
|
||||
const BACKUP_VERSION = 1;
|
||||
@@ -2718,6 +2722,58 @@ export function createApp({
|
||||
.json({ ok: false, code: "unparseable_alog", error: err.message });
|
||||
}
|
||||
});
|
||||
// ─── Equipment (owned brewers + grinders) ──────────────────────────────
|
||||
app.get("/api/gear", requireAuth, async (req, res, next) => {
|
||||
try {
|
||||
const row = (
|
||||
await db.query("SELECT brewers,grinders FROM user_gear WHERE user_id=$1", [
|
||||
req.user.id,
|
||||
])
|
||||
).rows[0];
|
||||
res.json({
|
||||
ok: true,
|
||||
gear: { brewers: row?.brewers ?? [], grinders: row?.grinders ?? [] },
|
||||
});
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
app.put("/api/gear", requireAuth, csrf, async (req, res, next) => {
|
||||
try {
|
||||
const rawBrewers = req.body.brewers;
|
||||
const rawGrinders = req.body.grinders;
|
||||
if (!Array.isArray(rawBrewers) || !Array.isArray(rawGrinders))
|
||||
return res.status(400).json({ ok: false, code: "bad_gear" });
|
||||
const brewers = [...new Set(rawBrewers)];
|
||||
if (
|
||||
brewers.length > BREW_METHODS.length ||
|
||||
!brewers.every((k) => BREW_METHOD_KEYS.has(k))
|
||||
)
|
||||
return res
|
||||
.status(400)
|
||||
.json({ ok: false, code: "bad_gear", error: "unknown brewer key" });
|
||||
const grinders = [
|
||||
...new Set(
|
||||
rawGrinders
|
||||
.filter((g) => typeof g === "string")
|
||||
.map((g) => g.trim().slice(0, 100))
|
||||
.filter(Boolean),
|
||||
),
|
||||
].slice(0, 20);
|
||||
const row = (
|
||||
await db.query(
|
||||
`INSERT INTO user_gear(user_id,brewers,grinders,updated_at) VALUES($1,$2,$3,now())
|
||||
ON CONFLICT (user_id) DO UPDATE SET brewers=$2, grinders=$3, updated_at=now()
|
||||
RETURNING brewers,grinders`,
|
||||
[req.user.id, JSON.stringify(brewers), JSON.stringify(grinders)],
|
||||
)
|
||||
).rows[0];
|
||||
res.json({ ok: true, gear: { brewers: row.brewers, grinders: row.grinders } });
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/brew-methods", requireAuth, (_req, res) =>
|
||||
res.json({
|
||||
ok: true,
|
||||
|
||||
Reference in New Issue
Block a user