TICKET-005: Frontend daily view — milestone M1 complete

- Dashboard: progress bar vs target, entries grouped by meal slot,
  edit/delete inline, date navigation (UTC-safe shiftDate helper)
- Add Food manual creation form, search-and-log flow with live preview
- Minimal target form; loading/error/empty states throughout
- Stores (current date, log, summary) with summary refresh on mutation
- All HTTP via lib/api.js; formatting via lib/format.js; runes only
- Full suites green (backend 104 passed, frontend vitest + build)
This commit is contained in:
Craig
2026-07-26 13:55:39 +01:00
parent 5372e8cbca
commit b69661c997
11 changed files with 1375 additions and 47 deletions
+12 -1
View File
@@ -11,8 +11,16 @@ async function request(path, options = {}) {
...options,
})
if (!resp.ok) {
throw new Error(`API ${options.method ?? 'GET'} ${path} failed: ${resp.status}`)
// Try to extract backend error detail for a better message
let detail = `${resp.status}`
try {
const body = await resp.json()
if (body.detail) detail = body.detail
} catch { /* can't parse — use status code */ }
throw new Error(`API ${options.method ?? 'GET'} ${path} failed: ${detail}`)
}
// 204 No Content (DELETE) — return success indicator
if (resp.status === 204) return { ok: true }
return resp.json()
}
@@ -26,10 +34,13 @@ export const api = {
// Daily log (spec §3.3) — dates are YYYY-MM-DD strings end-to-end (spec §8.3 rule 4)
getLog: (date) => request(`/api/log?date=${date}`),
addLogEntry: (entry) => request('/api/log', { method: 'POST', body: JSON.stringify(entry) }),
updateLogEntry: (id, data) => request(`/api/log/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
deleteLogEntry: (id) => request(`/api/log/${id}`, { method: 'DELETE' }),
getSummary: (date) => request(`/api/log/summary?date=${date}`),
// Targets (spec §3.4)
getCurrentTarget: () => request('/api/targets/current'),
getTargets: () => request('/api/targets'),
createTarget: (target) => request('/api/targets', { method: 'POST', body: JSON.stringify(target) }),
// OFF proxy (spec §3.5) — the frontend never calls OFF directly
offProduct: (barcode) => request(`/api/off/product/${barcode}`),
offSearch: (q) => request(`/api/off/search?q=${encodeURIComponent(q)}`),