Conversation
WalkthroughAdds new nutrition dashboard feature: API routes for dashboard data and goals, React components for dashboard UI (summary, trends, recent meals, history, AI analysis), page update to render the dashboard, UI primitives (tabs, progress), and Supabase tables/triggers/migrations for daily summaries and user goals. Also adds Radix and date-fns dependencies. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant NextAPI as /api/nutrition/dashboard
participant Supabase
Client->>NextAPI: GET ?days=7 (cookies)
NextAPI->>Supabase: auth.getUser()
Supabase-->>NextAPI: user or null
alt unauthorized
NextAPI-->>Client: 401
else authorized
NextAPI->>Supabase: select daily_nutrition_summaries (date range)
NextAPI->>Supabase: select user_nutrition_goals (single)
NextAPI->>Supabase: select nutrition_logs (recent completed, limit 10)
Supabase-->>NextAPI: results
NextAPI-->>Client: 200 JSON {summaries, goals, recentMeals, analytics}
end
sequenceDiagram
participant Client
participant GoalsAPI as /api/nutrition/goals
participant Supabase
Client->>GoalsAPI: GET (cookies)
GoalsAPI->>Supabase: auth.getUser()
Supabase-->>GoalsAPI: user
GoalsAPI->>Supabase: select user_nutrition_goals (single)
Supabase-->>GoalsAPI: goals or none
GoalsAPI-->>Client: 200 {goals or defaults}
Client->>GoalsAPI: POST {goals payload}
GoalsAPI->>GoalsAPI: validate payload
GoalsAPI->>Supabase: upsert user_nutrition_goals
Supabase-->>GoalsAPI: upsert result
GoalsAPI-->>Client: 200 {success, goals}
sequenceDiagram
participant App
participant DB as Supabase DB
participant Trigger as update_daily_nutrition_summary()
App->>DB: INSERT/UPDATE/DELETE nutrition_logs
DB->>Trigger: AFTER row change
Trigger->>DB: Aggregate completed logs by (user_id, date)
Trigger->>DB: UPSERT daily_nutrition_summaries
DB-->>App: Row change committed
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 15
🧹 Nitpick comments (7)
src/components/calorie-tracker/FoodLogManager.tsx (1)
384-398: Constrain numeric inputs and guard parsingPrevent negatives and reduce NaN coercion with HTML attributes and safer parsing.
- <Input - type="number" - value={item.calories} - onChange={(e) => updateFoodItem(log.id, index, 'calories', parseInt(e.target.value) || 0)} - placeholder="Calories" - className="text-sm" - /> + <Input + type="number" + inputMode="numeric" + min={0} + value={item.calories} + onChange={(e) => { + const n = Number(e.target.value) + updateFoodItem(log.id, index, 'calories', Number.isFinite(n) ? n : 0) + }} + placeholder="Calories" + className="text-sm" + />Apply similar
min={0}and parsing to protein.src/app/calorie-tracker/page.tsx (2)
34-41: Consider extracting success alert styling to a variantThe success alert uses inline color classes. Consider adding a
successvariant to the Alert component for better reusability and consistency.You could extend the Alert component to support a success variant:
- <Alert className="mb-6 border-green-200 bg-green-50"> - <CheckCircle className="h-4 w-4 text-green-600" /> - <AlertDescription className="text-green-800"> + <Alert variant="success" className="mb-6"> + <CheckCircle className="h-4 w-4" /> + <AlertDescription>This would require updating the Alert component to handle the success variant with appropriate styling.
43-50: Consider extracting the loading spinner to a reusable componentThe inline spinner animation could be extracted to a reusable LoadingSpinner component for consistency across the app.
Consider creating a reusable loading component:
// components/ui/loading-spinner.tsx export function LoadingSpinner({ message }: { message?: string }) { return ( <div className="flex items-center justify-center py-12"> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500" /> {message && <span className="ml-2 text-gray-600">{message}</span>} </div> ); }Then use it here:
- <Suspense fallback={ - <div className="flex items-center justify-center py-12"> - <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div> - <span className="ml-2 text-gray-600">Loading your nutrition dashboard...</span> - </div> - }> + <Suspense fallback={<LoadingSpinner message="Loading your nutrition dashboard..." />}>src/components/calorie-tracker/WeeklyTrendsChart.tsx (1)
42-44: Move fetchWeeklyData inside useEffect or wrap with useCallbackThe
fetchWeeklyDatafunction is recreated on every render. While not causing issues due to the dependency array, it's better to follow React best practices.Move the function inside useEffect:
useEffect(() => { + const fetchWeeklyData = async () => { + try { + setLoading(true); + // ... rest of the function + } catch (err) { + // ... error handling + } + }; fetchWeeklyData(); }, [userId]); - - const fetchWeeklyData = async () => { - // ... function body - };src/components/calorie-tracker/AIAnalysisDisplay.tsx (2)
72-88: Extract magic numbers as constants and add score validationThe confidence thresholds are hardcoded and the score parameter lacks validation.
+const CONFIDENCE_THRESHOLDS = { + HIGH: 0.8, + MEDIUM: 0.6 +} as const; + const getConfidenceColor = (score: number) => { - if (score >= 0.8) return 'bg-green-500'; - if (score >= 0.6) return 'bg-yellow-500'; + const validScore = Math.max(0, Math.min(1, score)); // Clamp to [0, 1] + if (validScore >= CONFIDENCE_THRESHOLDS.HIGH) return 'bg-green-500'; + if (validScore >= CONFIDENCE_THRESHOLDS.MEDIUM) return 'bg-yellow-500'; return 'bg-red-500'; }; const getConfidenceText = (score: number) => { - if (score >= 0.8) return 'High Confidence'; - if (score >= 0.6) return 'Medium Confidence'; + const validScore = Math.max(0, Math.min(1, score)); + if (validScore >= CONFIDENCE_THRESHOLDS.HIGH) return 'High Confidence'; + if (validScore >= CONFIDENCE_THRESHOLDS.MEDIUM) return 'Medium Confidence'; return 'Low Confidence'; };
116-129: Align tip threshold with confidence thresholdsThe tips threshold (0.7) doesn't align with the confidence thresholds (0.6, 0.8), creating inconsistency.
const getTips = (score: number, hasError: boolean) => { if (hasError) return []; - if (score < 0.7) { + if (score < CONFIDENCE_THRESHOLDS.MEDIUM) { return [ 'Ensure good lighting when taking photos', 'Include the entire meal in the frame', 'Avoid heavily processed or mixed foods', 'Take photos from directly above the food', 'Remove packaging or plates that obscure the food' ]; } return []; };supabase/migrations/008_create_user_nutrition_goals.sql (1)
49-52: Consider using NOT EXISTS for better performanceThe current approach with NOT IN subquery might be less efficient than NOT EXISTS for large user bases.
-INSERT INTO public.user_nutrition_goals (user_id) -SELECT id FROM auth.users -WHERE id NOT IN (SELECT user_id FROM public.user_nutrition_goals) -ON CONFLICT (user_id) DO NOTHING; +INSERT INTO public.user_nutrition_goals (user_id) +SELECT u.id FROM auth.users u +WHERE NOT EXISTS ( + SELECT 1 FROM public.user_nutrition_goals g + WHERE g.user_id = u.id +) +ON CONFLICT (user_id) DO NOTHING;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (13)
package.json(1 hunks)src/app/api/nutrition/dashboard/route.ts(1 hunks)src/app/api/nutrition/goals/route.ts(1 hunks)src/app/calorie-tracker/page.tsx(3 hunks)src/components/calorie-tracker/AIAnalysisDisplay.tsx(1 hunks)src/components/calorie-tracker/CalorieTrackerDashboard.tsx(1 hunks)src/components/calorie-tracker/DailyCalorieSummary.tsx(1 hunks)src/components/calorie-tracker/FoodLogManager.tsx(1 hunks)src/components/calorie-tracker/WeeklyTrendsChart.tsx(1 hunks)src/components/ui/progress.tsx(1 hunks)src/components/ui/tabs.tsx(1 hunks)supabase/migrations/007_create_daily_nutrition_summaries.sql(1 hunks)supabase/migrations/008_create_user_nutrition_goals.sql(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (9)
src/app/api/nutrition/goals/route.ts (1)
src/app/api/nutrition/dashboard/route.ts (1)
GET(16-140)
src/components/calorie-tracker/AIAnalysisDisplay.tsx (6)
src/lib/nutrition-types.ts (1)
FoodItem(1-11)src/components/ui/badge.tsx (1)
Badge(46-46)src/components/ui/card.tsx (4)
Card(85-85)CardHeader(86-86)CardTitle(88-88)CardContent(91-91)src/components/ui/alert.tsx (2)
Alert(81-81)AlertDescription(81-81)src/components/ui/button.tsx (1)
Button(59-59)src/components/ui/progress.tsx (1)
Progress(28-28)
src/components/ui/tabs.tsx (1)
src/lib/utils.ts (1)
cn(4-6)
src/app/api/nutrition/dashboard/route.ts (1)
src/app/api/nutrition/goals/route.ts (1)
GET(14-59)
src/components/calorie-tracker/CalorieTrackerDashboard.tsx (5)
src/components/calorie-tracker/DailyCalorieSummary.tsx (1)
DailyCalorieSummary(58-245)src/components/calorie-tracker/WeeklyTrendsChart.tsx (1)
WeeklyTrendsChart(36-297)src/components/calorie-tracker/AIAnalysisDisplay.tsx (1)
AIAnalysisDisplay(63-363)src/components/calorie-tracker/PhotoUpload.tsx (1)
PhotoUpload(33-264)src/components/calorie-tracker/FoodLogManager.tsx (1)
FoodLogManager(67-484)
src/app/calorie-tracker/page.tsx (2)
src/components/ui/alert.tsx (2)
Alert(81-81)AlertDescription(81-81)src/components/calorie-tracker/CalorieTrackerDashboard.tsx (1)
CalorieTrackerDashboard(44-354)
src/components/ui/progress.tsx (1)
src/lib/utils.ts (1)
cn(4-6)
src/components/calorie-tracker/FoodLogManager.tsx (6)
src/lib/nutrition-types.ts (1)
FoodItem(1-11)src/components/ui/badge.tsx (1)
Badge(46-46)src/components/ui/card.tsx (4)
Card(85-85)CardHeader(86-86)CardTitle(88-88)CardContent(91-91)src/components/ui/button.tsx (1)
Button(59-59)src/components/ui/input.tsx (1)
Input(28-28)src/components/ui/textarea.tsx (1)
Textarea(24-24)
src/components/calorie-tracker/DailyCalorieSummary.tsx (4)
src/components/ui/card.tsx (4)
Card(85-85)CardHeader(86-86)CardTitle(88-88)CardContent(91-91)src/components/ui/button.tsx (1)
Button(59-59)src/components/ui/badge.tsx (1)
Badge(46-46)src/components/ui/progress.tsx (1)
Progress(28-28)
🔇 Additional comments (16)
package.json (1)
20-27: Verify peer-dependency and ESM compatibility for new UI/date librariesAdditions look consistent with the new components, but please verify:
- @radix-ui/react-tabs and @radix-ui/react-progress peer-deps align with React 19/Next 15.
- date-fns v4 is ESM-only; ensure Jest/ts-jest/transforms are configured if tests import it.
src/components/ui/tabs.tsx (1)
8-55: LGTM: Tabs wrappers are well-typed and consistent with shadcn patternsForwardRef typings, class composition, and exports look solid. No issues found.
src/components/calorie-tracker/FoodLogManager.tsx (1)
130-134: Timezone edge case in date filterFormatting
created_atwith local timezone can shift dates around midnight if the column is UTC. If users expect local dates, this is fine; otherwise, consider comparing against UTC or usingtoISOString().slice(0,10)consistently.supabase/migrations/007_create_daily_nutrition_summaries.sql (1)
15-27: Ensuregen_random_uuid()is available
gen_random_uuid()requires the pgcrypto extension. Some Supabase projects have it enabled by default, but if not, the migration will fail.Add before table creation if needed:
CREATE EXTENSION IF NOT EXISTS pgcrypto;src/app/calorie-tracker/page.tsx (1)
13-19: Documentation accurately reflects the new dashboard functionalityThe updated JSDoc properly describes the comprehensive dashboard features including daily summaries, trends, and meal logging.
src/app/api/nutrition/goals/route.ts (2)
14-59: GET handler implementation looks goodProper authentication, error handling, and fallback to default values when no goals exist. The PGRST116 code handling for missing data is correct.
82-102: Input validation is comprehensive and well-implementedGood validation for calorie goal range and enum values for activity level and weight goal with clear error messages.
src/components/calorie-tracker/WeeklyTrendsChart.tsx (2)
208-255: Chart implementation is well-structuredGood use of Recharts with responsive design, proper styling, and interactive elements. The tooltip configuration and line styling enhance user experience.
286-292: Good UX with the no-data stateHelpful messaging and clear call-to-action when no data is available for the selected metric.
src/components/calorie-tracker/DailyCalorieSummary.tsx (3)
66-84: Well-implemented loading skeletonGood use of skeleton UI with animated pulse effect that maintains layout consistency during loading.
91-124: Verify fiber progress calculation inconsistencyFiber progress is calculated inline (line 122) while other macros use
goalProgress. This suggests the API doesn't include fiber ingoalProgress.Consider either:
- Adding fiber to the API's
goalProgresscalculation for consistency- Document why fiber is handled differently
If this is intentional, consider adding a comment explaining the difference.
209-227: Clean and well-formatted macro displayGood formatting choices with
toFixed(1)for amounts andtoFixed(0)for percentages. The grid layout provides clear visual organization.src/app/api/nutrition/dashboard/route.ts (1)
81-102: Goal progress calculation is well-implementedGood null checking and fallback values. Note that fiber is intentionally excluded from goalProgress, which aligns with the frontend component's inline calculation for fiber.
src/components/calorie-tracker/CalorieTrackerDashboard.tsx (2)
57-66: Authentication handled server-side – no client auth header neededThe dashboard API route in
src/app/api/nutrition/dashboard/route.tscallssupabase.auth.getUser()and returns a 401 if the user isn’t signed in. Browserfetch('/api/nutrition/dashboard')defaults to"same-origin"credentials, so session cookies (and Supabase auth) are sent automatically. No changes are needed to the client‐side fetch call.
232-239: Fix off-by-one error in food items display logicThe condition
index < meal.food_items.length - 1 && index < 1has an off-by-one error. When showing 2 items, the comma won't appear between them.{meal.food_items.slice(0, 2).map((item: any, index: number) => ( <span key={index}> {item.name} - {index < meal.food_items.length - 1 && index < 1 ? ', ' : ''} + {index === 0 && meal.food_items.length > 1 ? ', ' : ''} </span> ))}Likely an incorrect or invalid review comment.
supabase/migrations/008_create_user_nutrition_goals.sql (1)
43-45: Trigger function verified
The trigger insupabase/migrations/008_create_user_nutrition_goals.sqlcorrectly referencespublic.update_updated_at_column(), which is defined in
supabase/migrations/007_create_daily_nutrition_summaries.sqlNo further changes required.
| // Fetch daily summaries | ||
| const { data: dailySummaries, error: summariesError } = await supabase | ||
| .from('daily_nutrition_summaries') | ||
| .select('*') | ||
| .eq('user_id', user.id) | ||
| .gte('date', format(startDate, 'yyyy-MM-dd')) | ||
| .lte('date', format(endDate, 'yyyy-MM-dd')) | ||
| .order('date', { ascending: true }); | ||
|
|
||
| if (summariesError) { | ||
| logger.error('Failed to fetch daily summaries', { error: summariesError, userId: user.id }); | ||
| throw summariesError; | ||
| } | ||
|
|
||
| // Fetch user goals | ||
| const { data: userGoals, error: goalsError } = await supabase | ||
| .from('user_nutrition_goals') | ||
| .select('*') | ||
| .eq('user_id', user.id) | ||
| .single(); | ||
|
|
||
| if (goalsError && goalsError.code !== 'PGRST116') { | ||
| logger.error('Failed to fetch user goals', { error: goalsError, userId: user.id }); | ||
| throw goalsError; | ||
| } | ||
|
|
||
| // Fetch recent meal logs | ||
| const { data: recentMeals, error: mealsError } = await supabase | ||
| .from('nutrition_logs') | ||
| .select('*') | ||
| .eq('user_id', user.id) | ||
| .eq('processing_status', 'completed') | ||
| .order('created_at', { ascending: false }) | ||
| .limit(10); | ||
|
|
||
| if (mealsError) { | ||
| logger.error('Failed to fetch recent meals', { error: mealsError, userId: user.id }); | ||
| throw mealsError; | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Parallelize independent database queries for better performance
The three database queries (dailySummaries, userGoals, recentMeals) are executed sequentially but are independent. Parallelize them for better performance.
Use Promise.all to run queries in parallel:
- // Fetch daily summaries
- const { data: dailySummaries, error: summariesError } = await supabase
- .from('daily_nutrition_summaries')
- .select('*')
- .eq('user_id', user.id)
- .gte('date', format(startDate, 'yyyy-MM-dd'))
- .lte('date', format(endDate, 'yyyy-MM-dd'))
- .order('date', { ascending: true });
-
- if (summariesError) {
- logger.error('Failed to fetch daily summaries', { error: summariesError, userId: user.id });
- throw summariesError;
- }
-
- // Fetch user goals
- const { data: userGoals, error: goalsError } = await supabase
- .from('user_nutrition_goals')
- .select('*')
- .eq('user_id', user.id)
- .single();
-
- if (goalsError && goalsError.code !== 'PGRST116') {
- logger.error('Failed to fetch user goals', { error: goalsError, userId: user.id });
- throw goalsError;
- }
-
- // Fetch recent meal logs
- const { data: recentMeals, error: mealsError } = await supabase
- .from('nutrition_logs')
- .select('*')
- .eq('user_id', user.id)
- .eq('processing_status', 'completed')
- .order('created_at', { ascending: false })
- .limit(10);
-
- if (mealsError) {
- logger.error('Failed to fetch recent meals', { error: mealsError, userId: user.id });
- throw mealsError;
- }
+ // Fetch all data in parallel
+ const [summariesResult, goalsResult, mealsResult] = await Promise.all([
+ supabase
+ .from('daily_nutrition_summaries')
+ .select('*')
+ .eq('user_id', user.id)
+ .gte('date', format(startDate, 'yyyy-MM-dd'))
+ .lte('date', format(endDate, 'yyyy-MM-dd'))
+ .order('date', { ascending: true }),
+
+ supabase
+ .from('user_nutrition_goals')
+ .select('*')
+ .eq('user_id', user.id)
+ .single(),
+
+ supabase
+ .from('nutrition_logs')
+ .select('*')
+ .eq('user_id', user.id)
+ .eq('processing_status', 'completed')
+ .order('created_at', { ascending: false })
+ .limit(10)
+ ]);
+
+ const { data: dailySummaries, error: summariesError } = summariesResult;
+ const { data: userGoals, error: goalsError } = goalsResult;
+ const { data: recentMeals, error: mealsError } = mealsResult;
+
+ if (summariesError) {
+ logger.error('Failed to fetch daily summaries', { error: summariesError, userId: user.id });
+ throw summariesError;
+ }
+
+ if (goalsError && goalsError.code !== 'PGRST116') {
+ logger.error('Failed to fetch user goals', { error: goalsError, userId: user.id });
+ throw goalsError;
+ }
+
+ if (mealsError) {
+ logger.error('Failed to fetch recent meals', { error: mealsError, userId: user.id });
+ throw mealsError;
+ }This reduces the total query time from sum(t1 + t2 + t3) to max(t1, t2, t3).
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Fetch daily summaries | |
| const { data: dailySummaries, error: summariesError } = await supabase | |
| .from('daily_nutrition_summaries') | |
| .select('*') | |
| .eq('user_id', user.id) | |
| .gte('date', format(startDate, 'yyyy-MM-dd')) | |
| .lte('date', format(endDate, 'yyyy-MM-dd')) | |
| .order('date', { ascending: true }); | |
| if (summariesError) { | |
| logger.error('Failed to fetch daily summaries', { error: summariesError, userId: user.id }); | |
| throw summariesError; | |
| } | |
| // Fetch user goals | |
| const { data: userGoals, error: goalsError } = await supabase | |
| .from('user_nutrition_goals') | |
| .select('*') | |
| .eq('user_id', user.id) | |
| .single(); | |
| if (goalsError && goalsError.code !== 'PGRST116') { | |
| logger.error('Failed to fetch user goals', { error: goalsError, userId: user.id }); | |
| throw goalsError; | |
| } | |
| // Fetch recent meal logs | |
| const { data: recentMeals, error: mealsError } = await supabase | |
| .from('nutrition_logs') | |
| .select('*') | |
| .eq('user_id', user.id) | |
| .eq('processing_status', 'completed') | |
| .order('created_at', { ascending: false }) | |
| .limit(10); | |
| if (mealsError) { | |
| logger.error('Failed to fetch recent meals', { error: mealsError, userId: user.id }); | |
| throw mealsError; | |
| } | |
| // Fetch all data in parallel | |
| const [summariesResult, goalsResult, mealsResult] = await Promise.all([ | |
| supabase | |
| .from('daily_nutrition_summaries') | |
| .select('*') | |
| .eq('user_id', user.id) | |
| .gte('date', format(startDate, 'yyyy-MM-dd')) | |
| .lte('date', format(endDate, 'yyyy-MM-dd')) | |
| .order('date', { ascending: true }), | |
| supabase | |
| .from('user_nutrition_goals') | |
| .select('*') | |
| .eq('user_id', user.id) | |
| .single(), | |
| supabase | |
| .from('nutrition_logs') | |
| .select('*') | |
| .eq('user_id', user.id) | |
| .eq('processing_status', 'completed') | |
| .order('created_at', { ascending: false }) | |
| .limit(10) | |
| ]); | |
| const { data: dailySummaries, error: summariesError } = summariesResult; | |
| const { data: userGoals, error: goalsError } = goalsResult; | |
| const { data: recentMeals, error: mealsError } = mealsResult; | |
| if (summariesError) { | |
| logger.error('Failed to fetch daily summaries', { error: summariesError, userId: user.id }); | |
| throw summariesError; | |
| } | |
| if (goalsError && goalsError.code !== 'PGRST116') { | |
| logger.error('Failed to fetch user goals', { error: goalsError, userId: user.id }); | |
| throw goalsError; | |
| } | |
| if (mealsError) { | |
| logger.error('Failed to fetch recent meals', { error: mealsError, userId: user.id }); | |
| throw mealsError; | |
| } |
🤖 Prompt for AI Agents
In src/app/api/nutrition/dashboard/route.ts around lines 31 to 69, the three
independent Supabase queries (dailySummaries, userGoals, recentMeals) are
executed sequentially; change them to run in parallel by constructing the three
query promises first (call
.from(...).select(...).eq(...).gte(...).lte(...).order... etc. but do not await
them immediately), then await Promise.all on the three promises and destructure
their results into the existing variables; after awaiting, keep the existing
per-query error handling logic (log and throw summariesError and mealsError as
before, and for userGoals keep the special-case check for PGRST116 before
logging/throwing). Ensure date formatting is done before creating the promises
so .gte/.lte receive strings, and preserve ordering/limit clauses in each query.
| const { data: goals, error } = await supabase | ||
| .from('user_nutrition_goals') | ||
| .upsert({ | ||
| user_id: user.id, | ||
| daily_calorie_goal: parseInt(daily_calorie_goal), | ||
| daily_protein_goal_g: parseFloat(daily_protein_goal_g) || 150, | ||
| daily_carbs_goal_g: parseFloat(daily_carbs_goal_g) || 200, | ||
| daily_fat_goal_g: parseFloat(daily_fat_goal_g) || 70, | ||
| daily_fiber_goal_g: parseFloat(daily_fiber_goal_g) || 25, | ||
| activity_level: activity_level || 'moderate', | ||
| weight_goal: weight_goal || 'maintain', | ||
| updated_at: new Date().toISOString(), | ||
| }) | ||
| .select() | ||
| .single(); | ||
|
|
There was a problem hiding this comment.
Fix potential issue with zero values and improve number parsing
The current implementation has issues with handling zero values and number parsing:
- The
||operator will incorrectly replace legitimate0values with defaults parseIntshould specify radix for clarity- Consider using nullish coalescing (
??) instead
Apply this fix:
.upsert({
user_id: user.id,
- daily_calorie_goal: parseInt(daily_calorie_goal),
- daily_protein_goal_g: parseFloat(daily_protein_goal_g) || 150,
- daily_carbs_goal_g: parseFloat(daily_carbs_goal_g) || 200,
- daily_fat_goal_g: parseFloat(daily_fat_goal_g) || 70,
- daily_fiber_goal_g: parseFloat(daily_fiber_goal_g) || 25,
+ daily_calorie_goal: parseInt(daily_calorie_goal, 10),
+ daily_protein_goal_g: daily_protein_goal_g !== undefined ? parseFloat(daily_protein_goal_g) : 150,
+ daily_carbs_goal_g: daily_carbs_goal_g !== undefined ? parseFloat(daily_carbs_goal_g) : 200,
+ daily_fat_goal_g: daily_fat_goal_g !== undefined ? parseFloat(daily_fat_goal_g) : 70,
+ daily_fiber_goal_g: daily_fiber_goal_g !== undefined ? parseFloat(daily_fiber_goal_g) : 25,
activity_level: activity_level || 'moderate',
weight_goal: weight_goal || 'maintain',
updated_at: new Date().toISOString(),
})Also, since the migration includes an update trigger for updated_at, you might not need to set it manually on line 115.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const { data: goals, error } = await supabase | |
| .from('user_nutrition_goals') | |
| .upsert({ | |
| user_id: user.id, | |
| daily_calorie_goal: parseInt(daily_calorie_goal), | |
| daily_protein_goal_g: parseFloat(daily_protein_goal_g) || 150, | |
| daily_carbs_goal_g: parseFloat(daily_carbs_goal_g) || 200, | |
| daily_fat_goal_g: parseFloat(daily_fat_goal_g) || 70, | |
| daily_fiber_goal_g: parseFloat(daily_fiber_goal_g) || 25, | |
| activity_level: activity_level || 'moderate', | |
| weight_goal: weight_goal || 'maintain', | |
| updated_at: new Date().toISOString(), | |
| }) | |
| .select() | |
| .single(); | |
| const { data: goals, error } = await supabase | |
| .from('user_nutrition_goals') | |
| .upsert({ | |
| user_id: user.id, | |
| daily_calorie_goal: parseInt(daily_calorie_goal, 10), | |
| daily_protein_goal_g: daily_protein_goal_g !== undefined | |
| ? parseFloat(daily_protein_goal_g) | |
| : 150, | |
| daily_carbs_goal_g: daily_carbs_goal_g !== undefined | |
| ? parseFloat(daily_carbs_goal_g) | |
| : 200, | |
| daily_fat_goal_g: daily_fat_goal_g !== undefined | |
| ? parseFloat(daily_fat_goal_g) | |
| : 70, | |
| daily_fiber_goal_g: daily_fiber_goal_g !== undefined | |
| ? parseFloat(daily_fiber_goal_g) | |
| : 25, | |
| activity_level: activity_level || 'moderate', | |
| weight_goal: weight_goal || 'maintain', | |
| updated_at: new Date().toISOString(), | |
| }) | |
| .select() | |
| .single(); |
| interface FoodItem { | ||
| name: string; | ||
| quantity: string; | ||
| calories: number; | ||
| protein_g: number; | ||
| carbs_g: number; | ||
| fat_g: number; | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Consider using the existing FoodItem interface from nutrition-types
The local FoodItem interface duplicates a subset of fields from the existing FoodItem interface in src/lib/nutrition-types.ts. This could lead to type inconsistency and maintenance issues.
Replace the local interface with an import:
+import { FoodItem } from '@/lib/nutrition-types';
+
-interface FoodItem {
- name: string;
- quantity: string;
- calories: number;
- protein_g: number;
- carbs_g: number;
- fat_g: number;
-}If quantity is needed but not in the global type, consider extending it:
interface AIFoodItem extends FoodItem {
quantity: string;
}🤖 Prompt for AI Agents
In src/components/calorie-tracker/AIAnalysisDisplay.tsx around lines 21 to 28
the file declares a local FoodItem interface that duplicates fields from the
global FoodItem in src/lib/nutrition-types.ts; remove the local declaration and
import the shared FoodItem from src/lib/nutrition-types.ts instead, and if the
component needs the quantity field that isn’t present on the shared type create
an extended interface (e.g., AIFoodItem extends FoodItem { quantity: string })
and use that extended type throughout the file to replace the local duplicate to
keep types consistent.
| const handleReprocess = async () => { | ||
| if (!onReprocess) return; | ||
|
|
||
| setIsReprocessing(true); | ||
| try { | ||
| await onReprocess(log.id); | ||
| } catch (error) { | ||
| console.error('Reprocess failed:', error); | ||
| } finally { | ||
| setIsReprocessing(false); | ||
| } | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Surface reprocessing errors to the user
Errors are only logged to console without user feedback, leading to a poor UX when reprocessing fails.
Add error state and display:
const [isReprocessing, setIsReprocessing] = useState(false);
+const [reprocessError, setReprocessError] = useState<string | null>(null);
const handleReprocess = async () => {
if (!onReprocess) return;
setIsReprocessing(true);
+ setReprocessError(null);
try {
await onReprocess(log.id);
} catch (error) {
console.error('Reprocess failed:', error);
+ setReprocessError('Failed to reprocess. Please try again.');
} finally {
setIsReprocessing(false);
}
};Then display the error in the UI where the reprocess button is shown.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const handleReprocess = async () => { | |
| if (!onReprocess) return; | |
| setIsReprocessing(true); | |
| try { | |
| await onReprocess(log.id); | |
| } catch (error) { | |
| console.error('Reprocess failed:', error); | |
| } finally { | |
| setIsReprocessing(false); | |
| } | |
| }; | |
| // Add this alongside your other state hooks | |
| const [isReprocessing, setIsReprocessing] = useState(false); | |
| const [reprocessError, setReprocessError] = useState<string | null>(null); | |
| const handleReprocess = async () => { | |
| if (!onReprocess) return; | |
| setIsReprocessing(true); | |
| setReprocessError(null); | |
| try { | |
| await onReprocess(log.id); | |
| } catch (error) { | |
| console.error('Reprocess failed:', error); | |
| setReprocessError('Failed to reprocess. Please try again.'); | |
| } finally { | |
| setIsReprocessing(false); | |
| } | |
| }; |
🤖 Prompt for AI Agents
In src/components/calorie-tracker/AIAnalysisDisplay.tsx around lines 103 to 114,
the reprocess handler only logs errors to the console so failures aren’t visible
to users; add a state variable (e.g., reprocessError: string | null) and set it
to null at the start of handleReprocess, set it to a user-friendly message
derived from the caught error in the catch block (convert error to string
safely), and keep setIsReprocessing behavior in finally; then render that error
message in the component UI adjacent to the reprocess button (with minimal
styling/role for accessibility) so users see the failure and can retry.
| {log.food_items.map((item, index) => ( | ||
| <div key={index} className="p-3 bg-gray-50 rounded-lg"> | ||
| <div className="flex justify-between items-start"> | ||
| <div> | ||
| <span className="font-medium text-gray-900">{item.name}</span> | ||
| <span className="text-gray-500 ml-2">({item.quantity})</span> | ||
| </div> | ||
| <div className="text-lg font-bold text-orange-600"> | ||
| {item.calories} cal | ||
| </div> | ||
| </div> | ||
| <div className="mt-2 text-sm text-gray-600 grid grid-cols-3 gap-4"> | ||
| <div>Protein: {item.protein_g}g</div> | ||
| <div>Carbs: {item.carbs_g}g</div> | ||
| <div>Fat: {item.fat_g}g</div> | ||
| </div> | ||
| </div> | ||
| ))} |
There was a problem hiding this comment.
Add null check for item.quantity display
The quantity field is displayed without checking if it exists, potentially showing "(undefined)".
<div>
<span className="font-medium text-gray-900">{item.name}</span>
- <span className="text-gray-500 ml-2">({item.quantity})</span>
+ {item.quantity && (
+ <span className="text-gray-500 ml-2">({item.quantity})</span>
+ )}
</div>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| {log.food_items.map((item, index) => ( | |
| <div key={index} className="p-3 bg-gray-50 rounded-lg"> | |
| <div className="flex justify-between items-start"> | |
| <div> | |
| <span className="font-medium text-gray-900">{item.name}</span> | |
| <span className="text-gray-500 ml-2">({item.quantity})</span> | |
| </div> | |
| <div className="text-lg font-bold text-orange-600"> | |
| {item.calories} cal | |
| </div> | |
| </div> | |
| <div className="mt-2 text-sm text-gray-600 grid grid-cols-3 gap-4"> | |
| <div>Protein: {item.protein_g}g</div> | |
| <div>Carbs: {item.carbs_g}g</div> | |
| <div>Fat: {item.fat_g}g</div> | |
| </div> | |
| </div> | |
| ))} | |
| {log.food_items.map((item, index) => ( | |
| <div key={index} className="p-3 bg-gray-50 rounded-lg"> | |
| <div className="flex justify-between items-start"> | |
| <div> | |
| <span className="font-medium text-gray-900">{item.name}</span> | |
| - <span className="text-gray-500 ml-2">({item.quantity})</span> | |
| + {item.quantity && ( | |
| + <span className="text-gray-500 ml-2">({item.quantity})</span> | |
| + )} | |
| </div> | |
| <div className="text-lg font-bold text-orange-600"> | |
| {item.calories} cal | |
| </div> | |
| </div> | |
| <div className="mt-2 text-sm text-gray-600 grid grid-cols-3 gap-4"> | |
| <div>Protein: {item.protein_g}g</div> | |
| <div>Carbs: {item.carbs_g}g</div> | |
| <div>Fat: {item.fat_g}g</div> | |
| </div> | |
| </div> | |
| ))} |
🤖 Prompt for AI Agents
In src/components/calorie-tracker/AIAnalysisDisplay.tsx around lines 254 to 271,
the JSX renders item.quantity directly which can produce "(undefined)"; update
the rendering to check for null/undefined and only render the quantity segment
when it exists (or render a safe fallback like "1" or omit the parentheses
entirely). Ensure the conditional is applied inline so the span showing quantity
is only output when item.quantity !== null && item.quantity !== undefined (or
replaced with the chosen fallback).
| <div className="mt-3 flex items-center gap-2"> | ||
| <ImageIcon className="h-4 w-4 text-gray-500" /> | ||
| <Button | ||
| variant="ghost" | ||
| size="sm" | ||
| onClick={() => window.open(log.image_url, '_blank')} | ||
| className="text-blue-600 hover:text-blue-800" | ||
| > | ||
| View Image |
There was a problem hiding this comment.
Prevent reverse tabnabbing when opening images
Use noopener/noreferrer when opening a new tab.
- onClick={() => window.open(log.image_url, '_blank')}
+ onClick={() => window.open(log.image_url, '_blank', 'noopener,noreferrer')}Alternatively, render an anchor:
<a href={log.image_url} target="_blank" rel="noopener noreferrer">
<Button variant="ghost" size="sm" className="text-blue-600 hover:text-blue-800">
View Image
</Button>
</a>🤖 Prompt for AI Agents
In src/components/calorie-tracker/FoodLogManager.tsx around lines 463 to 471,
the "View Image" button opens a new tab using window.open without
noopener/noreferrer which allows reverse tabnabbing; fix by preventing opener
access: either render the Button inside an anchor <a href={log.image_url}
target="_blank" rel="noopener noreferrer">...</a>, or change the onClick to open
the window and immediately null out the opener (const w =
window.open(log.image_url, '_blank'); if (w) w.opener = null;). Ensure
rel="noopener noreferrer" is present if using an anchor, or set newWindow.opener
= null when using window.open.
| {Object.entries(metricConfig).map(([key, config]) => ( | ||
| <Button | ||
| key={key} | ||
| onClick={() => setActiveMetric(key as any)} | ||
| size="sm" | ||
| variant={activeMetric === key ? "default" : "outline"} | ||
| className="text-xs" | ||
| > | ||
| {config.label} | ||
| </Button> | ||
| ))} |
There was a problem hiding this comment.
Avoid using 'any' type - use proper type casting
The any type in the onClick handler defeats TypeScript's type safety.
Replace with proper type casting:
- onClick={() => setActiveMetric(key as any)}
+ onClick={() => setActiveMetric(key as 'calories' | 'protein' | 'carbs' | 'fat' | 'fiber')}Or better yet, extract the type:
type MetricType = 'calories' | 'protein' | 'carbs' | 'fat' | 'fiber';
// Then use: key as MetricType🤖 Prompt for AI Agents
In src/components/calorie-tracker/WeeklyTrendsChart.tsx around lines 194 to 204,
the onClick uses key as any which removes TypeScript safety; introduce a
concrete MetricType union (e.g. 'calories' | 'protein' | 'carbs' | 'fat' |
'fiber'), type metricConfig as Record<MetricType, ...> (or narrow Object.entries
with typed keys), and replace key as any with key as MetricType (or have the map
strongly-typed) so the handler and activeMetric use the proper MetricType
instead of any.
| >(({ className, value, ...props }, ref) => ( | ||
| <ProgressPrimitive.Root | ||
| ref={ref} | ||
| className={cn( | ||
| "relative h-4 w-full overflow-hidden rounded-full bg-secondary", | ||
| className | ||
| )} | ||
| {...props} | ||
| > | ||
| <ProgressPrimitive.Indicator | ||
| className="h-full w-full flex-1 bg-primary transition-all" | ||
| style={{ transform: `translateX(-${100 - (value || 0)}%)` }} | ||
| /> |
There was a problem hiding this comment.
Pass value to Root and clamp to [0,100] to avoid invalid CSS and improve a11y
- The destructured
valueis not forwarded toProgressPrimitive.Root. That removes ARIA semantics. translateX(-${100 - (value || 0)}%)can produce--20%when value > 100, which is invalid CSS.- Guard against NaN and clamp to [0, 100].
Apply this diff:
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
className
)}
- {...props}
+ value={typeof value === "number" ? value : 0}
+ {...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
- style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
+ style={{
+ transform: `translateX(-${
+ 100 - Math.min(100, Math.max(0, Number.isFinite(value as number) ? (value as number) : 0))
+ }%)`,
+ }}
/>
</ProgressPrimitive.Root>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| >(({ className, value, ...props }, ref) => ( | |
| <ProgressPrimitive.Root | |
| ref={ref} | |
| className={cn( | |
| "relative h-4 w-full overflow-hidden rounded-full bg-secondary", | |
| className | |
| )} | |
| {...props} | |
| > | |
| <ProgressPrimitive.Indicator | |
| className="h-full w-full flex-1 bg-primary transition-all" | |
| style={{ transform: `translateX(-${100 - (value || 0)}%)` }} | |
| /> | |
| >(({ className, value, ...props }, ref) => ( | |
| <ProgressPrimitive.Root | |
| ref={ref} | |
| className={cn( | |
| "relative h-4 w-full overflow-hidden rounded-full bg-secondary", | |
| className | |
| )} | |
| value={typeof value === "number" ? value : 0} | |
| {...props} | |
| > | |
| <ProgressPrimitive.Indicator | |
| className="h-full w-full flex-1 bg-primary transition-all" | |
| style={{ | |
| transform: `translateX(-${ | |
| 100 - | |
| Math.min( | |
| 100, | |
| Math.max(0, Number.isFinite(value as number) ? (value as number) : 0) | |
| ) | |
| }%)`, | |
| }} | |
| /> |
🤖 Prompt for AI Agents
In src/components/ui/progress.tsx around lines 11 to 23, the destructured value
is not forwarded to ProgressPrimitive.Root and the inline transform can generate
invalid CSS for values outside [0,100]; convert value to a number, guard against
NaN, clamp it to the range 0–100 (e.g. const safeValue = Math.min(100,
Math.max(0, Number(value) || 0))), pass that safeValue into
ProgressPrimitive.Root as the value prop to preserve ARIA semantics, and use
safeValue when computing the translateX style (translateX(-${100 -
safeValue}%)).
| -- Function to update daily summaries | ||
| CREATE OR REPLACE FUNCTION public.update_daily_nutrition_summary() | ||
| RETURNS TRIGGER AS $$ | ||
| BEGIN | ||
| -- Update or insert daily summary | ||
| INSERT INTO public.daily_nutrition_summaries ( | ||
| user_id, | ||
| date, | ||
| total_calories, | ||
| total_protein_g, | ||
| total_carbs_g, | ||
| total_fat_g, | ||
| total_fiber_g, | ||
| meal_count | ||
| ) | ||
| SELECT | ||
| user_id, | ||
| DATE(created_at) as date, | ||
| COALESCE(SUM(total_calories), 0) as total_calories, | ||
| COALESCE(SUM(total_protein_g), 0) as total_protein_g, | ||
| COALESCE(SUM(total_carbs_g), 0) as total_carbs_g, | ||
| COALESCE(SUM(total_fat_g), 0) as total_fat_g, | ||
| COALESCE(SUM(total_fiber_g), 0) as total_fiber_g, | ||
| COUNT(*) as meal_count | ||
| FROM public.nutrition_logs | ||
| WHERE user_id = COALESCE(NEW.user_id, OLD.user_id) | ||
| AND DATE(created_at) = DATE(COALESCE(NEW.created_at, OLD.created_at)) | ||
| AND processing_status = 'completed' | ||
| GROUP BY user_id, DATE(created_at) | ||
| ON CONFLICT (user_id, date) | ||
| DO UPDATE SET | ||
| total_calories = EXCLUDED.total_calories, | ||
| total_protein_g = EXCLUDED.total_protein_g, | ||
| total_carbs_g = EXCLUDED.total_carbs_g, | ||
| total_fat_g = EXCLUDED.total_fat_g, | ||
| total_fiber_g = EXCLUDED.total_fiber_g, | ||
| meal_count = EXCLUDED.meal_count, | ||
| updated_at = NOW(); | ||
|
|
||
| RETURN COALESCE(NEW, OLD); | ||
| END; | ||
| $$ LANGUAGE plpgsql; |
There was a problem hiding this comment.
Trigger function leaves stale rows on last-delete and misses old/new day recounts on UPDATE
Current logic only upserts when the SELECT returns rows. If the last completed log for a day is deleted or status changes away from 'completed', the SELECT yields 0 rows and the existing summary remains stale. Also, when an UPDATE changes user_id or created_at date, only the NEW pair is recalculated, leaving the OLD day/user incorrect.
Patch to:
- Recompute for both OLD and NEW (when applicable).
- Upsert when there are rows; otherwise delete the summary for that (user_id, date).
CREATE OR REPLACE FUNCTION public.update_daily_nutrition_summary()
RETURNS TRIGGER AS $$
-BEGIN
- -- Update or insert daily summary
- INSERT INTO public.daily_nutrition_summaries (
- user_id,
- date,
- total_calories,
- total_protein_g,
- total_carbs_g,
- total_fat_g,
- total_fiber_g,
- meal_count
- )
- SELECT
- user_id,
- DATE(created_at) as date,
- COALESCE(SUM(total_calories), 0) as total_calories,
- COALESCE(SUM(total_protein_g), 0) as total_protein_g,
- COALESCE(SUM(total_carbs_g), 0) as total_carbs_g,
- COALESCE(SUM(total_fat_g), 0) as total_fat_g,
- COALESCE(SUM(total_fiber_g), 0) as total_fiber_g,
- COUNT(*) as meal_count
- FROM public.nutrition_logs
- WHERE user_id = COALESCE(NEW.user_id, OLD.user_id)
- AND DATE(created_at) = DATE(COALESCE(NEW.created_at, OLD.created_at))
- AND processing_status = 'completed'
- GROUP BY user_id, DATE(created_at)
- ON CONFLICT (user_id, date)
- DO UPDATE SET
- total_calories = EXCLUDED.total_calories,
- total_protein_g = EXCLUDED.total_protein_g,
- total_carbs_g = EXCLUDED.total_carbs_g,
- total_fat_g = EXCLUDED.total_fat_g,
- total_fiber_g = EXCLUDED.total_fiber_g,
- meal_count = EXCLUDED.meal_count,
- updated_at = NOW();
-
- RETURN COALESCE(NEW, OLD);
-END;
+DECLARE
+ v_pairs TABLE (user_id uuid, day date);
+BEGIN
+ -- Collect affected (user, day) pairs for this mutation
+ IF TG_OP = 'UPDATE' THEN
+ INSERT INTO v_pairs VALUES (OLD.user_id, DATE(OLD.created_at));
+ IF DATE(NEW.created_at) <> DATE(OLD.created_at) OR NEW.user_id <> OLD.user_id THEN
+ INSERT INTO v_pairs VALUES (NEW.user_id, DATE(NEW.created_at));
+ END IF;
+ ELSE
+ INSERT INTO v_pairs VALUES (COALESCE(NEW.user_id, OLD.user_id), DATE(COALESCE(NEW.created_at, OLD.created_at)));
+ END IF;
+
+ -- Recompute each affected pair
+ FOR SELECT DISTINCT user_id, day FROM v_pairs LOOP
+ WITH agg AS (
+ SELECT
+ COALESCE(SUM(total_calories), 0) AS total_calories,
+ COALESCE(SUM(total_protein_g), 0) AS total_protein_g,
+ COALESCE(SUM(total_carbs_g), 0) AS total_carbs_g,
+ COALESCE(SUM(total_fat_g), 0) AS total_fat_g,
+ COALESCE(SUM(total_fiber_g), 0) AS total_fiber_g,
+ COUNT(*) AS meal_count
+ FROM public.nutrition_logs
+ WHERE user_id = v_pairs.user_id
+ AND DATE(created_at) = v_pairs.day
+ AND processing_status = 'completed'
+ )
+ INSERT INTO public.daily_nutrition_summaries (
+ user_id, date, total_calories, total_protein_g, total_carbs_g, total_fat_g, total_fiber_g, meal_count
+ )
+ SELECT v_pairs.user_id, v_pairs.day, a.total_calories, a.total_protein_g, a.total_carbs_g, a.total_fat_g, a.total_fiber_g, a.meal_count
+ FROM agg a
+ WHERE a.meal_count > 0
+ ON CONFLICT (user_id, date)
+ DO UPDATE SET
+ total_calories = EXCLUDED.total_calories,
+ total_protein_g = EXCLUDED.total_protein_g,
+ total_carbs_g = EXCLUDED.total_carbs_g,
+ total_fat_g = EXCLUDED.total_fat_g,
+ total_fiber_g = EXCLUDED.total_fiber_g,
+ meal_count = EXCLUDED.meal_count,
+ updated_at = NOW();
+
+ -- If no completed meals remain, remove the summary
+ IF NOT EXISTS (
+ SELECT 1 FROM public.nutrition_logs
+ WHERE user_id = v_pairs.user_id
+ AND DATE(created_at) = v_pairs.day
+ AND processing_status = 'completed'
+ ) THEN
+ DELETE FROM public.daily_nutrition_summaries
+ WHERE user_id = v_pairs.user_id AND date = v_pairs.day;
+ END IF;
+ END LOOP;
+
+ RETURN COALESCE(NEW, OLD);
+END;
$$ LANGUAGE plpgsql;Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In supabase/migrations/007_create_daily_nutrition_summaries.sql around lines 49
to 90, update the trigger function so it recomputes summaries for both the OLD
and NEW (when present and possibly different) (user_id, date) pairs and deletes
the summary row when no completed logs remain. Implement by building a small
loop over the distinct pairs (COALESCE(NEW.user_id, OLD.user_id) /
COALESCE(NEW.created_at, OLD.created_at) and, when NEW and OLD differ, also the
OLD pair), for each pair run an aggregate query against public.nutrition_logs
filtering processing_status='completed' and DATE(created_at)=that date; if the
aggregate returns count>0 then INSERT ... ON CONFLICT ... DO UPDATE with the
computed totals and updated_at=NOW(), otherwise DELETE FROM
public.daily_nutrition_summaries WHERE user_id=... AND date=.... Ensure
duplicate pairs are deduplicated so you don’t run twice for the same
(user_id,date).
| daily_calorie_goal INTEGER DEFAULT 2000, | ||
| daily_protein_goal_g DECIMAL(6,2) DEFAULT 150, | ||
| daily_carbs_goal_g DECIMAL(6,2) DEFAULT 200, | ||
| daily_fat_goal_g DECIMAL(6,2) DEFAULT 70, | ||
| daily_fiber_goal_g DECIMAL(6,2) DEFAULT 25, |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Consider adding CHECK constraints for positive values
The nutrition goal columns should validate that values are non-negative to prevent invalid data.
daily_calorie_goal INTEGER DEFAULT 2000,
-daily_protein_goal_g DECIMAL(6,2) DEFAULT 150,
-daily_carbs_goal_g DECIMAL(6,2) DEFAULT 200,
-daily_fat_goal_g DECIMAL(6,2) DEFAULT 70,
-daily_fiber_goal_g DECIMAL(6,2) DEFAULT 25,
+daily_protein_goal_g DECIMAL(6,2) DEFAULT 150 CHECK (daily_protein_goal_g >= 0),
+daily_carbs_goal_g DECIMAL(6,2) DEFAULT 200 CHECK (daily_carbs_goal_g >= 0),
+daily_fat_goal_g DECIMAL(6,2) DEFAULT 70 CHECK (daily_fat_goal_g >= 0),
+daily_fiber_goal_g DECIMAL(6,2) DEFAULT 25 CHECK (daily_fiber_goal_g >= 0),Also add for calories:
-daily_calorie_goal INTEGER DEFAULT 2000,
+daily_calorie_goal INTEGER DEFAULT 2000 CHECK (daily_calorie_goal > 0),📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| daily_calorie_goal INTEGER DEFAULT 2000, | |
| daily_protein_goal_g DECIMAL(6,2) DEFAULT 150, | |
| daily_carbs_goal_g DECIMAL(6,2) DEFAULT 200, | |
| daily_fat_goal_g DECIMAL(6,2) DEFAULT 70, | |
| daily_fiber_goal_g DECIMAL(6,2) DEFAULT 25, | |
| daily_calorie_goal INTEGER DEFAULT 2000 CHECK (daily_calorie_goal > 0), | |
| daily_protein_goal_g DECIMAL(6,2) DEFAULT 150 CHECK (daily_protein_goal_g >= 0), | |
| daily_carbs_goal_g DECIMAL(6,2) DEFAULT 200 CHECK (daily_carbs_goal_g >= 0), | |
| daily_fat_goal_g DECIMAL(6,2) DEFAULT 70 CHECK (daily_fat_goal_g >= 0), | |
| daily_fiber_goal_g DECIMAL(6,2) DEFAULT 25 CHECK (daily_fiber_goal_g >= 0), |
🤖 Prompt for AI Agents
In supabase/migrations/008_create_user_nutrition_goals.sql around lines 7 to 11,
the numeric nutrition goal columns lack validation; add CHECK constraints so
daily_calorie_goal must be > 0 and the other goals (daily_protein_goal_g,
daily_carbs_goal_g, daily_fat_goal_g, daily_fiber_goal_g) must be >= 0 to
prevent negative values. Update the CREATE TABLE column definitions (or add
separate ALTER TABLE ... ADD CONSTRAINT statements) to include these checks and
ensure DECIMAL columns compare to 0.0 where applicable; keep default values
intact.
Summary by CodeRabbit