Skip to content

Commit 493339e

Browse files
authored
Merge pull request #501 from Gtt1229/main
Add option to autoscale foods via OpenFoodFacts
2 parents 7016b3c + 7d2654f commit 493339e

File tree

6 files changed

+161
-52
lines changed

6 files changed

+161
-52
lines changed

SparkyFitnessFrontend/src/components/EnhancedFoodSearch.tsx

Lines changed: 65 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import { UserCustomNutrient } from "@/types/customNutrient"; // Add import
5959
interface OpenFoodFactsProduct {
6060
product_name: string;
6161
brands?: string;
62+
serving_quantity?: number;
6263
nutriments: {
6364
"energy-kcal_100g"?: number;
6465
proteins_100g?: number;
@@ -198,6 +199,7 @@ const EnhancedFoodSearch = ({
198199
energyUnit,
199200
convertEnergy,
200201
getEnergyUnitString,
202+
autoScaleOpenFoodFactsImports, // Add auto-scale preference
201203
} = usePreferences(); // Get loggingLevel, itemDisplayLimit, and foodDisplayLimit
202204
const isMobile = useIsMobile();
203205
const platform = isMobile ? "mobile" : "desktop";
@@ -395,23 +397,29 @@ const EnhancedFoodSearch = ({
395397
};
396398

397399
const convertOpenFoodFactsToFood = (product: OpenFoodFactsProduct): Food => {
400+
// Calculate scaling factor based on serving_quantity (defaults to 100g if not provided)
401+
// Only apply scaling if autoScaleOpenFoodFactsImports preference is enabled
402+
const shouldScale = autoScaleOpenFoodFactsImports && product.serving_quantity && product.serving_quantity > 0;
403+
const servingSize = shouldScale ? product.serving_quantity! : 100;
404+
const scaleFactor = shouldScale ? servingSize / 100 : 1; // Scale from 100g to actual serving size, or 1 if disabled
405+
398406
const defaultVariant: FoodVariant = {
399407
id: "default", // Assign a default ID for now
400-
serving_size: 100,
408+
serving_size: servingSize,
401409
serving_unit: "g",
402-
calories: Math.round(product.nutriments["energy-kcal_100g"] || 0), // Assumed to be in kcal
403-
protein: Math.round((product.nutriments["proteins_100g"] || 0) * 10) / 10,
410+
calories: Math.round((product.nutriments["energy-kcal_100g"] || 0) * scaleFactor), // Assumed to be in kcal, and scaled from 100g to serving
411+
protein: Math.round((product.nutriments["proteins_100g"] || 0) * scaleFactor * 10) / 10,
404412
carbs:
405-
Math.round((product.nutriments["carbohydrates_100g"] || 0) * 10) / 10,
406-
fat: Math.round((product.nutriments["fat_100g"] || 0) * 10) / 10,
413+
Math.round((product.nutriments["carbohydrates_100g"] || 0) * scaleFactor * 10) / 10,
414+
fat: Math.round((product.nutriments["fat_100g"] || 0) * scaleFactor * 10) / 10,
407415
saturated_fat:
408-
Math.round((product.nutriments["saturated-fat_100g"] || 0) * 10) / 10,
416+
Math.round((product.nutriments["saturated-fat_100g"] || 0) * scaleFactor * 10) / 10,
409417
sodium: product.nutriments["sodium_100g"]
410-
? Math.round(product.nutriments["sodium_100g"] * 1000)
418+
? Math.round(product.nutriments["sodium_100g"] * 1000 * scaleFactor)
411419
: 0,
412420
dietary_fiber:
413-
Math.round((product.nutriments["fiber_100g"] || 0) * 10) / 10,
414-
sugars: Math.round((product.nutriments["sugars_100g"] || 0) * 10) / 10,
421+
Math.round((product.nutriments["fiber_100g"] || 0) * scaleFactor * 10) / 10,
422+
sugars: Math.round((product.nutriments["sugars_100g"] || 0) * scaleFactor * 10) / 10,
415423
// Initialize other nutrients to 0 or appropriate defaults
416424
polyunsaturated_fat: 0,
417425
monounsaturated_fat: 0,
@@ -423,6 +431,7 @@ const EnhancedFoodSearch = ({
423431
calcium: 0,
424432
iron: 0,
425433
is_default: true,
434+
is_locked: shouldScale, // Enable auto-scale only if preference is enabled and scaling was applied
426435
glycemic_index: "None", // Default GI for OpenFoodFacts
427436
};
428437

@@ -1329,49 +1338,56 @@ const EnhancedFoodSearch = ({
13291338

13301339
{activeTab === "online" &&
13311340
openFoodFactsResults.length > 0 &&
1332-
openFoodFactsResults.map((product) => (
1333-
<Card
1334-
key={product.code}
1335-
className="hover:bg-gray-50 dark:hover:bg-gray-700"
1336-
>
1337-
<CardContent className="p-4">
1338-
<div className="flex justify-between items-start">
1339-
<div className="flex-1">
1340-
<div className="flex items-center space-x-2 mb-2">
1341-
<h3 className="font-medium">{product.product_name}</h3>
1342-
{product.brands && (
1343-
<Badge variant="secondary" className="text-xs">
1344-
{product.brands.split(",")[0]}
1341+
openFoodFactsResults.map((product) => {
1342+
// Calculate display values based on auto-scaling preference
1343+
const shouldScale = autoScaleOpenFoodFactsImports && product.serving_quantity && product.serving_quantity > 0;
1344+
const servingSize = shouldScale ? product.serving_quantity! : 100;
1345+
const scaleFactor = shouldScale ? servingSize / 100 : 1;
1346+
1347+
return (
1348+
<Card
1349+
key={product.code}
1350+
className="hover:bg-gray-50 dark:hover:bg-gray-700"
1351+
>
1352+
<CardContent className="p-4">
1353+
<div className="flex justify-between items-start">
1354+
<div className="flex-1">
1355+
<div className="flex items-center space-x-2 mb-2">
1356+
<h3 className="font-medium">{product.product_name}</h3>
1357+
{product.brands && (
1358+
<Badge variant="secondary" className="text-xs">
1359+
{product.brands.split(",")[0]}
1360+
</Badge>
1361+
)}
1362+
<Badge variant="outline" className="text-xs">
1363+
{t("enhancedFoodSearch.openFoodFacts", "OpenFoodFacts")}
13451364
</Badge>
1346-
)}
1347-
<Badge variant="outline" className="text-xs">
1348-
{t("enhancedFoodSearch.openFoodFacts", "OpenFoodFacts")}
1349-
</Badge>
1365+
</div>
1366+
<NutrientGrid food={{
1367+
calories: Math.round((product.nutriments["energy-kcal_100g"] || 0) * scaleFactor),
1368+
protein: Math.round((product.nutriments["proteins_100g"] || 0) * scaleFactor * 10) / 10,
1369+
carbs: Math.round((product.nutriments["carbohydrates_100g"] || 0) * scaleFactor * 10) / 10,
1370+
fat: Math.round((product.nutriments["fat_100g"] || 0) * scaleFactor * 10) / 10,
1371+
dietary_fiber: Math.round((product.nutriments["fiber_100g"] || 0) * scaleFactor * 10) / 10,
1372+
// For OpenFoodFacts, GI is not directly available in product.nutriments,
1373+
// so we'll display "None" or handle it as a special case.
1374+
glycemic_index: "None"
1375+
}} visibleNutrients={visibleNutrients} energyUnit={energyUnit} convertEnergy={convertEnergy} getEnergyUnitString={getEnergyUnitString} customNutrients={customNutrients} />
1376+
<p className="text-xs text-gray-500 mt-1">Per {servingSize}g</p>
13501377
</div>
1351-
<NutrientGrid food={{
1352-
calories: product.nutriments["energy-kcal_100g"] || 0,
1353-
protein: product.nutriments["proteins_100g"] || 0,
1354-
carbs: product.nutriments["carbohydrates_100g"] || 0,
1355-
fat: product.nutriments["fat_100g"] || 0,
1356-
dietary_fiber: product.nutriments["fiber_100g"] || 0,
1357-
// For OpenFoodFacts, GI is not directly available in product.nutriments,
1358-
// so we'll display "None" or handle it as a special case.
1359-
glycemic_index: "None"
1360-
}} visibleNutrients={visibleNutrients} energyUnit={energyUnit} convertEnergy={convertEnergy} getEnergyUnitString={getEnergyUnitString} customNutrients={customNutrients} />
1361-
<p className="text-xs text-gray-500 mt-1">Per 100g</p>
1378+
<Button
1379+
size="sm"
1380+
onClick={() => handleOpenFoodFactsEdit(product)}
1381+
className="ml-2"
1382+
>
1383+
<Edit className="w-4 h-4 mr-1" />
1384+
{t("enhancedFoodSearch.editAndAdd", "Edit & Add")}
1385+
</Button>
13621386
</div>
1363-
<Button
1364-
size="sm"
1365-
onClick={() => handleOpenFoodFactsEdit(product)}
1366-
className="ml-2"
1367-
>
1368-
<Edit className="w-4 h-4 mr-1" />
1369-
{t("enhancedFoodSearch.editAndAdd", "Edit & Add")}
1370-
</Button>
1371-
</div>
1372-
</CardContent>
1373-
</Card>
1374-
))}
1387+
</CardContent>
1388+
</Card>
1389+
);
1390+
})}
13751391

13761392
{activeTab === "online" &&
13771393
nutritionixResults.length > 0 &&

SparkyFitnessFrontend/src/components/Settings.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
SelectTrigger,
1919
SelectValue,
2020
} from "@/components/ui/select";
21+
import { Switch } from "@/components/ui/switch";
2122
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; // Added import
2223
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
2324
import { Separator } from "@/components/ui/separator";
@@ -89,6 +90,7 @@ const Settings: React.FC<SettingsProps> = ({ onShowAboutDialog }) => {
8990
setLoggingLevel,
9091
itemDisplayLimit,
9192
setItemDisplayLimit, // Add itemDisplayLimit and setItemDisplayLimit
93+
autoScaleOpenFoodFactsImports, setAutoScaleOpenFoodFactsImports, // Add auto-scale preference
9294
loadPreferences: loadUserPreferencesFromContext, // Rename to avoid conflict
9395
saveAllPreferences, // Add saveAllPreferences from context
9496
formatDate, // Destructure formatDate
@@ -943,6 +945,19 @@ const Settings: React.FC<SettingsProps> = ({ onShowAboutDialog }) => {
943945
</SelectContent>
944946
</Select>
945947
</div>
948+
<div className="flex items-center justify-between col-span-2 py-2">
949+
<div className="space-y-0.5">
950+
<Label htmlFor="auto-scale-openfoodfacts">{t('settings.preferences.autoScaleOpenFoodFacts', 'Auto-scale OpenFoodFacts Imports')}</Label>
951+
<p className="text-sm text-muted-foreground">
952+
{t('settings.preferences.autoScaleOpenFoodFactsHint', 'When enabled, nutrition values from OpenFoodFacts will be automatically scaled from per-100g to the product\'s serving size.')}
953+
</p>
954+
</div>
955+
<Switch
956+
id="auto-scale-openfoodfacts"
957+
checked={autoScaleOpenFoodFactsImports}
958+
onCheckedChange={setAutoScaleOpenFoodFactsImports}
959+
/>
960+
</div>
946961
</div>
947962
<Button onClick={handlePreferencesUpdate} disabled={loading}>
948963
<Save className="h-4 w-4 mr-2" />

SparkyFitnessFrontend/src/contexts/PreferencesContext.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ interface PreferencesContextType {
7575
itemDisplayLimit: number;
7676
calorieGoalAdjustmentMode: 'dynamic' | 'fixed'; // Add new preference
7777
energyUnit: EnergyUnit; // Add energy unit
78+
autoScaleOpenFoodFactsImports: boolean; // Auto-scale nutrition from 100g to serving size
7879
nutrientDisplayPreferences: NutrientPreference[];
7980
water_display_unit: 'ml' | 'oz' | 'liter';
8081
language: string;
@@ -97,6 +98,7 @@ interface PreferencesContextType {
9798
setItemDisplayLimit: (limit: number) => void;
9899
setCalorieGoalAdjustmentMode: (mode: 'dynamic' | 'fixed') => void; // Add setter for calorie goal adjustment mode
99100
setEnergyUnit: (unit: EnergyUnit) => void; // Add setter for energy unit
101+
setAutoScaleOpenFoodFactsImports: (enabled: boolean) => void; // Add boolean for auto-scale
100102
loadNutrientDisplayPreferences: () => Promise<void>;
101103
setWaterDisplayUnit: (unit: 'ml' | 'oz' | 'liter') => void;
102104
setLanguage: (language: string) => void;
@@ -144,6 +146,7 @@ export const PreferencesProvider: React.FC<{ children: React.ReactNode }> = ({ c
144146
const [foodDisplayLimit, setFoodDisplayLimitState] = useState<number>(10); // Add state for foodDisplayLimit
145147
const [calorieGoalAdjustmentMode, setCalorieGoalAdjustmentModeState] = useState<'dynamic' | 'fixed'>('dynamic'); // New state for calorie goal adjustment
146148
const [energyUnit, setEnergyUnitState] = useState<EnergyUnit>('kcal'); // Add state for energy unit
149+
const [autoScaleOpenFoodFactsImports, setAutoScaleOpenFoodFactsImportsState] = useState<boolean>(false); // Auto-scale OpenFoodFacts imports
147150
const [nutrientDisplayPreferences, setNutrientDisplayPreferences] = useState<NutrientPreference[]>([]);
148151
const [waterDisplayUnit, setWaterDisplayUnitState] = useState<'ml' | 'oz' | 'liter'>('ml');
149152
const [language, setLanguageState] = useState<string>('en');
@@ -178,6 +181,7 @@ export const PreferencesProvider: React.FC<{ children: React.ReactNode }> = ({ c
178181
const savedLanguage = localStorage.getItem('language');
179182
const savedCalorieGoalAdjustmentMode = localStorage.getItem('calorieGoalAdjustmentMode') as 'dynamic' | 'fixed';
180183
const savedEnergyUnit = localStorage.getItem('energyUnit') as EnergyUnit; // Load energy unit
184+
const savedAutoScaleOpenFoodFactsImports = localStorage.getItem('autoScaleOpenFoodFactsImports'); // Load auto-scale preference
181185

182186
if (savedWeightUnit) {
183187
setWeightUnitState(savedWeightUnit);
@@ -207,6 +211,10 @@ export const PreferencesProvider: React.FC<{ children: React.ReactNode }> = ({ c
207211
setEnergyUnitState(savedEnergyUnit);
208212
debug(loggingLevel, "PreferencesProvider: Loaded energyUnit from localStorage:", savedEnergyUnit);
209213
}
214+
if (savedAutoScaleOpenFoodFactsImports !== null) { // Set auto-scale state from localStorage
215+
setAutoScaleOpenFoodFactsImportsState(savedAutoScaleOpenFoodFactsImports === 'true');
216+
debug(loggingLevel, "PreferencesProvider: Loaded autoScaleOpenFoodFactsImports from localStorage:", savedAutoScaleOpenFoodFactsImports);
217+
}
210218
}
211219
}
212220
}, [user, loading]); // Add loading to dependency array
@@ -236,6 +244,7 @@ export const PreferencesProvider: React.FC<{ children: React.ReactNode }> = ({ c
236244
setLanguageState(data.language || 'en');
237245
setCalorieGoalAdjustmentModeState(data.calorie_goal_adjustment_mode || 'dynamic'); // Set calorie goal adjustment mode state
238246
setEnergyUnitState(data.energy_unit as EnergyUnit || 'kcal'); // Set energy unit state, default to kcal
247+
setAutoScaleOpenFoodFactsImportsState(data.auto_scale_open_food_facts_imports ?? false); // Set auto-scale state, default to false
239248
setBmrAlgorithmState((data.bmr_algorithm as BmrAlgorithm) || BmrAlgorithm.MIFFLIN_ST_JEOR);
240249
setBodyFatAlgorithmState((data.body_fat_algorithm as BodyFatAlgorithm) || BodyFatAlgorithm.US_NAVY);
241250
setIncludeBmrInNetCaloriesState(data.include_bmr_in_net_calories ?? false);
@@ -293,6 +302,7 @@ export const PreferencesProvider: React.FC<{ children: React.ReactNode }> = ({ c
293302
language: 'en',
294303
calorie_goal_adjustment_mode: 'dynamic', // Add default for new preference
295304
energy_unit: 'kcal', // Add default energy unit
305+
auto_scale_open_food_facts_imports: false, // Add default auto-scale for OpenFoodFacts
296306
selected_diet: 'balanced', // Add default diet
297307
};
298308

@@ -332,6 +342,7 @@ export const PreferencesProvider: React.FC<{ children: React.ReactNode }> = ({ c
332342
language: string;
333343
calorie_goal_adjustment_mode: 'dynamic' | 'fixed';
334344
energy_unit: EnergyUnit;
345+
auto_scale_open_food_facts_imports: boolean;
335346
bmr_algorithm: BmrAlgorithm;
336347
body_fat_algorithm: BodyFatAlgorithm;
337348
include_bmr_in_net_calories: boolean;
@@ -373,6 +384,10 @@ export const PreferencesProvider: React.FC<{ children: React.ReactNode }> = ({ c
373384
localStorage.setItem('energyUnit', updates.energy_unit);
374385
debug(loggingLevel, "PreferencesProvider: Saved energyUnit to localStorage:", updates.energy_unit);
375386
}
387+
if (updates.auto_scale_open_food_facts_imports !== undefined) { // Save auto-scale to localStorage
388+
localStorage.setItem('autoScaleOpenFoodFactsImports', String(updates.auto_scale_open_food_facts_imports));
389+
debug(loggingLevel, "PreferencesProvider: Saved autoScaleOpenFoodFactsImports to localStorage:", updates.auto_scale_open_food_facts_imports);
390+
}
376391
// default_food_data_provider_id, logging_level and item_display_limit are not stored in localStorage
377392
// food_display_limit is also not stored in localStorage
378393
return;
@@ -590,6 +605,12 @@ export const PreferencesProvider: React.FC<{ children: React.ReactNode }> = ({ c
590605
saveAllPreferences({ energyUnit: unit }); // Persist the change
591606
};
592607

608+
const setAutoScaleOpenFoodFactsImports = (enabled: boolean) => {
609+
info(loggingLevel, "PreferencesProvider: Setting auto-scale OpenFoodFacts imports to:", enabled);
610+
setAutoScaleOpenFoodFactsImportsState(enabled);
611+
saveAllPreferences({ autoScaleOpenFoodFactsImports: enabled }); // Persist the change
612+
};
613+
593614
const saveAllPreferences = async (newPrefs?: Partial<PreferencesContextType>) => {
594615
info(loggingLevel, "PreferencesProvider: Saving all preferences to backend.");
595616

@@ -608,6 +629,7 @@ export const PreferencesProvider: React.FC<{ children: React.ReactNode }> = ({ c
608629
language: newPrefs?.language ?? language,
609630
calorie_goal_adjustment_mode: newPrefs?.calorieGoalAdjustmentMode ?? calorieGoalAdjustmentMode, // Include new preference
610631
energy_unit: newPrefs?.energyUnit ?? energyUnit, // Include energy unit preference
632+
auto_scale_open_food_facts_imports: newPrefs?.autoScaleOpenFoodFactsImports ?? autoScaleOpenFoodFactsImports, // Include auto-scale preference
611633
bmr_algorithm: newPrefs?.bmrAlgorithm ?? bmrAlgorithm,
612634
body_fat_algorithm: newPrefs?.bodyFatAlgorithm ?? bodyFatAlgorithm,
613635
include_bmr_in_net_calories: newPrefs?.includeBmrInNetCalories ?? includeBmrInNetCalories,
@@ -665,6 +687,7 @@ export const PreferencesProvider: React.FC<{ children: React.ReactNode }> = ({ c
665687
foodDisplayLimit, // Expose foodDisplayLimit
666688
calorieGoalAdjustmentMode, // Expose new preference
667689
energyUnit, // Expose energyUnit
690+
autoScaleOpenFoodFactsImports, // Expose auto-scale OpenFoodFacts imports
668691
nutrientDisplayPreferences,
669692
water_display_unit: waterDisplayUnit,
670693
language,
@@ -679,6 +702,7 @@ export const PreferencesProvider: React.FC<{ children: React.ReactNode }> = ({ c
679702
setItemDisplayLimit,
680703
setCalorieGoalAdjustmentMode, // Expose new setter
681704
setEnergyUnit, // Expose setEnergyUnit
705+
setAutoScaleOpenFoodFactsImports, // Expose setAutoScaleOpenFoodFactsImports
682706
loadNutrientDisplayPreferences,
683707
setWaterDisplayUnit: setWaterDisplayUnitState,
684708
setLanguage: setLanguageState,

0 commit comments

Comments
 (0)