batchCooking/apps/web/src/pages/HomePage.tsx
Nicolas ac7f8d9685 Web: HomePage becomes the weekly planning view (step 4/5)
- ApiClient.getCurrentPlanning() — GET /planning/current.
- HomePage.tsx: replaces the old greeting card (now redundant with
  AppLayout's sidebar) with the household's current planning — loading /
  error / empty ("aucun planning pour cette semaine") / loaded (table of
  weekDay/meal/recipe) states, modeled as a discriminated union so an
  impossible combination (e.g. loading with data) can't be represented.
  No invented weekday/meal grid — the API's `weekDay`/`meal` are free-form
  strings (no enum exists yet in the schema), so this renders the items
  as returned rather than assuming a specific vocabulary.
- locales/fr/translation.json: home.* replaced (title/loading/error/
  empty/table.*), old greeting/logout keys removed (superseded by
  layout.greeting/layout.logout from the AppLayout commit).
- cypress/e2e/auth.cy.ts: updated the now-stale "Bonjour Alice Martin"
  assertions (greeting moved to the sidebar, first name only) and added
  GET /planning/current intercepts so these specs don't depend on a real
  backend. Manually verified end-to-end against a real API in the browser
  preview (empty state + a seeded planning) — Cypress itself can't run
  headless Chromium in this sandboxed dev environment (confirmed
  pre-existing on main, unrelated to this change); CI runs the real
  suite.

Verified with `pnpm --filter web build` after each of steps 2-4 to keep
every commit in this sequence independently buildable.
2026-08-16 21:06:15 +02:00

81 lines
2.7 KiB
TypeScript

import type { PlanningView } from "@batch-cooking/shared";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { apiClient } from "../api/client";
import "./HomePage.scss";
/** Load state for the `GET /planning/current` call — a discriminated union so a stale/impossible combination (e.g. "loading" with data) can't be represented. */
type PlanningState =
| { status: "loading" }
| { status: "loaded"; planning: PlanningView | null }
| { status: "error" };
/**
* Landing page for an authenticated visitor — the household's current
* planning. Behind {@link RequireAuth} (via `AppLayout`), so this only
* renders once a session is confirmed; the planning itself still has to be
* fetched separately, hence the loading/error/empty/loaded states below.
* `null` from the API is a normal, common state (no planning created yet),
* not an error — see `apps/api`'s `planning.service.ts`.
*/
export function HomePage() {
const { t } = useTranslation();
const [state, setState] = useState<PlanningState>({ status: "loading" });
useEffect(() => {
// Guards against setting state after unmount (e.g. the user navigates
// away before the request resolves) — no cleanup-worthy resource here,
// just avoids a "set state on unmounted component" warning.
let cancelled = false;
apiClient
.getCurrentPlanning()
.then((planning) => {
if (!cancelled) setState({ status: "loaded", planning });
})
.catch(() => {
if (!cancelled) setState({ status: "error" });
});
return () => {
cancelled = true;
};
}, []);
return (
<div className="home-page">
<h1>{t("home.title")}</h1>
{state.status === "loading" && <p className="home-page__status">{t("home.loading")}</p>}
{state.status === "error" && (
<p className="home-page__status home-page__status--error">{t("home.error")}</p>
)}
{state.status === "loaded" && state.planning === null && (
<p className="home-page__status">{t("home.empty")}</p>
)}
{state.status === "loaded" && state.planning !== null && (
<table className="planning-table">
<thead>
<tr>
<th>{t("home.table.day")}</th>
<th>{t("home.table.meal")}</th>
<th>{t("home.table.recipe")}</th>
</tr>
</thead>
<tbody>
{state.planning.items.map((item) => (
<tr key={item.id}>
<td>{item.weekDay}</td>
<td>{item.meal}</td>
<td>{item.recipe.name}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
);
}