fix(recept): bakverk ur mellanmal + sub-kategorier, bladdra pa forstasidan, relevantare forslag
Kategorisering (din notis - bakverk hor till Baka, inte mellanmal): - Sub-typer bread/bun/pie/cake/cookie i RECIPE_TAGS. - Bullar/kakor/pajer/chokladbollar -> dessert (inte snack) + sub-tagg; brod -> breakfast. Befintliga lussekatter/saffransbullar taggas 'bun'. - Browse: sub-chips under Baka (Brod/Bullar/Paj/Tartor & kakor/Smakakor). Bladdra pa forstasidan (din onskan): - Kategori-genvagar pa 'Vad ska vi ata' -> oppnar bladdraren forvald (/recipes?cat=). recipes.tsx laser ?cat=. Relevantare 'Vad ska vi ata' (snurrar samma triviala): - view=pantry -> view=default (tackning vager tungt men inte allenaradande, ger variation), limit 15 -> 30 (storre pool sa 'Visa fler' upprepar senare). Felsokning inkopslista: - Tydligare felmeddelande (HTTP-status vs natverksfel) sa root cause syns. Full typecheck 20/20, invariant-test gront.
This commit is contained in:
@@ -50,6 +50,22 @@ interface WhatToEatResponse {
|
||||
context: { activeHolidays: string[]; remainingKcal: number; remainingProteinG: number };
|
||||
}
|
||||
|
||||
// Bläddra-genvägar direkt på förstasidan (öppnar recept-bläddraren förvald på
|
||||
// kategorin). Etiketter återanvänder myday.mealType.* där de finns.
|
||||
const BROWSE_CATS: ReadonlyArray<{
|
||||
key: string;
|
||||
labelKey: string | null;
|
||||
label?: string;
|
||||
glyph: string;
|
||||
}> = [
|
||||
{ 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: "🍰" },
|
||||
{ key: "baking", labelKey: null, label: "Baka", glyph: "🥐" },
|
||||
];
|
||||
|
||||
export default function WhatToEatScreen() {
|
||||
const [craving, setCraving] = useState("");
|
||||
const [submittedCraving, setSubmittedCraving] = useState("");
|
||||
@@ -59,17 +75,24 @@ export default function WhatToEatScreen() {
|
||||
const hour = new Date().getHours();
|
||||
const currentMeal = hour < 10 ? "breakfast" : hour < 14 ? "lunch" : "dinner";
|
||||
|
||||
// view=default ger en balanserad rankning (täckning väger tungt men inte
|
||||
// allenarådande som i pantry-vyn) → mer varierade, relevanta förslag i stället
|
||||
// för samma triviala högtäckningsrätter. Större pool (30) så "Visa fler"
|
||||
// bläddrar längre innan den upprepar.
|
||||
const query = useQuery({
|
||||
queryKey: ["what-to-eat", submittedCraving, currentMeal],
|
||||
queryFn: () =>
|
||||
api<WhatToEatResponse>(
|
||||
`/v1/recommendations/what-to-eat?limit=15&view=pantry&mealType=${currentMeal}${submittedCraving ? `&craving=${encodeURIComponent(submittedCraving)}` : ""}`,
|
||||
`/v1/recommendations/what-to-eat?limit=30&view=default&mealType=${currentMeal}${submittedCraving ? `&craving=${encodeURIComponent(submittedCraving)}` : ""}`,
|
||||
),
|
||||
});
|
||||
|
||||
const allRecs = query.data?.recommendations ?? [];
|
||||
const shownRecs: Recommendation[] = allRecs.length
|
||||
? Array.from({ length: Math.min(5, allRecs.length) }, (_, i) => allRecs[(page * 5 + i) % allRecs.length]).filter((r): r is Recommendation => r != null)
|
||||
? Array.from(
|
||||
{ length: Math.min(5, allRecs.length) },
|
||||
(_, i) => allRecs[(page * 5 + i) % allRecs.length],
|
||||
).filter((r): r is Recommendation => r != null)
|
||||
: [];
|
||||
|
||||
useEffect(() => {
|
||||
@@ -98,13 +121,30 @@ export default function WhatToEatScreen() {
|
||||
placeholder={t("wte.cravingPlaceholder")}
|
||||
value={craving}
|
||||
onChangeText={setCraving}
|
||||
onSubmitEditing={() => { setSubmittedCraving(craving); setPage(0); }}
|
||||
onSubmitEditing={() => {
|
||||
setSubmittedCraving(craving);
|
||||
setPage(0);
|
||||
}}
|
||||
returnKeyType="search"
|
||||
/>
|
||||
</View>
|
||||
</Row>
|
||||
<Spacer size={spacing.sm} />
|
||||
|
||||
{/* Bläddra bland recept per kategori direkt från förstasidan. */}
|
||||
<Row style={{ flexWrap: "wrap" }}>
|
||||
{BROWSE_CATS.map((c) => (
|
||||
<Button
|
||||
key={c.key}
|
||||
label={`${c.glyph} ${c.labelKey ? t(c.labelKey as never) : (c.label ?? "")}`}
|
||||
variant="ghost"
|
||||
onPress={() => router.push(`/recipes?cat=${c.key}`)}
|
||||
/>
|
||||
))}
|
||||
<Button label="Alla recept" variant="secondary" onPress={() => router.push("/recipes")} />
|
||||
</Row>
|
||||
<Spacer size={spacing.sm} />
|
||||
|
||||
{query.isLoading && <LoadingView />}
|
||||
{query.isError && <ErrorView onRetry={() => void query.refetch()} />}
|
||||
|
||||
@@ -156,7 +196,10 @@ export default function WhatToEatScreen() {
|
||||
</Row>
|
||||
<Row>
|
||||
{rec.ratingCount > 0 && rec.ratingAverage != null && (
|
||||
<Tag label={`★ ${rec.ratingAverage.toFixed(1).replace(".", ",")} · ${rec.ratingCount}`} tone="accent" />
|
||||
<Tag
|
||||
label={`★ ${rec.ratingAverage.toFixed(1).replace(".", ",")} · ${rec.ratingCount}`}
|
||||
tone="accent"
|
||||
/>
|
||||
)}
|
||||
<Tag
|
||||
label={t("wte.coverage", { pct: rec.coveragePercent })}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState } from "react";
|
||||
import { Alert, Pressable, View } from "react-native";
|
||||
import { router, useLocalSearchParams } from "expo-router";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { api } from "@/lib/api";
|
||||
import { api, ApiError } from "@/lib/api";
|
||||
import { t } from "@/lib/i18n";
|
||||
import { formatMinor } from "@/lib/money";
|
||||
import {
|
||||
@@ -162,8 +162,16 @@ export default function RecipeScreen() {
|
||||
`${count} ${count === 1 ? "vara" : "varor"} lades till.`,
|
||||
);
|
||||
},
|
||||
onError: (err) =>
|
||||
Alert.alert(t("common.oops"), err instanceof Error ? err.message : t("common.error")),
|
||||
onError: (err) => {
|
||||
// Visa tydlig diagnostik: HTTP-status vs nätverksfel (hjälper felsökning).
|
||||
const detail =
|
||||
err instanceof ApiError
|
||||
? `[${err.status}] ${err.message}`
|
||||
: err instanceof Error
|
||||
? err.message
|
||||
: t("common.error");
|
||||
Alert.alert(t("common.oops"), detail);
|
||||
},
|
||||
});
|
||||
|
||||
if (query.isLoading) return <LoadingView />;
|
||||
@@ -269,7 +277,10 @@ export default function RecipeScreen() {
|
||||
</Row>
|
||||
</Row>
|
||||
{ingredients.map((ing) => (
|
||||
<Row key={ing.id ?? (ing.displayName ?? ing.displayNameSv)} style={{ justifyContent: "space-between" }}>
|
||||
<Row
|
||||
key={ing.id ?? ing.displayName ?? ing.displayNameSv}
|
||||
style={{ justifyContent: "space-between" }}
|
||||
>
|
||||
<Body>
|
||||
{ing.displayName ?? ing.displayNameSv}
|
||||
{ing.optional ? " (valfritt)" : ""}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { router } from "expo-router";
|
||||
import { router, useLocalSearchParams } from "expo-router";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { api } from "@/lib/api";
|
||||
import { t } from "@/lib/i18n";
|
||||
@@ -63,19 +63,44 @@ const CATEGORIES: ReadonlyArray<{
|
||||
{ key: "baking", labelKey: null, label: "Baka", tag: "baking", glyph: "🥐" },
|
||||
];
|
||||
|
||||
// Under-kategorier som visas när "Baka" är vald. Varje bakverk bär både taggen
|
||||
// "baking" och en under-typ, så vi kan filtrera på under-typen direkt.
|
||||
const BAKING_SUBS: ReadonlyArray<{ key: string; label: string; tag?: string }> = [
|
||||
{ key: "all", label: "Alla" },
|
||||
{ key: "bread", label: "Bröd", tag: "bread" },
|
||||
{ key: "bun", label: "Bullar", tag: "bun" },
|
||||
{ key: "pie", label: "Paj", tag: "pie" },
|
||||
{ key: "cake", label: "Tårtor & kakor", tag: "cake" },
|
||||
{ key: "cookie", label: "Småkakor", tag: "cookie" },
|
||||
];
|
||||
|
||||
export default function BrowseRecipesScreen() {
|
||||
const [category, setCategory] = useState<string>("all");
|
||||
// Förvald kategori via ?cat= (från förstasidans genvägar); annars "Alla".
|
||||
const { cat } = useLocalSearchParams<{ cat?: string }>();
|
||||
const initialCategory = CATEGORIES.some((c) => c.key === cat) ? (cat as string) : "all";
|
||||
const [category, setCategory] = useState<string>(initialCategory);
|
||||
const [sub, setSub] = useState<string>("all"); // under-kategori, bara relevant för Baka
|
||||
const [search, setSearch] = useState("");
|
||||
const term = search.trim();
|
||||
|
||||
const selectCategory = (key: string) => {
|
||||
setCategory(key);
|
||||
if (key !== "baking") setSub("all"); // nollställ under-val när man lämnar Baka
|
||||
};
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ["browse-recipes", category, term],
|
||||
queryKey: ["browse-recipes", category, sub, 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 (cat?.mealType) path += `&mealType=${cat.mealType}`;
|
||||
if (cat?.tag) path += `&tags=${cat.tag}`;
|
||||
// Under Baka: filtrera på vald under-typ (bullar/bröd/…) i stället för "baking".
|
||||
if (category === "baking" && sub !== "all") {
|
||||
const subTag = BAKING_SUBS.find((s) => s.key === sub)?.tag;
|
||||
if (subTag) path = `/v1/recipes?limit=50&tags=${subTag}`;
|
||||
}
|
||||
if (term) path += `&search=${encodeURIComponent(term)}`;
|
||||
return api<{ recipes: BrowseRecipe[] }>(path);
|
||||
},
|
||||
@@ -101,11 +126,25 @@ export default function BrowseRecipesScreen() {
|
||||
key={c.key}
|
||||
label={`${c.glyph} ${c.labelKey ? t(c.labelKey as never) : (c.label ?? "Alla")}`}
|
||||
variant={category === c.key ? "secondary" : "ghost"}
|
||||
onPress={() => setCategory(c.key)}
|
||||
onPress={() => selectCategory(c.key)}
|
||||
/>
|
||||
))}
|
||||
</Row>
|
||||
|
||||
{/* Under-kategorier: visas bara när Baka är valt (bröd/bullar/paj/…). */}
|
||||
{category === "baking" && (
|
||||
<Row style={{ flexWrap: "wrap" }}>
|
||||
{BAKING_SUBS.map((s) => (
|
||||
<Button
|
||||
key={s.key}
|
||||
label={s.label}
|
||||
variant={sub === s.key ? "secondary" : "ghost"}
|
||||
onPress={() => setSub(s.key)}
|
||||
/>
|
||||
))}
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{query.isLoading ? (
|
||||
<LoadingView />
|
||||
) : query.isError ? (
|
||||
|
||||
Reference in New Issue
Block a user