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:
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 & 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
|
||||
|
||||
Reference in New Issue
Block a user