From 3d8bd68fe00663720afa022d4b7ce49fc90bf4ac Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 19 Aug 2026 12:29:21 +0000 Subject: [PATCH] feat(vad-ska-vi-ata): identisk vy i alla flikar + enhetliga rutor Alla kategori-flikar anvander nu SAMMA rekommendations-motor (what-to-eat) som 'Rekommenderat', sa vyn blir identisk overallt: - sok fungerar i alla flikar, %-hemma och stjarnor visas, samma kort-layout, och samma paginering med 'Visa fler' (5 at gangen). - Backend: what-to-eat tar nu valfri ?tag= (Baka-fliken filtrerar pa 'baking' i stallet for maltidstyp; mealType styr bara poangsattningen). - Enhetliga chip-rutor: Button far en style-prop, flikarna ligger i 2 kolumner med lika bred ruta (48%), centrerad text som krymper vid behov. - 'Toppval' visas bara pa forsta sidan. 'Alla recept & sok' kvar som lank. Full typecheck 20/20. --- apps/api/src/routes/recommendations.ts | 7 +- apps/mobile/src/app/(tabs)/index.tsx | 281 +++++++++------------ apps/mobile/src/components/ui.tsx | 24 +- packages/validation/src/recommendations.ts | 5 +- 4 files changed, 147 insertions(+), 170 deletions(-) diff --git a/apps/api/src/routes/recommendations.ts b/apps/api/src/routes/recommendations.ts index 550260a..a9cb66f 100644 --- a/apps/api/src/routes/recommendations.ts +++ b/apps/api/src/routes/recommendations.ts @@ -116,14 +116,17 @@ export async function recommendationRoutes(app: FastifyInstance) { const strictestSpice = Math.min(...allPrefs.map((p) => p.spiceLevelMax), 5); const myPrefs = allPrefs.find((p) => p.userId === req.userId); - // --- 3. Kandidater: publicerade recept för måltidstypen --- + // --- 3. Kandidater: publicerade recept för måltidstypen (eller taggen) --- + // Baka-fliken skickar tag=baking i stället för en måltidstyp. const candidates = await app.db .select() .from(schema.recipes) .where( and( eq(schema.recipes.status, "published"), - sql`${q.mealType} = ANY(${schema.recipes.mealTypes})`, + q.tag + ? sql`${q.tag} = ANY(${schema.recipes.tags})` + : sql`${q.mealType} = ANY(${schema.recipes.mealTypes})`, ), ) .limit(200); diff --git a/apps/mobile/src/app/(tabs)/index.tsx b/apps/mobile/src/app/(tabs)/index.tsx index 8c74779..28c334e 100644 --- a/apps/mobile/src/app/(tabs)/index.tsx +++ b/apps/mobile/src/app/(tabs)/index.tsx @@ -49,25 +49,18 @@ interface WhatToEatResponse { mealBoxSuggestions: MealBoxSuggestion[]; context: { activeHolidays: string[]; remainingKcal: number; remainingProteinG: number }; } -interface BrowseRecipe { - id: string; - titleSv: string; - title?: string; - totalTimeMinutes: number | null; - nutritionPerPortion: { kcal?: number } | null; - ratingAverage: number | null; - ratingCount: number; -} -// Kategori-flikar direkt på förstasidan. null = personliga rekommendationer; -// annars filtreras recepten IN-PLACE på samma sida (ingen navigering bort). -// De flesta filtrerar på måltidstyp; "Baka" på taggen "baking". +// Kategori-flikar. Alla använder SAMMA rekommendations-motor (what-to-eat) så +// vyn blir identisk överallt: sök, %-hemma, stjärnor och "Visa fler". De flesta +// filtrerar på måltidstyp; "Baka" på taggen "baking" (ctxMeal styr bara +// poängsättningen så bakverk inte nedviktas som "fel måltid"). const CATS: ReadonlyArray<{ key: string; labelKey: string | null; label?: string; mealType?: string; tag?: string; + ctxMeal?: string; glyph: string; }> = [ { key: "breakfast", labelKey: "myday.mealType.breakfast", mealType: "breakfast", glyph: "🥣" }, @@ -75,7 +68,7 @@ const CATS: ReadonlyArray<{ { 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: "🥐" }, + { key: "baking", labelKey: null, label: "Baka", tag: "baking", ctxMeal: "dessert", glyph: "🥐" }, ]; const errText = (e: unknown): string | undefined => @@ -91,31 +84,28 @@ export default function WhatToEatScreen() { const hour = new Date().getHours(); const currentMeal = hour < 10 ? "breakfast" : hour < 14 ? "lunch" : "dinner"; - // Personliga rekommendationer (bara i "Rekommenderat"-läget). view=default ger - // en balanserad rankning; limit=20 är schemats maxgräns. - const query = useQuery({ - queryKey: ["what-to-eat", submittedCraving, currentMeal], - queryFn: () => - api( - `/v1/recommendations/what-to-eat?limit=20&view=default&mealType=${currentMeal}${submittedCraving ? `&craving=${encodeURIComponent(submittedCraving)}` : ""}`, - ), - enabled: cat === null, - }); + const activeCat = cat === null ? null : CATS.find((c) => c.key === cat); - // Kategori-bläddring in-place (när en flik är vald). - const browse = useQuery({ - queryKey: ["wte-browse", cat], + const query = useQuery({ + queryKey: ["what-to-eat", cat, submittedCraving, currentMeal], queryFn: () => { - const c = CATS.find((x) => x.key === cat); - let path = "/v1/recipes?limit=50"; - if (c?.mealType) path += `&mealType=${c.mealType}`; - if (c?.tag) path += `&tags=${c.tag}`; - return api<{ recipes: BrowseRecipe[] }>(path); + let path = "/v1/recommendations/what-to-eat?limit=20&view=default"; + if (activeCat?.tag) { + path += `&tag=${activeCat.tag}&mealType=${activeCat.ctxMeal ?? "dessert"}`; + } else { + path += `&mealType=${activeCat?.mealType ?? currentMeal}`; + } + if (submittedCraving) path += `&craving=${encodeURIComponent(submittedCraving)}`; + return api(path); }, - enabled: cat !== null, placeholderData: keepPreviousData, }); + const selectCat = (value: string | null) => { + setCat(value); + setPage(0); + }; + const allRecs = query.data?.recommendations ?? []; const shownRecs: Recommendation[] = allRecs.length ? Array.from( @@ -144,19 +134,34 @@ export default function WhatToEatScreen() { {t("wte.subtitle")} - {/* Kategori-flikar – filtrerar på DENNA sida, ingen navigering bort. */} - + {/* Sök (fungerar i alla flikar) */} + { + setSubmittedCraving(craving); + setPage(0); + }} + returnKeyType="search" + /> + + + {/* Kategori-flikar med enhetliga rutor (2 kolumner). Filtrerar in-place. */} +