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.
This commit is contained in:
Claude
2026-08-19 12:58:59 +00:00
parent 3d8bd68fe0
commit dc5df13f25
5 changed files with 68 additions and 58 deletions
+11 -4
View File
@@ -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);
+11 -14
View File
@@ -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<string, string>();
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() {
<Screen>
<Title>{t("home.title")}</Title>
{needsProfile && (
<Card onPress={() => { setOnboardingStep(profileStep); router.push("/onboarding"); }}>
<Card
onPress={() => {
setOnboardingStep(profileStep);
router.push("/onboarding");
}}
>
<Heading>{t("home.completeProfile.title")}</Heading>
<Small>{t("home.completeProfile.body")}</Small>
</Card>
@@ -125,18 +131,9 @@ export default function HomeScreen() {
<QuickLink label={t("profile.title")} glyph="⚙️" onPress={() => router.push("/profile")} />
</Row>
<Row>
<View style={{ flex: 1 }}>
<Card onPress={() => router.push("/recipes")}>
<Body>📖 Bläddra bland recept</Body>
</Card>
</View>
<View style={{ flex: 1 }}>
<Card onPress={() => router.push("/saved-recipes")}>
<Body> Sparade recept</Body>
</Card>
</View>
</Row>
<Card onPress={() => router.push("/saved-recipes")}>
<Body> Dina sparade recept</Body>
</Card>
{/* Matlager städat, öppnas per plats eller som helhet (spec §8). */}
<Card>
+42 -38
View File
@@ -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<string | null>(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<WhatToEatResponse>(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() {
<Small>{t("wte.subtitle")}</Small>
<Spacer size={spacing.sm} />
{/* Sök (fungerar i alla flikar) */}
{/* Sök bland recept (fungerar i alla flikar, söker på namn). */}
<Input
placeholder={t("wte.cravingPlaceholder")}
value={craving}
onChangeText={setCraving}
onSubmitEditing={() => {
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 && (
<>
<Spacer size={spacing.xs} />
<Row style={{ flexWrap: "wrap" }}>
{query.data.context.activeHolidays.map((holiday) => (
<Pressable key={holiday} onPress={() => submitSearch(holiday)}>
<Tag label={`🎉 ${holiday}`} tone="accent" />
</Pressable>
))}
</Row>
</>
)}
<Spacer size={spacing.sm} />
{/* Kategori-flikar med enhetliga rutor (2 kolumner). Filtrerar in-place. */}
@@ -165,9 +183,6 @@ export default function WhatToEatScreen() {
/>
))}
</Row>
<Pressable onPress={() => router.push("/recipes")}>
<Small style={{ color: colors.primary }}>🔍 Alla recept &amp; sök</Small>
</Pressable>
<Spacer size={spacing.sm} />
{query.isLoading && <LoadingView />}
@@ -177,24 +192,7 @@ export default function WhatToEatScreen() {
{query.data && (
<>
{query.data.context.activeHolidays.length > 0 && (
<Row style={{ flexWrap: "wrap" }}>
{query.data.context.activeHolidays.map((holiday) => (
<Pressable
key={holiday}
onPress={() => {
setCraving(holiday);
setSubmittedCraving(holiday);
setPage(0);
}}
>
<Tag label={`🎉 ${holiday}`} tone="accent" />
</Pressable>
))}
</Row>
)}
{cat === null && query.data.mealBoxSuggestions.length > 0 && (
{cat === null && !submittedSearch && query.data.mealBoxSuggestions.length > 0 && (
<>
<Heading>{t("wte.mealBoxFirst")}</Heading>
{query.data.mealBoxSuggestions.map((box) => (
@@ -213,13 +211,19 @@ export default function WhatToEatScreen() {
</>
)}
{allRecs.length === 0 && <EmptyState text={t("wte.empty")} />}
{allRecs.length === 0 && (
<EmptyState
text={submittedSearch ? "Inga recept matchar din sökning." : t("wte.empty")}
/>
)}
{shownRecs.map((rec, index) => (
<Card key={rec.recipeId} onPress={() => router.push(`/recipe/${rec.recipeId}`)}>
<Row style={{ justifyContent: "space-between", alignItems: "center" }}>
<Heading>{rec.titleSv}</Heading>
{page === 0 && index === 0 && <Tag label="Toppval" tone="accent" />}
{page === 0 && index === 0 && !submittedSearch && (
<Tag label="Toppval" tone="accent" />
)}
</Row>
<Row>
{rec.ratingCount > 0 && rec.ratingAverage != null && (
@@ -254,7 +258,7 @@ export default function WhatToEatScreen() {
</Card>
))}
{allRecs.length > 0 && (
{allRecs.length > 5 && (
<>
<Spacer size={spacing.sm} />
<Button