Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
114 changes: 65 additions & 49 deletions SparkyFitnessFrontend/src/components/EnhancedFoodSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import { UserCustomNutrient } from "@/types/customNutrient"; // Add import
interface OpenFoodFactsProduct {
product_name: string;
brands?: string;
serving_quantity?: number;
nutriments: {
"energy-kcal_100g"?: number;
proteins_100g?: number;
Expand Down Expand Up @@ -198,6 +199,7 @@ const EnhancedFoodSearch = ({
energyUnit,
convertEnergy,
getEnergyUnitString,
autoScaleOpenFoodFactsImports, // Add auto-scale preference
} = usePreferences(); // Get loggingLevel, itemDisplayLimit, and foodDisplayLimit
const isMobile = useIsMobile();
const platform = isMobile ? "mobile" : "desktop";
Expand Down Expand Up @@ -395,23 +397,29 @@ const EnhancedFoodSearch = ({
};

const convertOpenFoodFactsToFood = (product: OpenFoodFactsProduct): Food => {
// Calculate scaling factor based on serving_quantity (defaults to 100g if not provided)
// Only apply scaling if autoScaleOpenFoodFactsImports preference is enabled
const shouldScale = autoScaleOpenFoodFactsImports && product.serving_quantity && product.serving_quantity > 0;
const servingSize = shouldScale ? product.serving_quantity! : 100;
const scaleFactor = shouldScale ? servingSize / 100 : 1; // Scale from 100g to actual serving size, or 1 if disabled

const defaultVariant: FoodVariant = {
id: "default", // Assign a default ID for now
serving_size: 100,
serving_size: servingSize,
serving_unit: "g",
calories: Math.round(product.nutriments["energy-kcal_100g"] || 0), // Assumed to be in kcal
protein: Math.round((product.nutriments["proteins_100g"] || 0) * 10) / 10,
calories: Math.round((product.nutriments["energy-kcal_100g"] || 0) * scaleFactor), // Assumed to be in kcal, and scaled from 100g to serving
protein: Math.round((product.nutriments["proteins_100g"] || 0) * scaleFactor * 10) / 10,
carbs:
Math.round((product.nutriments["carbohydrates_100g"] || 0) * 10) / 10,
fat: Math.round((product.nutriments["fat_100g"] || 0) * 10) / 10,
Math.round((product.nutriments["carbohydrates_100g"] || 0) * scaleFactor * 10) / 10,
fat: Math.round((product.nutriments["fat_100g"] || 0) * scaleFactor * 10) / 10,
saturated_fat:
Math.round((product.nutriments["saturated-fat_100g"] || 0) * 10) / 10,
Math.round((product.nutriments["saturated-fat_100g"] || 0) * scaleFactor * 10) / 10,
sodium: product.nutriments["sodium_100g"]
? Math.round(product.nutriments["sodium_100g"] * 1000)
? Math.round(product.nutriments["sodium_100g"] * 1000 * scaleFactor)
: 0,
dietary_fiber:
Math.round((product.nutriments["fiber_100g"] || 0) * 10) / 10,
sugars: Math.round((product.nutriments["sugars_100g"] || 0) * 10) / 10,
Math.round((product.nutriments["fiber_100g"] || 0) * scaleFactor * 10) / 10,
sugars: Math.round((product.nutriments["sugars_100g"] || 0) * scaleFactor * 10) / 10,
// Initialize other nutrients to 0 or appropriate defaults
polyunsaturated_fat: 0,
monounsaturated_fat: 0,
Expand All @@ -423,6 +431,7 @@ const EnhancedFoodSearch = ({
calcium: 0,
iron: 0,
is_default: true,
is_locked: shouldScale, // Enable auto-scale only if preference is enabled and scaling was applied
glycemic_index: "None", // Default GI for OpenFoodFacts
};

Expand Down Expand Up @@ -1329,49 +1338,56 @@ const EnhancedFoodSearch = ({

{activeTab === "online" &&
openFoodFactsResults.length > 0 &&
openFoodFactsResults.map((product) => (
<Card
key={product.code}
className="hover:bg-gray-50 dark:hover:bg-gray-700"
>
<CardContent className="p-4">
<div className="flex justify-between items-start">
<div className="flex-1">
<div className="flex items-center space-x-2 mb-2">
<h3 className="font-medium">{product.product_name}</h3>
{product.brands && (
<Badge variant="secondary" className="text-xs">
{product.brands.split(",")[0]}
openFoodFactsResults.map((product) => {
// Calculate display values based on auto-scaling preference
const shouldScale = autoScaleOpenFoodFactsImports && product.serving_quantity && product.serving_quantity > 0;
const servingSize = shouldScale ? product.serving_quantity! : 100;
const scaleFactor = shouldScale ? servingSize / 100 : 1;

return (
<Card
key={product.code}
className="hover:bg-gray-50 dark:hover:bg-gray-700"
>
<CardContent className="p-4">
<div className="flex justify-between items-start">
<div className="flex-1">
<div className="flex items-center space-x-2 mb-2">
<h3 className="font-medium">{product.product_name}</h3>
{product.brands && (
<Badge variant="secondary" className="text-xs">
{product.brands.split(",")[0]}
</Badge>
)}
<Badge variant="outline" className="text-xs">
{t("enhancedFoodSearch.openFoodFacts", "OpenFoodFacts")}
</Badge>
)}
<Badge variant="outline" className="text-xs">
{t("enhancedFoodSearch.openFoodFacts", "OpenFoodFacts")}
</Badge>
</div>
<NutrientGrid food={{
calories: Math.round((product.nutriments["energy-kcal_100g"] || 0) * scaleFactor),
protein: Math.round((product.nutriments["proteins_100g"] || 0) * scaleFactor * 10) / 10,
carbs: Math.round((product.nutriments["carbohydrates_100g"] || 0) * scaleFactor * 10) / 10,
fat: Math.round((product.nutriments["fat_100g"] || 0) * scaleFactor * 10) / 10,
dietary_fiber: Math.round((product.nutriments["fiber_100g"] || 0) * scaleFactor * 10) / 10,
// For OpenFoodFacts, GI is not directly available in product.nutriments,
// so we'll display "None" or handle it as a special case.
glycemic_index: "None"
}} visibleNutrients={visibleNutrients} energyUnit={energyUnit} convertEnergy={convertEnergy} getEnergyUnitString={getEnergyUnitString} customNutrients={customNutrients} />
<p className="text-xs text-gray-500 mt-1">Per {servingSize}g</p>
</div>
<NutrientGrid food={{
calories: product.nutriments["energy-kcal_100g"] || 0,
protein: product.nutriments["proteins_100g"] || 0,
carbs: product.nutriments["carbohydrates_100g"] || 0,
fat: product.nutriments["fat_100g"] || 0,
dietary_fiber: product.nutriments["fiber_100g"] || 0,
// For OpenFoodFacts, GI is not directly available in product.nutriments,
// so we'll display "None" or handle it as a special case.
glycemic_index: "None"
}} visibleNutrients={visibleNutrients} energyUnit={energyUnit} convertEnergy={convertEnergy} getEnergyUnitString={getEnergyUnitString} customNutrients={customNutrients} />
<p className="text-xs text-gray-500 mt-1">Per 100g</p>
<Button
size="sm"
onClick={() => handleOpenFoodFactsEdit(product)}
className="ml-2"
>
<Edit className="w-4 h-4 mr-1" />
{t("enhancedFoodSearch.editAndAdd", "Edit & Add")}
</Button>
</div>
<Button
size="sm"
onClick={() => handleOpenFoodFactsEdit(product)}
className="ml-2"
>
<Edit className="w-4 h-4 mr-1" />
{t("enhancedFoodSearch.editAndAdd", "Edit & Add")}
</Button>
</div>
</CardContent>
</Card>
))}
</CardContent>
</Card>
);
})}

{activeTab === "online" &&
nutritionixResults.length > 0 &&
Expand Down
15 changes: 15 additions & 0 deletions SparkyFitnessFrontend/src/components/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; // Added import
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
import { Separator } from "@/components/ui/separator";
Expand Down Expand Up @@ -115,6 +116,7 @@ const Settings: React.FC<SettingsProps> = ({ onShowAboutDialog }) => {
setLoggingLevel,
itemDisplayLimit,
setItemDisplayLimit, // Add itemDisplayLimit and setItemDisplayLimit
autoScaleOpenFoodFactsImports, setAutoScaleOpenFoodFactsImports, // Add auto-scale preference
loadPreferences: loadUserPreferencesFromContext, // Rename to avoid conflict
saveAllPreferences, // Add saveAllPreferences from context
formatDate, // Destructure formatDate
Expand Down Expand Up @@ -969,6 +971,19 @@ const Settings: React.FC<SettingsProps> = ({ onShowAboutDialog }) => {
</SelectContent>
</Select>
</div>
<div className="flex items-center justify-between col-span-2 py-2">
<div className="space-y-0.5">
<Label htmlFor="auto-scale-openfoodfacts">{t('settings.preferences.autoScaleOpenFoodFacts', 'Auto-scale OpenFoodFacts Imports')}</Label>
<p className="text-sm text-muted-foreground">
{t('settings.preferences.autoScaleOpenFoodFactsHint', 'When enabled, nutrition values from OpenFoodFacts will be automatically scaled from per-100g to the product\'s serving size.')}
</p>
</div>
<Switch
id="auto-scale-openfoodfacts"
checked={autoScaleOpenFoodFactsImports}
onCheckedChange={setAutoScaleOpenFoodFactsImports}
/>
</div>
</div>
<Button onClick={handlePreferencesUpdate} disabled={loading}>
<Save className="h-4 w-4 mr-2" />
Expand Down
24 changes: 24 additions & 0 deletions SparkyFitnessFrontend/src/contexts/PreferencesContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ interface PreferencesContextType {
itemDisplayLimit: number;
calorieGoalAdjustmentMode: 'dynamic' | 'fixed'; // Add new preference
energyUnit: EnergyUnit; // Add energy unit
autoScaleOpenFoodFactsImports: boolean; // Auto-scale nutrition from 100g to serving size
nutrientDisplayPreferences: NutrientPreference[];
water_display_unit: 'ml' | 'oz' | 'liter';
language: string;
Expand All @@ -97,6 +98,7 @@ interface PreferencesContextType {
setItemDisplayLimit: (limit: number) => void;
setCalorieGoalAdjustmentMode: (mode: 'dynamic' | 'fixed') => void; // Add setter for calorie goal adjustment mode
setEnergyUnit: (unit: EnergyUnit) => void; // Add setter for energy unit
setAutoScaleOpenFoodFactsImports: (enabled: boolean) => void; // Add boolean for auto-scale
loadNutrientDisplayPreferences: () => Promise<void>;
setWaterDisplayUnit: (unit: 'ml' | 'oz' | 'liter') => void;
setLanguage: (language: string) => void;
Expand Down Expand Up @@ -144,6 +146,7 @@ export const PreferencesProvider: React.FC<{ children: React.ReactNode }> = ({ c
const [foodDisplayLimit, setFoodDisplayLimitState] = useState<number>(10); // Add state for foodDisplayLimit
const [calorieGoalAdjustmentMode, setCalorieGoalAdjustmentModeState] = useState<'dynamic' | 'fixed'>('dynamic'); // New state for calorie goal adjustment
const [energyUnit, setEnergyUnitState] = useState<EnergyUnit>('kcal'); // Add state for energy unit
const [autoScaleOpenFoodFactsImports, setAutoScaleOpenFoodFactsImportsState] = useState<boolean>(false); // Auto-scale OpenFoodFacts imports
const [nutrientDisplayPreferences, setNutrientDisplayPreferences] = useState<NutrientPreference[]>([]);
const [waterDisplayUnit, setWaterDisplayUnitState] = useState<'ml' | 'oz' | 'liter'>('ml');
const [language, setLanguageState] = useState<string>('en');
Expand Down Expand Up @@ -178,6 +181,7 @@ export const PreferencesProvider: React.FC<{ children: React.ReactNode }> = ({ c
const savedLanguage = localStorage.getItem('language');
const savedCalorieGoalAdjustmentMode = localStorage.getItem('calorieGoalAdjustmentMode') as 'dynamic' | 'fixed';
const savedEnergyUnit = localStorage.getItem('energyUnit') as EnergyUnit; // Load energy unit
const savedAutoScaleOpenFoodFactsImports = localStorage.getItem('autoScaleOpenFoodFactsImports'); // Load auto-scale preference

if (savedWeightUnit) {
setWeightUnitState(savedWeightUnit);
Expand Down Expand Up @@ -207,6 +211,10 @@ export const PreferencesProvider: React.FC<{ children: React.ReactNode }> = ({ c
setEnergyUnitState(savedEnergyUnit);
debug(loggingLevel, "PreferencesProvider: Loaded energyUnit from localStorage:", savedEnergyUnit);
}
if (savedAutoScaleOpenFoodFactsImports !== null) { // Set auto-scale state from localStorage
setAutoScaleOpenFoodFactsImportsState(savedAutoScaleOpenFoodFactsImports === 'true');
debug(loggingLevel, "PreferencesProvider: Loaded autoScaleOpenFoodFactsImports from localStorage:", savedAutoScaleOpenFoodFactsImports);
}
}
}
}, [user, loading]); // Add loading to dependency array
Expand Down Expand Up @@ -236,6 +244,7 @@ export const PreferencesProvider: React.FC<{ children: React.ReactNode }> = ({ c
setLanguageState(data.language || 'en');
setCalorieGoalAdjustmentModeState(data.calorie_goal_adjustment_mode || 'dynamic'); // Set calorie goal adjustment mode state
setEnergyUnitState(data.energy_unit as EnergyUnit || 'kcal'); // Set energy unit state, default to kcal
setAutoScaleOpenFoodFactsImportsState(data.auto_scale_open_food_facts_imports ?? false); // Set auto-scale state, default to false
setBmrAlgorithmState((data.bmr_algorithm as BmrAlgorithm) || BmrAlgorithm.MIFFLIN_ST_JEOR);
setBodyFatAlgorithmState((data.body_fat_algorithm as BodyFatAlgorithm) || BodyFatAlgorithm.US_NAVY);
setIncludeBmrInNetCaloriesState(data.include_bmr_in_net_calories ?? false);
Expand Down Expand Up @@ -293,6 +302,7 @@ export const PreferencesProvider: React.FC<{ children: React.ReactNode }> = ({ c
language: 'en',
calorie_goal_adjustment_mode: 'dynamic', // Add default for new preference
energy_unit: 'kcal', // Add default energy unit
auto_scale_open_food_facts_imports: false, // Add default auto-scale for OpenFoodFacts
selected_diet: 'balanced', // Add default diet
};

Expand Down Expand Up @@ -332,6 +342,7 @@ export const PreferencesProvider: React.FC<{ children: React.ReactNode }> = ({ c
language: string;
calorie_goal_adjustment_mode: 'dynamic' | 'fixed';
energy_unit: EnergyUnit;
auto_scale_open_food_facts_imports: boolean;
bmr_algorithm: BmrAlgorithm;
body_fat_algorithm: BodyFatAlgorithm;
include_bmr_in_net_calories: boolean;
Expand Down Expand Up @@ -373,6 +384,10 @@ export const PreferencesProvider: React.FC<{ children: React.ReactNode }> = ({ c
localStorage.setItem('energyUnit', updates.energy_unit);
debug(loggingLevel, "PreferencesProvider: Saved energyUnit to localStorage:", updates.energy_unit);
}
if (updates.auto_scale_open_food_facts_imports !== undefined) { // Save auto-scale to localStorage
localStorage.setItem('autoScaleOpenFoodFactsImports', String(updates.auto_scale_open_food_facts_imports));
debug(loggingLevel, "PreferencesProvider: Saved autoScaleOpenFoodFactsImports to localStorage:", updates.auto_scale_open_food_facts_imports);
}
// default_food_data_provider_id, logging_level and item_display_limit are not stored in localStorage
// food_display_limit is also not stored in localStorage
return;
Expand Down Expand Up @@ -590,6 +605,12 @@ export const PreferencesProvider: React.FC<{ children: React.ReactNode }> = ({ c
saveAllPreferences({ energyUnit: unit }); // Persist the change
};

const setAutoScaleOpenFoodFactsImports = (enabled: boolean) => {
info(loggingLevel, "PreferencesProvider: Setting auto-scale OpenFoodFacts imports to:", enabled);
setAutoScaleOpenFoodFactsImportsState(enabled);
saveAllPreferences({ autoScaleOpenFoodFactsImports: enabled }); // Persist the change
};

const saveAllPreferences = async (newPrefs?: Partial<PreferencesContextType>) => {
info(loggingLevel, "PreferencesProvider: Saving all preferences to backend.");

Expand All @@ -608,6 +629,7 @@ export const PreferencesProvider: React.FC<{ children: React.ReactNode }> = ({ c
language: newPrefs?.language ?? language,
calorie_goal_adjustment_mode: newPrefs?.calorieGoalAdjustmentMode ?? calorieGoalAdjustmentMode, // Include new preference
energy_unit: newPrefs?.energyUnit ?? energyUnit, // Include energy unit preference
auto_scale_open_food_facts_imports: newPrefs?.autoScaleOpenFoodFactsImports ?? autoScaleOpenFoodFactsImports, // Include auto-scale preference
bmr_algorithm: newPrefs?.bmrAlgorithm ?? bmrAlgorithm,
body_fat_algorithm: newPrefs?.bodyFatAlgorithm ?? bodyFatAlgorithm,
include_bmr_in_net_calories: newPrefs?.includeBmrInNetCalories ?? includeBmrInNetCalories,
Expand Down Expand Up @@ -665,6 +687,7 @@ export const PreferencesProvider: React.FC<{ children: React.ReactNode }> = ({ c
foodDisplayLimit, // Expose foodDisplayLimit
calorieGoalAdjustmentMode, // Expose new preference
energyUnit, // Expose energyUnit
autoScaleOpenFoodFactsImports, // Expose auto-scale OpenFoodFacts imports
nutrientDisplayPreferences,
water_display_unit: waterDisplayUnit,
language,
Expand All @@ -679,6 +702,7 @@ export const PreferencesProvider: React.FC<{ children: React.ReactNode }> = ({ c
setItemDisplayLimit,
setCalorieGoalAdjustmentMode, // Expose new setter
setEnergyUnit, // Expose setEnergyUnit
setAutoScaleOpenFoodFactsImports, // Expose setAutoScaleOpenFoodFactsImports
loadNutrientDisplayPreferences,
setWaterDisplayUnit: setWaterDisplayUnitState,
setLanguage: setLanguageState,
Expand Down
Loading