Skip to content

Commit be9007f

Browse files
committed
Food search display fix for serving unit and serving size
1 parent f16c5ca commit be9007f

4 files changed

Lines changed: 87 additions & 59 deletions

File tree

SparkyFitnessServer/index.js renamed to SparkyFitnessServer/SparkyFitnessServer.js

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ const FATSECRET_API_BASE_URL = "https://platform.fatsecret.com/rest";
2525
let fatSecretAccessToken = null;
2626
let tokenExpiryTime = 0;
2727

28+
// In-memory cache for FatSecret food nutrient data
29+
const foodNutrientCache = new Map();
30+
const CACHE_DURATION_MS = 5 * 60 * 1000; // 5 minutes
31+
2832
// Function to get FatSecret OAuth 2.0 Access Token
2933
async function getFatSecretAccessToken(clientId, clientSecret) {
3034
if (fatSecretAccessToken && Date.now() < tokenExpiryTime) {
@@ -160,6 +164,13 @@ app.get('/api/fatsecret/nutrients', async (req, res) => {
160164
return res.status(400).json({ error: "Missing foodId" });
161165
}
162166

167+
// Check cache first
168+
const cachedData = foodNutrientCache.get(foodId);
169+
if (cachedData && Date.now() < cachedData.expiry) {
170+
console.log(`Returning cached data for foodId: ${foodId}`);
171+
return res.json(cachedData.data);
172+
}
173+
163174
try {
164175
const accessToken = await getFatSecretAccessToken(clientId, clientSecret);
165176
const nutrientsUrl = `${FATSECRET_API_BASE_URL}?${new URLSearchParams({
@@ -175,25 +186,29 @@ app.get('/api/fatsecret/nutrients', async (req, res) => {
175186
method: "GET",
176187
headers: {
177188
Authorization: `Bearer ${accessToken}`,
178-
"Content-Type": "application/json", // Keep this for now, as it was in their example
179-
"Accept": "application/json", // Add Accept header
189+
"Content-Type": "application/json",
190+
"Accept": "application/json",
180191
}
181192
}
182193
);
183194

184195
if (!response.ok) {
185-
const errorText = await response.text(); // Get raw response text
196+
const errorText = await response.text();
186197
console.error("FatSecret Food Get API error:", errorText);
187198
try {
188199
const errorData = JSON.parse(errorText);
189200
return res.status(response.status).json({ error: errorData.message || response.statusText });
190201
} catch (jsonError) {
191-
// If it's not JSON, return the raw text as an error
192202
return res.status(response.status).json({ error: `FatSecret API returned non-JSON error: ${errorText}` });
193203
}
194204
}
195205

196206
const data = await response.json();
207+
// Store in cache
208+
foodNutrientCache.set(foodId, {
209+
data: data,
210+
expiry: Date.now() + CACHE_DURATION_MS
211+
});
197212
res.json(data);
198213
} catch (error) {
199214
console.error("Error in FatSecret nutrient proxy:", error);

SparkyFitnessServer/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "sparkyfitnessserver",
33
"version": "1.0.0",
4-
"main": "index.js",
4+
"main": "SparkyFitnessServer.js",
55
"scripts": {
66
"test": "echo \"Error: no test specified\" && exit 1"
77
},

src/components/EnhancedFoodSearch.tsx

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import EnhancedCustomFoodForm from './EnhancedCustomFoodForm';
1212
import BarcodeScanner from './BarcodeScanner';
1313
import { usePreferences } from "@/contexts/PreferencesContext";
1414
import { searchNutritionixFoods, getNutritionixNutrients, getNutritionixBrandedNutrients } from "@/services/NutritionixService";
15-
import { searchFatSecretFoods, getFatSecretNutrients } from "@/services/FatSecretService";
15+
import { searchFatSecretFoods, getFatSecretNutrients, FatSecretFoodItem } from "@/services/FatSecretService";
1616
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
1717
import { useAuth } from "@/hooks/useAuth";
1818
import { TablesInsert } from "@/integrations/supabase/types";
@@ -73,7 +73,7 @@ const EnhancedFoodSearch = ({ onFoodSelect }: EnhancedFoodSearchProps) => {
7373
const [foods, setFoods] = useState<Food[]>([]);
7474
const [openFoodFactsResults, setOpenFoodFactsResults] = useState<OpenFoodFactsProduct[]>([]);
7575
const [nutritionixResults, setNutritionixResults] = useState<any[]>([]); // To store Nutritionix search results
76-
const [fatSecretResults, setFatSecretResults] = useState<any[]>([]); // To store FatSecret search results
76+
const [fatSecretResults, setFatSecretResults] = useState<FatSecretFoodItem[]>([]); // To store FatSecret search results
7777
const [loading, setLoading] = useState(false);
7878
const [activeTab, setActiveTab] = useState<'database' | 'online' | 'barcode'>('database');
7979
const [showEditDialog, setShowEditDialog] = useState(false);
@@ -338,11 +338,11 @@ const EnhancedFoodSearch = ({ onFoodSelect }: EnhancedFoodSearchProps) => {
338338
}
339339
};
340340

341-
const convertFatSecretToFood = (item: any, nutrientData: any): Food => {
341+
const convertFatSecretToFood = (item: FatSecretFoodItem, nutrientData: any): Food => {
342342
return {
343343
id: undefined, // Let Supabase generate UUID
344344
name: nutrientData.name,
345-
brand: nutrientData.brand,
345+
brand: nutrientData.brand || item.brand_name || null, // Use detailed brand if available, else from search
346346
calories: nutrientData.calories,
347347
protein: nutrientData.protein,
348348
carbs: nutrientData.carbohydrates,
@@ -352,7 +352,7 @@ const EnhancedFoodSearch = ({ onFoodSelect }: EnhancedFoodSearchProps) => {
352352
serving_size: nutrientData.serving_qty,
353353
serving_unit: nutrientData.serving_unit,
354354
is_custom: false,
355-
provider_external_id: item.id,
355+
provider_external_id: item.food_id, // Use food_id from FatSecretFoodItem
356356
provider_type: 'fatsecret',
357357
polyunsaturated_fat: nutrientData.polyunsaturated_fat,
358358
monounsaturated_fat: nutrientData.monounsaturated_fat,
@@ -368,9 +368,10 @@ const EnhancedFoodSearch = ({ onFoodSelect }: EnhancedFoodSearchProps) => {
368368
};
369369
};
370370

371-
const handleFatSecretEdit = async (item: any) => {
371+
const handleFatSecretEdit = async (item: FatSecretFoodItem) => {
372372
setLoading(true);
373-
const nutrientData = await getFatSecretNutrients(item.id, selectedFoodDataProvider);
373+
// Only fetch detailed nutrients when "Edit & Add" is clicked
374+
const nutrientData = await getFatSecretNutrients(item.food_id, selectedFoodDataProvider);
374375
setLoading(false);
375376

376377
if (nutrientData) {
@@ -563,21 +564,21 @@ const EnhancedFoodSearch = ({ onFoodSelect }: EnhancedFoodSearchProps) => {
563564
))}
564565

565566
{activeTab === 'online' && fatSecretResults.length > 0 && fatSecretResults.map((item) => (
566-
<Card key={item.id} className="hover:bg-gray-50">
567+
<Card key={item.food_id} className="hover:bg-gray-50">
567568
<CardContent className="p-4">
568569
<div className="flex justify-between items-start">
569570
<div className="flex-1">
570571
<div className="flex items-center space-x-2 mb-2">
571-
<h3 className="font-medium">{item.name}</h3>
572-
{item.brand && <Badge variant="secondary" className="text-xs">{item.brand}</Badge>}
572+
<h3 className="font-medium">{item.food_name}</h3>
573+
{item.brand_name && <Badge variant="secondary" className="text-xs">{item.brand_name}</Badge>}
573574
<Badge variant="outline" className="text-xs">FatSecret</Badge>
574575
</div>
575-
{item.calories && (
576+
{item.calories !== undefined && item.protein !== undefined && item.carbs !== undefined && item.fat !== undefined && (
576577
<div className="grid grid-cols-4 gap-2 text-sm text-gray-600 mt-1">
577578
<span><strong>{Math.round(item.calories)}</strong> cal</span>
578-
{item.protein && <span><strong>{Math.round(item.protein)}g</strong> protein</span>}
579-
{item.carbs && <span><strong>{Math.round(item.carbs)}g</strong> carbs</span>}
580-
{item.fat && <span><strong>{Math.round(item.fat)}g</strong> fat</span>}
579+
<span><strong>{Math.round(item.protein)}g</strong> protein</span>
580+
<span><strong>{Math.round(item.carbs)}g</strong> carbs</span>
581+
<span><strong>{Math.round(item.fat)}g</strong> fat</span>
581582
</div>
582583
)}
583584
{item.serving_size && item.serving_unit && (

src/services/FatSecretService.ts

Lines changed: 52 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,20 @@ import { toast } from "@/hooks/use-toast";
22

33
const PROXY_BASE_URL = import.meta.env.VITE_SPARKY_FITNESS_SERVER_URL || "http://localhost:3010/api/fatsecret"; // URL of your Node.js proxy server
44

5-
interface FatSecretFoodItem {
5+
export interface FatSecretFoodItem {
66
food_id: string;
77
food_name: string;
88
brand_name?: string;
99
food_type: string;
1010
food_url: string;
1111
food_description: string;
12-
servings?: {
13-
serving: FatSecretServing[];
14-
};
12+
// Add parsed basic nutrients from food_description
13+
calories?: number;
14+
protein?: number;
15+
carbs?: number;
16+
fat?: number;
17+
serving_size?: number;
18+
serving_unit?: string;
1519
}
1620

1721
interface FatSecretServing {
@@ -65,6 +69,33 @@ interface FatSecretFoodGetResponse {
6569
};
6670
}
6771

72+
// Helper function to parse food_description for nutrients and serving info
73+
const parseFoodDescription = (description: string) => {
74+
const caloriesMatch = description.match(/Calories: (\d+)kcal/);
75+
const fatMatch = description.match(/Fat: ([\d.]+)g/);
76+
const carbsMatch = description.match(/Carbs: ([\d.]+)g/);
77+
const proteinMatch = description.match(/Protein: ([\d.]+)g/);
78+
79+
// Extract serving size and unit (e.g., "Per 1 serving", "Per 100g")
80+
const servingMatch = description.match(/Per ([\d.]+) (.+?) -/);
81+
let serving_size: number | undefined;
82+
let serving_unit: string | undefined;
83+
84+
if (servingMatch) {
85+
serving_size = parseFloat(servingMatch[1]);
86+
serving_unit = servingMatch[2].trim();
87+
}
88+
89+
return {
90+
calories: caloriesMatch ? parseFloat(caloriesMatch[1]) : 0,
91+
fat: fatMatch ? parseFloat(fatMatch[1]) : 0,
92+
carbs: carbsMatch ? parseFloat(carbsMatch[1]) : 0,
93+
protein: proteinMatch ? parseFloat(proteinMatch[1]) : 0,
94+
serving_size,
95+
serving_unit,
96+
};
97+
};
98+
6899
export const searchFatSecretFoods = async (query: string, providerId: string) => {
69100
try {
70101
const response = await fetch(
@@ -90,42 +121,23 @@ export const searchFatSecretFoods = async (query: string, providerId: string) =>
90121

91122
const data: FatSecretSearchResponse = await response.json();
92123
if (data.foods && data.foods.food) {
93-
const detailedFoods: any[] = [];
94-
for (const item of data.foods.food) {
95-
// For each food found in search, fetch its detailed nutrients using food.get.v4
96-
const nutrientData = await getFatSecretNutrients(item.food_id, providerId);
97-
if (nutrientData) {
98-
detailedFoods.push({
99-
id: item.food_id,
100-
name: nutrientData.name,
101-
brand: nutrientData.brand || null,
102-
food_type: item.food_type, // Keep original food_type from search
103-
description: item.food_description, // Keep original description from search
104-
source: "FatSecret",
105-
calories: nutrientData.calories,
106-
protein: nutrientData.protein,
107-
carbs: nutrientData.carbohydrates,
108-
fat: nutrientData.fat,
109-
serving_size: nutrientData.serving_qty,
110-
serving_unit: nutrientData.serving_unit,
111-
// Include other detailed nutrients if needed for initial display
112-
saturated_fat: nutrientData.saturated_fat,
113-
polyunsaturated_fat: nutrientData.polyunsaturated_fat,
114-
monounsaturated_fat: nutrientData.monounsaturated_fat,
115-
trans_fat: nutrientData.trans_fat,
116-
cholesterol: nutrientData.cholesterol,
117-
sodium: nutrientData.sodium,
118-
potassium: nutrientData.potassium,
119-
dietary_fiber: nutrientData.dietary_fiber,
120-
sugars: nutrientData.sugars,
121-
vitamin_a: nutrientData.vitamin_a,
122-
vitamin_c: nutrientData.vitamin_c,
123-
calcium: nutrientData.calcium,
124-
iron: nutrientData.iron,
125-
});
126-
}
127-
}
128-
return detailedFoods;
124+
return data.foods.food.map(item => {
125+
const parsedData = parseFoodDescription(item.food_description);
126+
return {
127+
food_id: item.food_id,
128+
food_name: item.food_name,
129+
brand_name: item.brand_name || null,
130+
food_type: item.food_type,
131+
food_url: item.food_url,
132+
food_description: item.food_description, // Keep original for now, will remove from display in frontend
133+
calories: parsedData.calories,
134+
protein: parsedData.protein,
135+
carbs: parsedData.carbs,
136+
fat: parsedData.fat,
137+
serving_size: parsedData.serving_size,
138+
serving_unit: parsedData.serving_unit,
139+
};
140+
});
129141
}
130142
return [];
131143
} catch (error) {

0 commit comments

Comments
 (0)