feat(mobile): Sparade recept-sida (sökbar favoritlista) + länk på hemma (SAVED-1)

This commit is contained in:
Sven (AAMOS AI)
2026-08-16 00:55:32 +07:00
parent c2a8aa8fa1
commit d098bf705e
3 changed files with 70 additions and 0 deletions
+4
View File
@@ -114,6 +114,10 @@ export default function HomeScreen() {
<QuickLink label={t("profile.title")} glyph="⚙️" onPress={() => router.push("/profile")} /> <QuickLink label={t("profile.title")} glyph="⚙️" onPress={() => router.push("/profile")} />
</Row> </Row>
<Card onPress={() => router.push("/saved-recipes")}>
<Body> Dina sparade recept</Body>
</Card>
{budget.data && ( {budget.data && (
<Card> <Card>
<Heading>{t("home.budget")}</Heading> <Heading>{t("home.budget")}</Heading>
+1
View File
@@ -84,6 +84,7 @@ export default function RootLayout() {
<Stack.Screen name="meal-boxes" options={{ title: t("mealbox.title"), presentation: "modal" }} /> <Stack.Screen name="meal-boxes" options={{ title: t("mealbox.title"), presentation: "modal" }} />
<Stack.Screen name="household" options={{ title: t("home.household"), presentation: "modal" }} /> <Stack.Screen name="household" options={{ title: t("home.household"), presentation: "modal" }} />
<Stack.Screen name="memory" options={{ title: t("memory.title") }} /> <Stack.Screen name="memory" options={{ title: t("memory.title") }} />
<Stack.Screen name="saved-recipes" options={{ title: "Sparade recept" }} />
<Stack.Screen name="profile" options={{ title: t("profile.title"), presentation: "modal" }} /> <Stack.Screen name="profile" options={{ title: t("profile.title"), presentation: "modal" }} />
<Stack.Screen <Stack.Screen
name="paywall" name="paywall"
+65
View File
@@ -0,0 +1,65 @@
import { useState } from "react";
import { router } from "expo-router";
import { useQuery } from "@tanstack/react-query";
import { api } from "@/lib/api";
import { Card, EmptyState, ErrorView, Heading, Input, LoadingView, Screen, Small } from "@/components/ui";
import { spacing } from "@/lib/theme";
/** Dina sparade recept (favoriter) sökbar lista. Backend: GET /v1/recipes/favorites/mine. */
interface SavedRecipe {
id: string;
titleSv: string;
totalTimeMinutes: number | null;
nutritionPerPortion: { kcal?: number } | null;
}
interface FavoritesResponse {
recipes: SavedRecipe[];
}
export default function SavedRecipesScreen() {
const [query, setQuery] = useState("");
const favs = useQuery<FavoritesResponse>({
queryKey: ["favorites-mine"],
queryFn: () => api<FavoritesResponse>("/v1/recipes/favorites/mine"),
});
if (favs.isLoading) return <LoadingView />;
if (favs.isError) return <ErrorView onRetry={() => void favs.refetch()} />;
const all = favs.data?.recipes ?? [];
const q = query.trim().toLowerCase();
const filtered = q ? all.filter((r) => r.titleSv.toLowerCase().includes(q)) : all;
return (
<Screen style={{ gap: spacing.md }}>
<Input
placeholder="Sök bland dina sparade recept"
autoCapitalize="none"
value={query}
onChangeText={setQuery}
/>
{all.length === 0 ? (
<EmptyState text="Du har inga sparade recept än. Tryck på ☆ Spara på ett recept så hamnar det här." />
) : filtered.length === 0 ? (
<EmptyState text="Inga sparade recept matchar din sökning." />
) : (
filtered.map((r) => (
<Card key={r.id} onPress={() => router.push(`/recipe/${r.id}`)} style={{ gap: spacing.xs }}>
<Heading>{r.titleSv}</Heading>
<Small>
{[
r.totalTimeMinutes != null ? `${r.totalTimeMinutes} min` : null,
r.nutritionPerPortion?.kcal != null
? `${Math.round(r.nutritionPerPortion.kcal)} kcal`
: null,
]
.filter(Boolean)
.join(" · ")}
</Small>
</Card>
))
)}
</Screen>
);
}