feat(recept): Baka-kategori + 16 nya redaktionella recept

- Ny 'baking'-tagg (RECIPE_TAGS, text[] - ingen migration). Browse-vyn far
  en 'Baka'-chip som filtrerar pa taggen; befintliga lussekatter +
  saffransbullar taggas ocksa.
- extra-recipes.ts: 16 nya recept som fyller de glesa kategorierna -
  4 bak (kanelbullar, kardemummabullar, appelsmulpaj, frukostfrallor),
  3 frukost, 3 mellanmal (tomma kategorin -> fylld), 3 efterratt, 3 lunch.
- Endast verifierade canonical-ingredienser + berakningsbara enheter, sa
  seed harleder narings-/allergendata utan att krascha (run.ts 675/691).
- Nytt rent invariant-test (ingen DB) kor riktiga computeRecipeNutrition pa
  alla 16: id:n finns, naring berakningsbar, inga slug-krockar. 5 tester grona.
- Seed ar additiv (onConflictDoUpdate) - 'pnpm db:seed' lagger till + taggar
  om utan dataforlust. Full typecheck 20/20.
This commit is contained in:
Claude
2026-08-18 23:35:47 +00:00
parent 7d0fca0dad
commit 100d70dc2d
6 changed files with 799 additions and 12 deletions
+23 -9
View File
@@ -42,14 +42,26 @@ interface BrowseRecipe {
verificationStatus: string;
}
const CATEGORIES = [
// De flesta kategorier filtrerar på måltidstyp (MEAL_TYPES). "Baka" filtrerar i
// stället på taggen "baking" (bullar/bröd/pajer) därför bär varje kategori
// antingen ett mealType eller ett tag. Etiketter återanvänder myday.mealType.*
// där de finns; annars en egen svensk etikett.
const CATEGORIES: ReadonlyArray<{
key: string;
labelKey: string | null;
label?: string;
mealType?: string;
tag?: string;
glyph: string;
}> = [
{ key: "all", labelKey: null, glyph: "🍴" },
{ key: "breakfast", labelKey: "myday.mealType.breakfast", glyph: "🥣" },
{ key: "lunch", labelKey: "myday.mealType.lunch", glyph: "🥗" },
{ key: "dinner", labelKey: "myday.mealType.dinner", glyph: "🍽️" },
{ key: "snack", labelKey: "myday.mealType.snack", glyph: "🍎" },
{ key: "dessert", labelKey: "myday.mealType.dessert", glyph: "🍰" },
] as const;
{ key: "breakfast", labelKey: "myday.mealType.breakfast", mealType: "breakfast", glyph: "🥣" },
{ key: "lunch", labelKey: "myday.mealType.lunch", mealType: "lunch", glyph: "🥗" },
{ key: "dinner", labelKey: "myday.mealType.dinner", mealType: "dinner", glyph: "🍽️" },
{ key: "snack", labelKey: "myday.mealType.snack", mealType: "snack", glyph: "🍎" },
{ key: "dessert", labelKey: "myday.mealType.dessert", mealType: "dessert", glyph: "🍰" },
{ key: "baking", labelKey: null, label: "Baka", tag: "baking", glyph: "🥐" },
];
export default function BrowseRecipesScreen() {
const [category, setCategory] = useState<string>("all");
@@ -60,8 +72,10 @@ export default function BrowseRecipesScreen() {
queryKey: ["browse-recipes", category, term],
queryFn: () => {
// Bygg query-strängen som resten av appen (template + encodeURIComponent).
const cat = CATEGORIES.find((c) => c.key === category);
let path = "/v1/recipes?limit=50";
if (category !== "all") path += `&mealType=${category}`;
if (cat?.mealType) path += `&mealType=${cat.mealType}`;
if (cat?.tag) path += `&tags=${cat.tag}`;
if (term) path += `&search=${encodeURIComponent(term)}`;
return api<{ recipes: BrowseRecipe[] }>(path);
},
@@ -85,7 +99,7 @@ export default function BrowseRecipesScreen() {
{CATEGORIES.map((c) => (
<Button
key={c.key}
label={`${c.glyph} ${c.labelKey ? t(c.labelKey as never) : "Alla"}`}
label={`${c.glyph} ${c.labelKey ? t(c.labelKey as never) : (c.label ?? "Alla")}`}
variant={category === c.key ? "secondary" : "ghost"}
onPress={() => setCategory(c.key)}
/>