batchCooking/apps/web/src/features/house/SourceSelect.tsx
Nicolas 766d48eaa5 feat(recipes): préférences de sources par foyer + distinction officielle/non-officielle
Répond à deux besoins : permettre à chaque foyer de choisir quelles
sources apparaissent dans ses onglets de recettes, et distinguer les
sources à API officielle des sources scrapées.

- RecipeSourceAdapter.official (booléen, sans défaut — chaque
  adaptateur doit le déclarer explicitement) synchronisé sur
  Source.official par syncRecipeSources.
- HouseSource : table de jointure opt-in (House <-> Source) — aucune
  ligne = source masquée. Un foyer nouvellement créé ne voit aucune
  source tant qu'il ne les active pas explicitement.
- GET /reference/sources (catalogue des sources implémentées, avec le
  flag officiel).
- GET/PATCH /house/current/sources (lecture/remplacement complet des
  sources activées par le foyer courant).
- recipe.service.ts : sourceVisibilityWhere() filtre désormais TOUS
  les onglets (perso/foyer/publique/favoris) — une recette sans
  source reste toujours visible ; une recette importée ne l'est que
  si sa source est activée pour le foyer du viewer. Un viewer sans
  foyer ne voit aucune recette sourcée.

Côté web :
- Nouvelle étape /onboarding/sources dans le wizard d'inscription,
  atteinte uniquement si un foyer vient d'être créé/rejoint (sinon on
  saute direct aux allergènes) ; s'auto-saute aussi si aucune source
  n'est encore implémentée (catalogue vide aujourd'hui).
- Nouvelle section « Sources de recettes » dans /parametres/foyer
  (masquée dans les mêmes conditions), avec sauvegarde à la volée
  (même pattern que les autres préférences hot-saved).
- SourceSelect (features/house/), grille de cases à cocher avec badge
  officiel/non-officielle, sur le même principe qu'AllergySelect.

172 tests backend passent (dont 25 nouveaux). Build et lint propres.
Vérifié manuellement en navigateur : le parcours d'onboarding saute
bien l'étape sources (catalogue vide) et affiche « 4 sur 4 » quand un
foyer a été créé ; la section paramètres reste invisible tant
qu'aucune source n'existe.

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

57 lines
2.1 KiB
TypeScript

import type { SourceView } from "@batch-cooking/shared";
import { useTranslation } from "react-i18next";
import { CheckboxOption } from "../../components/ui/Checkbox";
import "./house-forms.scss";
interface SourceSelectProps {
legend: string;
sources: SourceView[];
value: number[];
onChange: (sourceIds: number[]) => void;
}
/**
* Multi-select (checkbox grid) for which recipe sources a household sees
* in its recipe tabs — same "grid of `CheckboxOption`s" shape as
* `AllergySelect` (`features/profile/`), but household-scoped rather than
* per-user, hence its own `features/house/` home. Used both by the
* onboarding wizard's source step and the `/parametres/foyer` settings
* page. An empty `value` is this household's normal starting state
* (opt-in — see `HouseSource` in schema.prisma), not an incomplete one.
*
* Unlike `AllergySelect`, a source's display text is its own `name`
* (`SourceView.name` — a proper noun like "Marmiton"), not resolved
* through `catalog.<domain>.<key>` i18n — nothing to translate. The
* `official`/`unofficial` badge next to it is what *is* translated, so a
* household can tell an official API apart from a scraped site before
* deciding whether to trust it.
*/
export function SourceSelect({ legend, sources, value, onChange }: SourceSelectProps) {
const { t } = useTranslation();
function toggle(id: number) {
onChange(value.includes(id) ? value.filter((existing) => existing !== id) : [...value, id]);
}
return (
<fieldset className="source-select">
<legend>{legend}</legend>
{sources.map((source) => {
const checked = value.includes(source.id);
return (
<CheckboxOption
key={source.id}
checked={checked}
onChange={() => toggle(source.id)}
className="source-select__option"
>
{source.name}
<span className={`source-select__badge ${source.official ? "is-official" : ""}`}>
{t(source.official ? "household.sources.official" : "household.sources.unofficial")}
</span>
</CheckboxOption>
);
})}
</fieldset>
);
}