"use client"; import { useEffect, useState } from "react"; import Link from "next/link"; import { useTranslations } from "next-intl"; import { Card, CardContent } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { buttonVariants } from "@/components/ui/button"; import { cn } from "@/lib/utils"; type NutritionTotals = { calories: number; proteinG: number; carbsG: number; fatG: number; fiberG: number; sodiumMg: number; }; type NutritionGoals = { caloriesKcal: number | null; proteinG: number | null; carbsG: number | null; fatG: number | null; } | null; type NutritionCoverage = { calories: number; protein: number; carbs: number; fat: number; }; type DiaryEntry = { id: string; recipeId: string; title: string; servings: number; cookedAt: string; nutritionKnown: boolean; }; type DiaryResponse = { date: string; totals: NutritionTotals; goals: NutritionGoals; coverage: NutritionCoverage; entries: DiaryEntry[]; unknownCount: number; }; function todayIso(): string { return new Date().toISOString().slice(0, 10); } export function NutritionDiary() { const t = useTranslations("nutritionDiary"); const [date, setDate] = useState(todayIso()); // Tagged with the date it was fetched for, and set only from inside the // fetch's own then/catch — never synchronously in the effect body — so we // don't need a separate "loading" setState call at the top of the effect. const [result, setResult] = useState< { date: string; ok: true; data: DiaryResponse } | { date: string; ok: false } | null >(null); useEffect(() => { let cancelled = false; fetch(`/api/v1/users/me/nutrition-diary?date=${date}`) .then((res) => (res.ok ? res.json() : Promise.reject(new Error("request failed")))) .then((json: DiaryResponse) => { if (!cancelled) setResult({ date, ok: true, data: json }); }) .catch(() => { if (!cancelled) setResult({ date, ok: false }); }); return () => { cancelled = true; }; }, [date]); const loading = result === null || result.date !== date; const error = !loading && result !== null && !result.ok; const data = !loading && result !== null && result.ok ? result.data : null; const bars = data ? [ { label: t("calories"), value: data.totals.calories, goal: data.goals?.caloriesKcal ?? null, unit: "kcal", percentage: data.coverage.calories, colorClass: "bg-orange-500", }, { label: t("protein"), value: data.totals.proteinG, goal: data.goals?.proteinG ?? null, unit: "g", percentage: data.coverage.protein, colorClass: "bg-blue-500", }, { label: t("carbs"), value: data.totals.carbsG, goal: data.goals?.carbsG ?? null, unit: "g", percentage: data.coverage.carbs, colorClass: "bg-green-500", }, { label: t("fat"), value: data.totals.fatG, goal: data.goals?.fatG ?? null, unit: "g", percentage: data.coverage.fat, colorClass: "bg-yellow-500", }, ] : []; return (
setDate(e.target.value || todayIso())} className="w-auto" />
{date !== todayIso() && ( )}
{loading &&

{t("loading")}

} {error &&

{t("loadError")}

} {!loading && !error && data && ( <> {!data.goals && (

{t("goalNotSet")}

{t("setGoalsCta")}
)} {data.goals && ( {bars.map((bar) => bar.goal == null ? null : (
{bar.label} {bar.value} / {bar.goal} {bar.unit}
) )}
{t("fiber")} {data.totals.fiberG}g
{t("sodium")} {data.totals.sodiumMg}mg
)}

{t("entriesTitle")}

{data.entries.length === 0 ? (

{t("noEntries")}

) : (
    {data.entries.map((entry) => (
  • {entry.title} {t("servingsLabel", { count: entry.servings })} {!entry.nutritionKnown && ( {t("nutritionUnknownBadge")} )}
  • ))}
)} {data.unknownCount > 0 && (

{t("unknownNote", { count: data.unknownCount })}

)}
)}
); }