Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
9f5582a
feat(frontend): ✨ added header + nutrition breakdown column on nutrit…
samikabhatkar-web Feb 23, 2026
98bf0d8
feat(frontend): ✨ added userGoal table and macro counting component o…
samikabhatkar-web Feb 23, 2026
c166e2e
feat(frontend): ✨ remove datastring from props
samikabhatkar-web Feb 23, 2026
6a4663d
feat(frontend): ✨ edited macro breakdown + added history button
samikabhatkar-web Mar 1, 2026
5b61ef2
feat: ✨ tracked-meal-card displays food card details, has working del…
Mar 2, 2026
4e62ab2
fix: 🐛 corrected tracked-meal-card to reflect food-card's implemeneta…
Mar 2, 2026
c930fe8
feat: ✨ added edit servings/bowls functionality
Mar 2, 2026
5ad6ef7
feat: ✨ added checking for unavailable dishes
Mar 2, 2026
102f4c1
fix: 🐛 tracker heading displays date
Mar 2, 2026
ce5553d
feat: ✨ if diet plan tagged but unavailable, move to uncounted foods
Mar 2, 2026
a978a80
fix: 🐛 corrected nutrition breakdown overflow
Mar 2, 2026
1edba0f
Merge branch 'dev' into 643/new-nutrition-page
Mar 3, 2026
2fafa40
feat(frontend): ✨ fixed mobile view of tracker page
samikabhatkar-web Mar 3, 2026
3fcac82
feat(frontend): ✨ fixed edit box bug
samikabhatkar-web Mar 3, 2026
8084181
feat(fix): ✨ 🐛 changed protein default to 100 g
samikabhatkar-web Mar 4, 2026
7874fc5
feat(fix): ✨ 🐛 clicking outside edit goals component works
samikabhatkar-web Mar 4, 2026
6229260
feat(fix): ✨ 🐛 removed duplicate logged meals procedure
samikabhatkar-web Mar 4, 2026
85a136e
feat(frontend): ✨ added prev meals/history logic, need to fix some bugs
samikabhatkar-web Mar 5, 2026
70f6b43
feat(fix): ✨ 🐛 cleaned up dialog click issues
samikabhatkar-web Mar 5, 2026
a346584
feat(fix): ✨ 🐛 removed diet plan logic
samikabhatkar-web Mar 5, 2026
9106722
feat(style): ✨ 🎨 added drawer for meal history tracker
samikabhatkar-web Mar 5, 2026
ab75af8
feat(build): ✨ 📦️ added usergoalsbyday table + trpc procedures
samikabhatkar-web Mar 5, 2026
07be1f1
feat(style): ✨ 🎨 fixed counted food card to be wider on mobile
samikabhatkar-web Mar 5, 2026
66917e2
feat(fix): ✨ 🐛 removed goalsbyday import in index.ts
samikabhatkar-web Mar 5, 2026
97e492e
feat(fix): ✨ 🐛 had to export userGoalsByDay in index.ts (not sure why…
samikabhatkar-web Mar 5, 2026
71ca033
Merge branch 'dev' of github.com:icssc/PeterPlate into 643/new-nutrit…
samikabhatkar-web Mar 31, 2026
88586f8
feat(frontend): ✨ added suggested foods section
samikabhatkar-web Mar 31, 2026
c4c1419
feat(frontend): ✨ added search component to scroll for foods
samikabhatkar-web Mar 31, 2026
3634897
feat(chore): ✨ 🔧 used mui disable date prop on tracker history
samikabhatkar-web Apr 6, 2026
019d178
feat(frontend): ✨ updated onchange event + added onblur event to fix …
samikabhatkar-web Apr 6, 2026
103b11d
feat(frontend): ✨ changed suggested meal logic to 1 meal instead of 5
samikabhatkar-web Apr 6, 2026
ee28c33
fix(chore): 🐛 🔧 corrected nutrition breakdown overflow
Apr 7, 2026
3f42754
fix: 🐛 fixed suggested food cards spacing to match counted food cards
Apr 14, 2026
d648dc0
fix(chore): 🐛 🔧 changed nutrition goals to update after user clicks o…
Apr 14, 2026
629a7c1
fix: 🐛 unavailable food cards greyed out & automatically pushed to su…
Apr 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
395 changes: 365 additions & 30 deletions apps/next/src/app/nutrition/page.tsx

Large diffs are not rendered by default.

32 changes: 24 additions & 8 deletions apps/next/src/components/progress-donut.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,41 +18,57 @@ interface Props {
* The unit of measurement to display within the progress donut. Ex: 'g' for grams
*/
display_unit: string;

/**
* ring colors, passing props to add custom colors to certain goals
*/
trackColor?: string;
progressColor?: string;
}

export function ProgressDonut({
progress_value,
max_value,
display_unit,
trackColor = "#ffffff",
progressColor = "#0084D1",
}: Props) {
const value = Math.max(0, Math.min(progress_value, max_value));
const percent = value / max_value;
const strokeDashoffset = CIRCLE_CIRCUMFERENCE * (1 - percent);

return (
<div className="flex flex-col items-center justify-center p-4 pt-0">
<div className="relative w-40 h-40">
<div className="flex flex-col justify-center -ml-4 translate-y-2">
<div className="relative w-36 h-36">
<svg viewBox="0 0 100 100" className="w-full h-full">
<title>Progress Donut</title>
{/* outer translucent white circle */}
<circle cx="50" cy="50" r="30" fill="white" fillOpacity="0.5" />
{/* inner white circle */}
<circle cx="50" cy="50" r="25" fill="white" fillOpacity="0.9" />
{/* background arc track - semicircle */}
<circle
cx="50"
cy="50"
r={CIRCLE_RADIUS}
stroke="#e5e7eb"
stroke={trackColor}
strokeWidth="10"
strokeLinecap="round"
fill="none"
strokeDasharray={`${CIRCLE_CIRCUMFERENCE * 0.75} ${CIRCLE_CIRCUMFERENCE * 0.25}`}
transform="rotate(135 50 50)"
/>
{/* progress arc */}
<circle
cx="50"
cy="50"
r={CIRCLE_RADIUS}
stroke="#3b82f6"
stroke={progressColor}
strokeWidth="10"
fill="none"
strokeDasharray={CIRCLE_CIRCUMFERENCE}
strokeDashoffset={strokeDashoffset}
strokeDasharray={`${CIRCLE_CIRCUMFERENCE * 0.75 * percent} ${CIRCLE_CIRCUMFERENCE}`}
strokeLinecap="round"
transform="rotate(-90 50 50)"
transform="rotate(135 50 50)"
style={{ transition: "stroke-dashoffset 0.4s ease" }}
/>
</svg>
Expand All @@ -61,7 +77,7 @@ export function ProgressDonut({
{progress_value}
{display_unit}
</span>
<span className="text-sm text-muted-foreground">/ {max_value}</span>
<span className="text-sm font-bold text-gray-400">/ {max_value}</span>
</div>
</div>
</div>
Expand Down
191 changes: 191 additions & 0 deletions apps/next/src/components/ui/card/search-meal-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
"use client";

import {
Add,
ArrowDropDown,
ArrowDropUp,
Restaurant,
} from "@mui/icons-material";
import { Card, CardContent } from "@mui/material";
import type { SelectLoggedMeal } from "@peterplate/db";
import Image from "next/image";
import React from "react";
import { getFoodIcon } from "@/utils/funcs";
import { trpc } from "@/utils/trpc";
import { cn } from "@/utils/tw";

type LoggedMealJoinedWithNutrition = SelectLoggedMeal & {
calories: number;
protein: number;
carbs: number;
fat: number;
};

interface Props {
meal: LoggedMealJoinedWithNutrition;
isUnavailable?: boolean;
/** Called when the user clicks "+". Receives the meal*/
onAdd?: (meal: LoggedMealJoinedWithNutrition, servings: number) => void;
}

export default function SearchMealCard({ meal, isUnavailable, onAdd }: Props) {
const [servingsDraft, setServingsDraft] = React.useState(meal.servings ?? 1);
const [imageError, setImageError] = React.useState(false);

// fetch dish detail for image URL + icon
const { data } = trpc.peterplate.useQuery(
{ date: new Date(meal.eatenAt) },
{ enabled: !!meal.dishId },
);

const dish = React.useMemo(() => {
const halls = [data?.anteatery, data?.brandywine].filter(Boolean);
for (const hall of halls) {
for (const menu of hall?.menus ?? []) {
for (const station of menu.stations ?? []) {
const found = station.dishes?.find((d) => d.id === meal.dishId);
if (found) return found;
}
}
}
return undefined;
}, [data, meal.dishId]);

const imageUrl = dish?.image_url;
const dishNameForIcon = dish?.name ?? meal.dishName;
const showImage =
typeof imageUrl === "string" && imageUrl.trim() !== "" && !imageError;
const IconComponent = getFoodIcon(dishNameForIcon ?? "") ?? Restaurant;

return (
<div className={cn("w-full md:w-72")}>
<Card
className={cn(
"cursor-pointer transition w-full border",
isUnavailable ? "bg-zinc-200/90" : "bg-white hover:shadow-lg",
)}
sx={{ borderRadius: "12px" }}
>
<CardContent sx={{ padding: "0 !important" }}>
<div className="h-auto p-3 md:h-40 md:p-4 flex justify-between gap-3 text-left">
<div className="min-w-0 flex flex-col justify-between gap-3 flex-1">
<div className="flex items-center gap-3 min-w-0">
{showImage && imageUrl ? (
<Image
src={imageUrl}
alt=""
width={40}
height={40}
className="w-10 h-10 object-cover rounded flex-shrink-0"
onError={() => setImageError(true)}
/>
) : (
<IconComponent className="w-12 h-12 text-slate-700 flex-shrink-0" />
)}

<div className="min-w-0">
<h3 className="text-sky-700 font-semibold text-lg truncate">
{meal.dishName}
</h3>
<div className="flex items-center gap-2 text-xs text-zinc-500">
<div
className={cn(
"inline-flex items-stretch rounded-md ring-1",
isUnavailable
? "bg-sky-200/70 ring-sky-300/70"
: "bg-sky-100 ring-sky-200",
)}
>
<div className="w-8 px-2 py-1 text-slate-900 tabular-nums leading-none flex items-center justify-center">
{servingsDraft}
</div>

<div className="flex flex-col border-l border-sky-200 w-6 min-w-6 shrink-0">
<button
type="button"
className={cn(
"h-4 w-6 flex items-center justify-center transition p-0",
isUnavailable
? "hover:bg-sky-300/60"
: "hover:bg-sky-200/60",
)}
onClick={(e) => {
e.stopPropagation();
setServingsDraft(
Math.round((servingsDraft + 0.5) * 2) / 2,
);
}}
aria-label="Increase servings"
>
<ArrowDropUp
sx={{ fontSize: 18 }}
className="text-sky-700"
/>
</button>

<button
type="button"
className={cn(
"h-4 w-6 flex items-center justify-center transition p-0",
isUnavailable
? "hover:bg-sky-300/60"
: "hover:bg-sky-200/60",
)}
onClick={(e) => {
e.stopPropagation();
setServingsDraft(
Math.max(
0.5,
Math.round((servingsDraft - 0.5) * 2) / 2,
),
);
}}
aria-label="Decrease servings"
>
<ArrowDropDown
sx={{ fontSize: 18 }}
className="text-sky-700"
/>
</button>
</div>
</div>

<span className="whitespace-nowrap">
serving{servingsDraft !== 1 ? "s" : ""}/bowl
{servingsDraft !== 1 ? "s" : ""}
</span>
</div>
</div>
</div>

{/* Nutrition content */}
<div className="flex gap-4 text-sm text-zinc-600">
<span>{Math.round(meal.calories * servingsDraft)} cal</span>
<span>{Math.round(meal.protein * servingsDraft)}g protein</span>
<span>{Math.round(meal.carbs * servingsDraft)}g carbs</span>
<span>{Math.round(meal.fat * servingsDraft)}g fat</span>
</div>
</div>

{/* "+"" button instead of trash one */}
<div className="flex flex-col justify-between items-end">
<button
type="button"
onClick={(e) => {
e.stopPropagation();
if (!isUnavailable) {
onAdd?.(meal, servingsDraft);
}
}}
className="shrink-0 p-2 text-zinc-500 hover:text-sky-600 transition"
aria-label={`Add ${meal.dishName} to tracker`}
>
<Add fontSize="small" />
</button>
</div>
</div>
</CardContent>
</Card>
</div>
);
}
Loading
Loading