batchCooking/apps/web/src/features/profile/AllergySelect.tsx
Nicolas 9dd90359c9 Tests: dates fixes pour Planning + refonte des radio/checkbox
- apps/api: les tests Planning (mocha et cucumber) lisaient l'horloge
  systeme (new Date()/DateTime.utc()) pour construire leurs fixtures et
  interroger /planning, ce qui les rendait non deterministes. Ajoute
  test-support/reference-date.ts (TEST_REFERENCE_DATE, une date UTC
  fixe) et l'utilise dans planning.test.ts / planning.steps.ts a la
  place du systeme.

- apps/web: nouveau style global pour tous les radio/checkbox de
  l'app (theme-select, allergy-select, onboarding) - "carte
  selectionnable" : le controle natif reste reel/accessible mais
  visuellement cache, toute la ligne devient la surface interactive
  (bordure + fond teinte + coche au survol/selection). Corrige au
  passage le bug de fond qui causait le desalignement des radios sur
  /parametres/preferences-utilisateur (la regle generique
  input, select { width: 100% } de profile-forms.scss s'appliquait
  aussi aux checkbox/radio) et une regression de font-weight ou les
  lignes non selectionnees du theme apparaissaient en gras comme si
  elles l'etaient.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 22:36:02 +02:00

58 lines
2.4 KiB
TypeScript

import type { AllergyView } from "@batch-cooking/shared";
import "./profile-forms.scss";
interface AllergySelectProps {
legend: string;
allergies: AllergyView[];
value: number[];
onChange: (allergyIds: number[]) => void;
}
/**
* Multi-select (checkbox grid, not a native `<select multiple>` — far more
* discoverable/tappable, especially on the mobile viewport this app is
* eventually embedded into via Capacitor) for a group of allergens. Used
* both by the signup wizard's allergens step and the `/parametres/preferences`
* settings page, and rendered *twice* by each — once for allergies, once for
* intolerances (`AllergyView.kind` groups them; callers filter and pass
* two separate lists rather than this component knowing about the split).
* An empty `value` is a normal, valid state (no declared allergies, or
* this skippable step was skipped), not an incomplete one.
*
* `legend` (not a fixed internal label) — the same component serves both
* groups, only the heading differs. A `<fieldset>`/`<legend>` (not a bare
* `<label>`, which only associates with a single control) is the correct
* semantic label for a group of checkboxes.
*
* Receives `allergies` as a prop rather than fetching them itself — same
* rationale as `DietSelect`.
*/
export function AllergySelect({ legend, allergies, value, onChange }: AllergySelectProps) {
function toggle(id: number) {
onChange(value.includes(id) ? value.filter((existing) => existing !== id) : [...value, id]);
}
return (
<fieldset className="allergy-select">
<legend>{legend}</legend>
{allergies.map((allergy) => {
const checked = value.includes(allergy.id);
return (
<label
key={allergy.id}
// `is-selected` (not a `:has(:checked)` CSS rule) drives the
// selected look — chaining `:has(...):has(:checked)` to react to
// a *state* change (rather than a DOM mutation) turned out to be
// unreliable, so this stays a plain, always-correct React class
// instead of relying on CSS to derive it.
className={`allergy-select__option${checked ? " is-selected" : ""}`}
>
<input type="checkbox" checked={checked} onChange={() => toggle(allergy.id)} />
<span className="check-mark" aria-hidden="true" />
{allergy.name}
</label>
);
})}
</fieldset>
);
}