ReportRaahat CI
Deploy from GitHub: 7c7b0b67e1dae2679d91ef948e246730d7d10fbf
ee7023b
"use client";
import { useEffect, useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import {
RadarChart,
PolarGrid,
PolarAngleAxis,
Radar,
ResponsiveContainer,
Tooltip,
} from "recharts";
import { useGUCStore } from "@/lib/store";
import { PageShell } from "@/components/ui";
interface FoodItem {
name_english: string;
name_hindi: string;
nutrient_highlights: Record<string, number>;
serving_suggestion: string;
food_group: string;
}
interface NutritionResponse {
recommended_foods: FoodItem[];
daily_targets: Record<string, number>;
deficiency_summary: string;
}
const NUTRIENT_LABELS: Record<string, { label: string; unit: string; icon: string }> = {
protein_g: { label: "Protein", unit: "g", icon: "💪" },
iron_mg: { label: "Iron", unit: "mg", icon: "🩸" },
calcium_mg: { label: "Calcium", unit: "mg", icon: "🦴" },
vitaminD_iu: { label: "Vit D", unit: "IU", icon: "☀️" },
fiber_g: { label: "Fiber", unit: "g", icon: "🌾" },
calories_kcal: { label: "Calories", unit: "kcal",icon: "⚡" },
};
const FOOD_GROUP_COLORS: Record<string, string> = {
"Green Leafy Vegetables": "#22C55E",
"Cereals & Millets": "#F59E0B",
"Grain Legumes": "#F97316",
"Fruits": "#EC4899",
"Nuts & Oil Seeds": "#8B5CF6",
"Milk & Products": "#06B6D4",
"Eggs": "#EAB308",
"Condiments & Spices": "#EF4444",
};
const FOOD_ICONS: Record<string, string> = {
"Green Leafy Vegetables": "🥬",
"Cereals & Millets": "🌾",
"Grain Legumes": "🫘",
"Fruits": "🍎",
"Nuts & Oil Seeds": "🥜",
"Milk & Products": "🥛",
"Eggs": "🥚",
"Condiments & Spices": "🌿",
};
function buildRadarData(loggedCount: number) {
return [
{ nutrient: "Protein", target: 100, current: Math.min(100, loggedCount * 15 + 30) },
{ nutrient: "Iron", target: 100, current: Math.min(100, loggedCount * 12 + 20) },
{ nutrient: "Calcium", target: 100, current: Math.min(100, loggedCount * 10 + 25) },
{ nutrient: "Vit D", target: 100, current: Math.min(100, loggedCount * 8 + 15) },
{ nutrient: "Fiber", target: 100, current: Math.min(100, loggedCount * 14 + 35) },
];
}
export default function NutritionPage() {
const nutritionProfile = useGUCStore((s) => s.nutritionProfile);
const latestReport = useGUCStore((s) => s.latestReport);
const profile = useGUCStore((s) => s.profile);
const logFood = useGUCStore((s) => s.logFood);
const addXP = useGUCStore((s) => s.addXP);
const setAvatarState = useGUCStore((s) => s.setAvatarState);
const [data, setData] = useState<NutritionResponse | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const [loggedToday, setLoggedToday] = useState<string[]>([]);
const [activeCard, setActiveCard] = useState<string | null>(null);
const flags = nutritionProfile.deficiencies.join(",") || "INCREASE_IRON";
useEffect(() => {
setLoggedToday(nutritionProfile.loggedToday);
}, [nutritionProfile.loggedToday]);
useEffect(() => {
const fetchNutrition = async () => {
try {
setLoading(true);
setError(false);
// Call Next.js API route which proxies to backend
const res = await fetch(`/api/nutrition`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
dietary_flags: nutritionProfile.deficiencies.length > 0
? nutritionProfile.deficiencies
: ["INCREASE_IRON"],
allergy_flags: [],
vegetarian: true,
}),
});
if (!res.ok) throw new Error("API error");
const json = await res.json();
// Transform backend FoodItem → frontend FoodItem shape
const transformed: NutritionResponse = {
recommended_foods: (json.recommended_foods ?? []).map((f: Record<string, unknown>) => ({
name_english: f.food_name ?? "",
name_hindi: f.food_name_hindi ?? "",
food_group: f.food_group ?? "",
serving_suggestion: f.serving_suggestion ?? "",
nutrient_highlights: {
protein_g: (f.protein_g as number) ?? 0,
iron_mg: (f.iron_mg as number) ?? 0,
calcium_mg: (f.calcium_mg as number) ?? 0,
vitaminD_iu: (f.vitamin_d_mcg as number) ?? 0,
fiber_g: (f.fibre_g as number) ?? 0,
calories_kcal: (f.energy_kcal as number) ?? 0,
},
})),
daily_targets: json.daily_targets ?? {},
deficiency_summary: (json.deficiencies ?? []).join(" · "),
};
setData(transformed);
} catch {
try {
const res = await fetch(`/api/nutrition`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ dietary_flags: ["INCREASE_IRON"], vegetarian: true }),
});
if (!res.ok) throw new Error();
const json = await res.json();
const transformed: NutritionResponse = {
recommended_foods: (json.recommended_foods ?? []).map((f: Record<string, unknown>) => ({
name_english: f.food_name ?? "",
name_hindi: f.food_name_hindi ?? "",
food_group: f.food_group ?? "",
serving_suggestion: f.serving_suggestion ?? "",
nutrient_highlights: {
protein_g: (f.protein_g as number) ?? 0,
iron_mg: (f.iron_mg as number) ?? 0,
calcium_mg: (f.calcium_mg as number) ?? 0,
vitaminD_iu: (f.vitamin_d_mcg as number) ?? 0,
fiber_g: (f.fibre_g as number) ?? 0,
calories_kcal: (f.energy_kcal as number) ?? 0,
},
})),
daily_targets: json.daily_targets ?? {},
deficiency_summary: (json.deficiencies ?? []).join(" · "),
};
setData(transformed);
} catch {
setError(true);
}
} finally {
setLoading(false);
}
};
fetchNutrition();
}, [flags, profile.language, nutritionProfile.deficiencies]);
const handleAddToToday = (food: FoodItem) => {
logFood(food.name_english);
setLoggedToday((prev) => [...prev, food.name_english]);
addXP(15);
setAvatarState("HAPPY");
};
const radarData = buildRadarData(loggedToday.length);
if (loading) {
return (
<PageShell>
<div className="space-y-4">
<div className="h-8 w-48 bg-white/5 rounded-xl animate-pulse" />
<div className="h-64 bg-white/5 rounded-2xl animate-pulse" />
<div className="grid grid-cols-2 gap-3">
{[...Array(6)].map((_, i) => (
<div key={i} className="h-32 bg-white/5 rounded-2xl animate-pulse" />
))}
</div>
</div>
</PageShell>
);
}
return (
<PageShell>
{/* Header */}
<motion.div initial={{ opacity: 0, y: -12 }} animate={{ opacity: 1, y: 0 }} className="mb-6">
<div className="flex items-center gap-3 mb-1">
<span className="text-2xl">🥗</span>
<h1 className="text-xl font-bold tracking-tight">Nutrition Profile</h1>
</div>
<p className="text-white/40 text-sm">Based on your report · IFCT 2017 Indian Food Data</p>
</motion.div>
{/* Deficiency banner */}
{data?.deficiency_summary && (
<motion.div
initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.1 }}
className="mb-5 p-3.5 rounded-xl bg-[#FF9933]/10 border border-[#FF9933]/20"
>
<p className="text-[#FF9933] text-xs leading-relaxed">{data.deficiency_summary}</p>
</motion.div>
)}
{/* Daily targets grid */}
<motion.div
initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.15 }}
className="mb-5"
>
<p className="text-white/40 text-xs font-medium uppercase tracking-widest mb-3">Daily Targets</p>
<div className="grid grid-cols-3 gap-2">
{Object.entries(NUTRIENT_LABELS).map(([key, meta]) => {
const val = data?.daily_targets[key]
?? nutritionProfile.dailyTargets[key as keyof typeof nutritionProfile.dailyTargets]
?? 0;
return (
<div key={key} className="bg-white/[0.04] border border-white/[0.07] rounded-xl p-3">
<div className="text-lg mb-1">{meta.icon}</div>
<div className="text-white font-semibold text-sm">
{typeof val === "number" ? val.toLocaleString() : val}
<span className="text-white/30 text-[10px] ml-0.5">{meta.unit}</span>
</div>
<div className="text-white/35 text-[10px]">{meta.label}</div>
</div>
);
})}
</div>
</motion.div>
{/* Radar chart */}
<motion.div
initial={{ opacity: 0, scale: 0.97 }} animate={{ opacity: 1, scale: 1 }} transition={{ delay: 0.2 }}
className="mb-5 bg-white/[0.03] border border-white/[0.07] rounded-2xl p-4"
>
<div className="flex justify-between items-center mb-3">
<p className="text-white/60 text-xs font-medium uppercase tracking-widest">Coverage Today</p>
<div className="flex gap-3 text-[10px] text-white/40">
<span className="flex items-center gap-1">
<span className="w-2 h-2 rounded-full inline-block" style={{ background: "#FF9933" }} />Current
</span>
<span className="flex items-center gap-1">
<span className="w-2 h-2 rounded-full inline-block bg-white/20" />Target
</span>
</div>
</div>
<ResponsiveContainer width="100%" height={220}>
<RadarChart data={radarData} margin={{ top: 8, right: 16, bottom: 8, left: 16 }}>
<PolarGrid stroke="rgba(255,255,255,0.06)" />
<PolarAngleAxis dataKey="nutrient" tick={{ fill: "rgba(255,255,255,0.4)", fontSize: 11 }} />
<Radar name="Target" dataKey="target" stroke="rgba(255,255,255,0.12)" fill="rgba(255,255,255,0.04)" strokeWidth={1} />
<Radar name="Current" dataKey="current" stroke="#FF9933" fill="#FF9933" fillOpacity={0.18} strokeWidth={2} />
<Tooltip
contentStyle={{ background: "#1a1a2e", border: "1px solid rgba(255,255,255,0.1)", borderRadius: "8px", fontSize: "11px" }}
/>
</RadarChart>
</ResponsiveContainer>
{loggedToday.length > 0 && (
<p className="text-white/25 text-[10px] text-center mt-1">
{loggedToday.length} food{loggedToday.length !== 1 ? "s" : ""} logged today
</p>
)}
</motion.div>
{/* Food cards */}
<div className="mb-3">
<p className="text-white/40 text-xs font-medium uppercase tracking-widest mb-3">
Recommended for You · Top Indian Foods
</p>
{error && (
<div className="text-white/40 text-sm text-center py-8">
Unable to load food data. Make sure the backend is running.
</div>
)}
<div className="space-y-2.5">
<AnimatePresence>
{(data?.recommended_foods ?? []).map((food, i) => {
const groupColor = FOOD_GROUP_COLORS[food.food_group] ?? "#FF9933";
const isLogged = loggedToday.includes(food.name_english);
const isExpanded = activeCard === food.name_english;
const icon = FOOD_ICONS[food.food_group] ?? "🌿";
return (
<motion.div
key={food.name_english}
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: i * 0.06, duration: 0.3 }}
className="rounded-xl border overflow-hidden"
style={{
background: isExpanded ? "rgba(255,255,255,0.05)" : "rgba(255,255,255,0.025)",
borderColor: isExpanded ? `${groupColor}40` : "rgba(255,255,255,0.07)",
}}
>
{/* Card header */}
<button
className="w-full flex items-center gap-3 p-3.5 text-left"
onClick={() => setActiveCard(isExpanded ? null : food.name_english)}
>
<div
className="w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 text-sm"
style={{ background: `${groupColor}20`, color: groupColor }}
>
{icon}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-white text-sm font-medium">{food.name_english}</span>
<span className="text-white/40 text-xs">{food.name_hindi}</span>
</div>
<span
className="text-[10px] px-1.5 py-0.5 rounded-full mt-0.5 inline-block"
style={{ background: `${groupColor}15`, color: groupColor }}
>
{food.food_group}
</span>
</div>
<div className="flex-shrink-0 flex items-center gap-2">
{isLogged && <span className="text-green-400 text-xs">✓ added</span>}
<span className="text-white/20 text-xs">{isExpanded ? "▲" : "▼"}</span>
</div>
</button>
{/* Expanded details */}
<AnimatePresence>
{isExpanded && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2 }}
className="overflow-hidden"
>
<div className="px-3.5 pb-3.5 space-y-3">
<div className="flex flex-wrap gap-2">
{Object.entries(food.nutrient_highlights)
.filter(([, v]) => v > 0)
.slice(0, 5)
.map(([key, value]) => {
const meta = NUTRIENT_LABELS[key];
if (!meta) return null;
return (
<div key={key} className="flex items-center gap-1.5 bg-white/5 rounded-lg px-2 py-1">
<span className="text-xs">{meta.icon}</span>
<span className="text-white/70 text-[11px]">
{meta.label}: <strong className="text-white">{value}</strong>
<span className="text-white/30">{meta.unit}</span>
</span>
</div>
);
})}
</div>
<p className="text-white/40 text-xs">📏 {food.serving_suggestion}</p>
<motion.button
whileTap={{ scale: 0.96 }}
onClick={() => handleAddToToday(food)}
disabled={isLogged}
className="w-full py-2 rounded-lg text-xs font-medium transition-all"
style={
isLogged
? { background: "rgba(34,197,94,0.1)", color: "#22C55E" }
: { background: groupColor, color: "#0d0d1a" }
}
>
{isLogged ? "✓ Added to Today" : "Add to Today · +15 XP"}
</motion.button>
</div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
);
})}
</AnimatePresence>
</div>
</div>
<p className="text-white/15 text-[10px] text-center mt-6">
Nutritional data: IFCT 2017 · National Institute of Nutrition, ICMR
</p>
</PageShell>
);
}