From dc5df13f2517702319214e6a005d8a919fbd7362 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 19 Aug 2026 12:58:59 +0000 Subject: [PATCH] fix(vad-ska-vi-ata): fungerande titelsok, hogtider under sokrutan, ratta bakverk, ta bort separata bladdra-lankar - Sok fungerar nu: what-to-eat tar ?search= och filtrerar recept pa titel/ beskrivning BLAND ALLA recept (t.ex. 'pannkaka' hittar pannkakor), oavsett vald flik. Sokrutan skickar nu search i stallet for craving. - Hogtids-/sasongsknapparna (kraftskiva, grill, skolstart) ligger nu direkt UNDER sokrutan, inte nere bland recept-forslagen. - Saffransbullar + lussekatter ur 'Frukost' (mealTypes dessert+breakfast -> dessert). Bakverk hor inte till frukost i Sverige. - Bort med 'Alla recept & sok'-lanken pa forstasidan och 'Bladdra bland recept' pa Hemma - allt nas nu via flikarna/soket pa 'Vad ska vi ata'. Full typecheck gron. --- apps/api/src/routes/recommendations.ts | 15 +++- apps/mobile/src/app/(tabs)/home.tsx | 25 +++--- apps/mobile/src/app/(tabs)/index.tsx | 80 ++++++++++--------- .../src/seed/data/verified-recipes.ts | 4 +- packages/validation/src/recommendations.ts | 2 + 5 files changed, 68 insertions(+), 58 deletions(-) diff --git a/apps/api/src/routes/recommendations.ts b/apps/api/src/routes/recommendations.ts index a9cb66f..c296a02 100644 --- a/apps/api/src/routes/recommendations.ts +++ b/apps/api/src/routes/recommendations.ts @@ -1,5 +1,5 @@ import type { FastifyInstance } from "fastify"; -import { and, desc, eq, gt, inArray, isNull, or, sql } from "drizzle-orm"; +import { and, desc, eq, gt, ilike, inArray, isNull, or, sql } from "drizzle-orm"; import { schema } from "@app/database"; import type { MemoryItem, TasteSignal } from "@app/shared-types"; import { whatToEatQuerySchema } from "@app/validation"; @@ -124,9 +124,16 @@ export async function recommendationRoutes(app: FastifyInstance) { .where( and( eq(schema.recipes.status, "published"), - q.tag - ? sql`${q.tag} = ANY(${schema.recipes.tags})` - : sql`${q.mealType} = ANY(${schema.recipes.mealTypes})`, + // Titelsök (t.ex. "pannkaka") söker BLAND ALLA recept, oavsett flik. + // Utan sökterm filtreras i stället på flikens tagg/måltidstyp. + q.search + ? or( + ilike(schema.recipes.titleSv, `%${q.search}%`), + ilike(schema.recipes.descriptionSv, `%${q.search}%`), + ) + : 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)/home.tsx b/apps/mobile/src/app/(tabs)/home.tsx index 0bc2492..3aa63ae 100644 --- a/apps/mobile/src/app/(tabs)/home.tsx +++ b/apps/mobile/src/app/(tabs)/home.tsx @@ -79,7 +79,8 @@ export default function HomeScreen() { // Distinkta platser (Kyl/Frys/Skafferi …) för genvägsknappar in i Matlager. const locations = useMemo(() => { const seen = new Map(); - for (const it of items) if (!seen.has(it.locationType)) seen.set(it.locationType, it.locationName); + for (const it of items) + if (!seen.has(it.locationType)) seen.set(it.locationType, it.locationName); return [...seen.entries()] .map(([type, name]) => ({ type, name })) .sort((a, b) => orderIndex(a.type) - orderIndex(b.type)); @@ -97,7 +98,12 @@ export default function HomeScreen() { {t("home.title")} {needsProfile && ( - { setOnboardingStep(profileStep); router.push("/onboarding"); }}> + { + setOnboardingStep(profileStep); + router.push("/onboarding"); + }} + > {t("home.completeProfile.title")} {t("home.completeProfile.body")} @@ -125,18 +131,9 @@ export default function HomeScreen() { router.push("/profile")} /> - - - router.push("/recipes")}> - 📖 Bläddra bland recept - - - - router.push("/saved-recipes")}> - ⭐ Sparade recept - - - + router.push("/saved-recipes")}> + ⭐ Dina sparade recept + {/* Matlager – städat, öppnas per plats eller som helhet (spec §8). */} diff --git a/apps/mobile/src/app/(tabs)/index.tsx b/apps/mobile/src/app/(tabs)/index.tsx index 28c334e..135d6ab 100644 --- a/apps/mobile/src/app/(tabs)/index.tsx +++ b/apps/mobile/src/app/(tabs)/index.tsx @@ -51,7 +51,7 @@ interface WhatToEatResponse { } // 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 +// vyn blir identisk överallt: %-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<{ @@ -75,8 +75,8 @@ const errText = (e: unknown): string | undefined => e instanceof ApiError ? `[${e.status}] ${e.message}` : e instanceof Error ? e.message : undefined; export default function WhatToEatScreen() { - const [craving, setCraving] = useState(""); - const [submittedCraving, setSubmittedCraving] = useState(""); + const [searchText, setSearchText] = useState(""); + const [submittedSearch, setSubmittedSearch] = useState(""); const [page, setPage] = useState(0); const [cat, setCat] = useState(null); // null = Rekommenderat const { track } = useAnalytics(); @@ -87,7 +87,7 @@ export default function WhatToEatScreen() { const activeCat = cat === null ? null : CATS.find((c) => c.key === cat); const query = useQuery({ - queryKey: ["what-to-eat", cat, submittedCraving, currentMeal], + queryKey: ["what-to-eat", cat, submittedSearch, currentMeal], queryFn: () => { let path = "/v1/recommendations/what-to-eat?limit=20&view=default"; if (activeCat?.tag) { @@ -95,12 +95,18 @@ export default function WhatToEatScreen() { } else { path += `&mealType=${activeCat?.mealType ?? currentMeal}`; } - if (submittedCraving) path += `&craving=${encodeURIComponent(submittedCraving)}`; + // Sök bland alla recept (titel/beskrivning) oavsett vald flik. + if (submittedSearch) path += `&search=${encodeURIComponent(submittedSearch)}`; return api(path); }, placeholderData: keepPreviousData, }); + const submitSearch = (value: string) => { + setSearchText(value); + setSubmittedSearch(value.trim()); + setPage(0); + }; const selectCat = (value: string | null) => { setCat(value); setPage(0); @@ -119,7 +125,7 @@ export default function WhatToEatScreen() { track( recommendationsViewed({ properties: { - craving: submittedCraving || undefined, + craving: submittedSearch || undefined, count: query.data.recommendations.length, hasMealBoxSuggestions: query.data.mealBoxSuggestions.length > 0, }, @@ -134,17 +140,29 @@ export default function WhatToEatScreen() { {t("wte.subtitle")} - {/* Sök (fungerar i alla flikar) */} + {/* Sök bland recept (fungerar i alla flikar, söker på namn). */} { - setSubmittedCraving(craving); - setPage(0); - }} + placeholder="Sök recept, t.ex. pannkaka…" + value={searchText} + onChangeText={setSearchText} + onSubmitEditing={() => submitSearch(searchText)} returnKeyType="search" + autoCorrect={false} /> + + {/* Aktuella högtider/säsonger – snabbknappar direkt under sökrutan. */} + {query.data && query.data.context.activeHolidays.length > 0 && ( + <> + + + {query.data.context.activeHolidays.map((holiday) => ( + submitSearch(holiday)}> + + + ))} + + + )} {/* Kategori-flikar med enhetliga rutor (2 kolumner). Filtrerar in-place. */} @@ -165,9 +183,6 @@ export default function WhatToEatScreen() { /> ))} - router.push("/recipes")}> - 🔍 Alla recept & sök - {query.isLoading && } @@ -177,24 +192,7 @@ export default function WhatToEatScreen() { {query.data && ( <> - {query.data.context.activeHolidays.length > 0 && ( - - {query.data.context.activeHolidays.map((holiday) => ( - { - setCraving(holiday); - setSubmittedCraving(holiday); - setPage(0); - }} - > - - - ))} - - )} - - {cat === null && query.data.mealBoxSuggestions.length > 0 && ( + {cat === null && !submittedSearch && query.data.mealBoxSuggestions.length > 0 && ( <> {t("wte.mealBoxFirst")} {query.data.mealBoxSuggestions.map((box) => ( @@ -213,13 +211,19 @@ export default function WhatToEatScreen() { )} - {allRecs.length === 0 && } + {allRecs.length === 0 && ( + + )} {shownRecs.map((rec, index) => ( router.push(`/recipe/${rec.recipeId}`)}> {rec.titleSv} - {page === 0 && index === 0 && } + {page === 0 && index === 0 && !submittedSearch && ( + + )} {rec.ratingCount > 0 && rec.ratingAverage != null && ( @@ -254,7 +258,7 @@ export default function WhatToEatScreen() { ))} - {allRecs.length > 0 && ( + {allRecs.length > 5 && ( <>