Files
Cibello-app/apps/mobile/src/app/log-meal.tsx
T
Claude 18388bb31c feat(i18n): översätt kvarvarande vyer + härda vakten (audit-backlog)
- API: översatt title/displayName/toName läggs nu bredvid Sv-fälten i favoriter,
  receptvarianter, /scaled, skapar-profiler, topplistor, memory-impact,
  substitutions — samt de denormaliserade titlarna (min dag, historik, matlådor,
  matlåde-förslag, veckoplan) via nullable recipeId med svensk fallback.
  10 endpoints, batchade resolvers (ett anrop per endpoint).
- Mobil: konsumerar de nya fälten med ?? Sv-fallback (swap-meal, sparade recept,
  varianter, min dag, logg, matlådor, veckoplan, inköpslista). Hårdkodade
  strängar -> t() (kitchen, scan-review, register, recept protein/Skapat av);
  decimalkomma -> Intl.NumberFormat. Allergen-etiketter -> t() (+5 nya nycklar).
  11 nya nycklar i alla 12 språk.
- i18n-vakt härdad: fångar nu hårdkodad svenska i prop={`...`}-mallliteraler
  (blind fläck förr). Verifierat att den fäller men inte ger falska positiv.
- whySv var redan lokaliserad (buildWhy med språktagg) - orörd.
- typecheck grönt (alla paket), vakt grön (603 nycklar).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-21 20:07:00 +00:00

139 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from "react";
import { Alert, View } from "react-native";
import { router } from "expo-router";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { api } from "@/lib/api";
import { t } from "@/lib/i18n";
import { Body, Button, Card, Heading, Input, Row, Screen, Small, Spacer } from "@/components/ui";
/**
* Måltidsloggning (spec §23): tidigare måltid, favoritrecept eller manuellt.
* Tallriksfoto loggas via Skanna-fliken. Näringsvärden hittas ALDRIG på:
* manuell loggning kräver egna värden eller känt recept/livsmedel.
*/
/** Etiketter ur i18n-katalogen (myday.mealType.<typ>) id:na är stabila API-värden. */
const MEAL_TYPES = ["breakfast", "lunch", "dinner", "snack"] as const;
interface RecentMeal {
id: string;
titleSv: string;
title?: string;
mealType: string;
nutrition: {
kcal: number;
proteinG: number;
carbsG: number;
fatG: number;
saturatedFatG: number;
fiberG: number;
sugarG: number;
saltG: number;
};
}
export default function LogMealScreen() {
const queryClient = useQueryClient();
const [mealType, setMealType] = useState<string>("lunch");
const [title, setTitle] = useState("");
const [kcal, setKcal] = useState("");
const [protein, setProtein] = useState("");
const recent = useQuery({
queryKey: ["recent-meals"],
queryFn: () => api<{ meals: RecentMeal[] }>("/v1/meals/recent"),
});
const log = useMutation({
mutationFn: (body: unknown) => api("/v1/meals", { method: "POST", body }),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["day"] });
router.back();
},
onError: (err) =>
Alert.alert(t("common.oops"), err instanceof Error ? err.message : t("common.error")),
});
const today = new Date().toISOString().slice(0, 10);
const logManual = () => {
if (!title.trim() || !kcal) {
Alert.alert(t("logmeal.validationTitle"), t("logmeal.validationBody"));
return;
}
log.mutate({
date: today,
mealType,
source: "manual",
titleSv: title.trim(),
nutritionOverride: {
kcal: Number(kcal),
proteinG: Number(protein) || 0,
carbsG: 0,
fatG: 0,
saturatedFatG: 0,
fiberG: 0,
sugarG: 0,
saltG: 0,
},
});
};
const logPrevious = (meal: RecentMeal) => {
log.mutate({
date: today,
mealType,
source: "previous_meal",
titleSv: meal.titleSv,
nutritionOverride: meal.nutrition,
});
};
return (
<Screen>
<Heading>{t("logmeal.mealTypeTitle")}</Heading>
<Row>
{MEAL_TYPES.map((id) => (
<Button
key={id}
label={t(`myday.mealType.${id}`)}
variant={mealType === id ? "secondary" : "ghost"}
onPress={() => setMealType(id)}
/>
))}
</Row>
<Spacer />
<Heading>{t("logmeal.quickTitle")}</Heading>
<Small>{t("logmeal.quickSubtitle")}</Small>
{(recent.data?.meals ?? []).slice(0, 5).map((meal) => (
<Card key={meal.id} onPress={() => logPrevious(meal)}>
<Row style={{ justifyContent: "space-between" }}>
<Body>{meal.title ?? meal.titleSv}</Body>
<Small>{Math.round(meal.nutrition.kcal)} kcal</Small>
</Row>
</Card>
))}
<Spacer />
<Heading>{t("logmeal.manualTitle")}</Heading>
<Input placeholder={t("logmeal.whatPlaceholder")} value={title} onChangeText={setTitle} />
<Row>
<View style={{ flex: 1 }}>
<Input placeholder="kcal" keyboardType="numeric" value={kcal} onChangeText={setKcal} />
</View>
<View style={{ flex: 1 }}>
<Input
placeholder={t("logmeal.proteinPlaceholder")}
keyboardType="numeric"
value={protein}
onChangeText={setProtein}
/>
</View>
</Row>
<Button label={t("myday.logMeal")} onPress={logManual} loading={log.isPending} />
<Small>{t("logmeal.photoTip")}</Small>
</Screen>
);
}