b69661c997
- 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)
48 lines
2.1 KiB
JavaScript
48 lines
2.1 KiB
JavaScript
/**
|
|
* All HTTP goes through this module (spec §8.2 rule 1).
|
|
* No raw fetch() in components. Shapes mirror backend schemas.py (spec §8.3 rule 1).
|
|
*/
|
|
|
|
const BASE_URL = '' // same origin; Vite dev server proxies /api to the backend
|
|
|
|
async function request(path, options = {}) {
|
|
const resp = await fetch(`${BASE_URL}${path}`, {
|
|
headers: { 'Content-Type': 'application/json' },
|
|
...options,
|
|
})
|
|
if (!resp.ok) {
|
|
// 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()
|
|
}
|
|
|
|
export const api = {
|
|
health: () => request('/api/health'),
|
|
// Foods (spec §3.1)
|
|
searchFoods: (q) => request(`/api/foods?q=${encodeURIComponent(q)}`),
|
|
recentFoods: (limit = 10) => request(`/api/foods/recent?limit=${limit}`),
|
|
getFood: (id) => request(`/api/foods/${id}`),
|
|
createFood: (food) => request('/api/foods', { method: 'POST', body: JSON.stringify(food) }),
|
|
// 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)}`),
|
|
}
|