feat: Add "Show other options" to surface nutritionally equivalent food alternatives (#10)
* Initial plan * feat: Add 'Show other options' feature to find similar foods by category and bestFor Agent-Logs-Url: https://github.com/RMCampos/calories-tracker/sessions/00437e19-3c33-48b4-8d15-f29ac843a20f Co-authored-by: RMCampos <2219519+RMCampos@users.noreply.github.com> * fix: Address code review issues - string type, dedup logic, HTML escaping Agent-Logs-Url: https://github.com/RMCampos/calories-tracker/sessions/00437e19-3c33-48b4-8d15-f29ac843a20f Co-authored-by: RMCampos <2219519+RMCampos@users.noreply.github.com> * fix: Add full HTML escaping including quotes in alternatives display Agent-Logs-Url: https://github.com/RMCampos/calories-tracker/sessions/00437e19-3c33-48b4-8d15-f29ac843a20f Co-authored-by: RMCampos <2219519+RMCampos@users.noreply.github.com> * feat: improve food options by food type --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: RMCampos <2219519+RMCampos@users.noreply.github.com> Co-authored-by: Ricardo Campos <ricardompcampos@gmail.com>
This commit is contained in:
+15
@@ -298,6 +298,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group checkbox-group">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="enableAlternatives" class="ai-checkbox">
|
||||||
|
<span>Show other options</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Alternatives Container -->
|
||||||
|
<div class="display-none" id="alternatives-container">
|
||||||
|
<div class="alternatives-list-container">
|
||||||
|
<h3 class="alternatives-title">Similar Food Options</h3>
|
||||||
|
<div id="alternatives-list"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group checkbox-group">
|
<div class="form-group checkbox-group">
|
||||||
<label class="checkbox-label">
|
<label class="checkbox-label">
|
||||||
<input type="checkbox" id="enableAINutrition" class="ai-checkbox">
|
<input type="checkbox" id="enableAINutrition" class="ai-checkbox">
|
||||||
|
|||||||
+8
-7
@@ -3,11 +3,11 @@ import { FoodItem } from "./types";
|
|||||||
export const foodDatabase: FoodItem[] = [
|
export const foodDatabase: FoodItem[] = [
|
||||||
// Gorduras - Fats
|
// Gorduras - Fats
|
||||||
{ name: 'Abacate', nameEn: 'Avocado', info: { calories: 96, protein: 1.2, fat: 8.4, carbs: 6, fiber: 6.3, category: 'fats', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
{ name: 'Abacate', nameEn: 'Avocado', info: { calories: 96, protein: 1.2, fat: 8.4, carbs: 6, fiber: 6.3, category: 'fats', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
{ name: 'Amêndoas', nameEn: 'Almonds', info: { calories: 581, protein: 18.6, fat: 47.3, carbs: 29.5, fiber: 11.6, category: 'fats', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks', 'lunch', 'dinner'] } },
|
{ name: 'Amêndoas', nameEn: 'Almonds', info: { calories: 581, protein: 18.6, fat: 47.3, carbs: 29.5, fiber: 11.6, category: 'fats', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
{ name: 'Amendoin natural', nameEn: 'Natural Peanut', info: { calories: 611, protein: 26, fat: 49, carbs: 16, fiber: 8.5, category: 'fats', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks', 'lunch', 'dinner'] } },
|
{ name: 'Amendoin natural', nameEn: 'Natural Peanut', info: { calories: 611, protein: 26, fat: 49, carbs: 16, fiber: 8.5, category: 'fats', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
{ name: 'Azeite de Oliva EV', nameEn: 'Extra Virgin Olive Oil', info: { calories: 844, protein: 0, fat: 100, carbs: 0, fiber: 0, category: 'fats', alkaline: false, bestFor: ['lunch', 'dinner'] } },
|
{ name: 'Azeite de Oliva EV', nameEn: 'Extra Virgin Olive Oil', info: { calories: 844, protein: 0, fat: 100, carbs: 0, fiber: 0, category: 'fats', alkaline: false, bestFor: ['lunch', 'dinner'] } },
|
||||||
{ name: 'Castanha de Caju', nameEn: 'Cashew Nut', info: { calories: 570, protein: 18.5, fat: 46.3, carbs: 29.1, fiber: 3.7, category: 'fats', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks', 'lunch', 'dinner'] } },
|
{ name: 'Castanha de Caju', nameEn: 'Cashew Nut', info: { calories: 570, protein: 18.5, fat: 46.3, carbs: 29.1, fiber: 3.7, category: 'fats', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
{ name: 'Castanha do Pará', nameEn: 'Brazil Nut', info: { calories: 643, protein: 14.5, fat: 63.5, carbs: 15.1, fiber: 7.9, category: 'fats', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks', 'lunch', 'dinner'] } },
|
{ name: 'Castanha do Pará', nameEn: 'Brazil Nut', info: { calories: 643, protein: 14.5, fat: 63.5, carbs: 15.1, fiber: 7.9, category: 'fats', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
{ name: 'Semente de Chia', nameEn: 'Chia Seed', info: { calories: 490, protein: 15.6, fat: 30.7, carbs: 43.8, fiber: 37.7, category: 'fats', alkaline: true, bestFor: ['breakfast', 'brunch'] } },
|
{ name: 'Semente de Chia', nameEn: 'Chia Seed', info: { calories: 490, protein: 15.6, fat: 30.7, carbs: 43.8, fiber: 37.7, category: 'fats', alkaline: true, bestFor: ['breakfast', 'brunch'] } },
|
||||||
{ name: 'Semente de Linhaça Dourada', nameEn: 'Golden Flaxseed', info: { calories: 515, protein: 20, fat: 35, carbs: 30, fiber: 26, category: 'fats', alkaline: true, bestFor: ['breakfast', 'brunch'] } },
|
{ name: 'Semente de Linhaça Dourada', nameEn: 'Golden Flaxseed', info: { calories: 515, protein: 20, fat: 35, carbs: 30, fiber: 26, category: 'fats', alkaline: true, bestFor: ['breakfast', 'brunch'] } },
|
||||||
{ name: 'Semente de Gergelim', nameEn: 'Sesame Seed', info: { calories: 584, protein: 21.2, fat: 50.4, carbs: 21.6, fiber: 11.9, category: 'fats', alkaline: true, bestFor: ['breakfast', 'brunch'] } },
|
{ name: 'Semente de Gergelim', nameEn: 'Sesame Seed', info: { calories: 584, protein: 21.2, fat: 50.4, carbs: 21.6, fiber: 11.9, category: 'fats', alkaline: true, bestFor: ['breakfast', 'brunch'] } },
|
||||||
@@ -44,9 +44,6 @@ export const foodDatabase: FoodItem[] = [
|
|||||||
|
|
||||||
// Carboidratos, amiláceos - Carbs
|
// Carboidratos, amiláceos - Carbs
|
||||||
{ name: 'Grão de Bico, cozido', nameEn: 'Chickpeas, cooked', info: { calories: 180, protein: 9.5, fat: 3, carbs: 30, fiber: 8.6, category: 'carbs', alkaline: false, bestFor: ['lunch', 'dinner'] } },
|
{ name: 'Grão de Bico, cozido', nameEn: 'Chickpeas, cooked', info: { calories: 180, protein: 9.5, fat: 3, carbs: 30, fiber: 8.6, category: 'carbs', alkaline: false, bestFor: ['lunch', 'dinner'] } },
|
||||||
{ name: 'Banana Nanica', nameEn: 'Banana Nanica', info: { calories: 92, protein: 1.4, fat: 0.1, carbs: 23.8, fiber: 1.9, category: 'carbs', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
|
||||||
{ name: 'Manga Palmer', nameEn: 'Mango Palmer', info: { calories: 72, protein: 0.4, fat: 0.2, carbs: 19.4, fiber: 1.6, category: 'carbs', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
|
||||||
{ name: 'Mamão', nameEn: 'Papaya', info: { calories: 40, protein: 0.5, fat: 0.1, carbs: 10.4, fiber: 1, category: 'carbs', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
|
||||||
{ name: 'Batata doce, assada', nameEn: 'Sweet Potato, baked', info: { calories: 90, protein: 2.0, fat: 0.1, carbs: 20.7, fiber: 3.3, category: 'carbs', alkaline: true, bestFor: ['lunch', 'dinner'] } },
|
{ name: 'Batata doce, assada', nameEn: 'Sweet Potato, baked', info: { calories: 90, protein: 2.0, fat: 0.1, carbs: 20.7, fiber: 3.3, category: 'carbs', alkaline: true, bestFor: ['lunch', 'dinner'] } },
|
||||||
{ name: 'Batata doce, cozida', nameEn: 'Sweet Potato, boiled', info: { calories: 77, protein: 0.6, fat: 0.1, carbs: 18.4, fiber: 2.2, category: 'carbs', alkaline: true, bestFor: ['lunch', 'dinner'] } },
|
{ name: 'Batata doce, cozida', nameEn: 'Sweet Potato, boiled', info: { calories: 77, protein: 0.6, fat: 0.1, carbs: 18.4, fiber: 2.2, category: 'carbs', alkaline: true, bestFor: ['lunch', 'dinner'] } },
|
||||||
{ name: 'Batata inglesa, assada', nameEn: 'Potato, baked', info: { calories: 94, protein: 2.1, fat: 0.1, carbs: 21.8, fiber: 2.1, category: 'carbs', alkaline: true, bestFor: ['lunch', 'dinner'] } },
|
{ name: 'Batata inglesa, assada', nameEn: 'Potato, baked', info: { calories: 94, protein: 2.1, fat: 0.1, carbs: 21.8, fiber: 2.1, category: 'carbs', alkaline: true, bestFor: ['lunch', 'dinner'] } },
|
||||||
@@ -92,7 +89,11 @@ export const foodDatabase: FoodItem[] = [
|
|||||||
// Frutas - Fruits
|
// Frutas - Fruits
|
||||||
{ name: 'Ameixa, crua', nameEn: 'Raw Plum', info: { calories: 53, protein: 0.8, fat: 0, carbs: 13.9, fiber: 2.4, category: 'fruits', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
{ name: 'Ameixa, crua', nameEn: 'Raw Plum', info: { calories: 53, protein: 0.8, fat: 0, carbs: 13.9, fiber: 2.4, category: 'fruits', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
{ name: 'Abacaxi', nameEn: 'Pineapple', info: { calories: 48, protein: 0.9, fat: 0.1, carbs: 12.3, fiber: 1, category: 'fruits', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
{ name: 'Abacaxi', nameEn: 'Pineapple', info: { calories: 48, protein: 0.9, fat: 0.1, carbs: 12.3, fiber: 1, category: 'fruits', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
|
{ name: 'Banana Nanica', nameEn: 'Banana Nanica', info: { calories: 92, protein: 1.4, fat: 0.1, carbs: 23.8, fiber: 1.9, category: 'fruits', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
{ name: 'Laranja', nameEn: 'Orange', info: { calories: 37, protein: 1, fat: 0.1, carbs: 8.9, fiber: 0.8, category: 'fruits', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
{ name: 'Laranja', nameEn: 'Orange', info: { calories: 37, protein: 1, fat: 0.1, carbs: 8.9, fiber: 0.8, category: 'fruits', alkaline: false, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
|
{ name: 'Manga Palmer', nameEn: 'Mango Palmer', info: { calories: 72, protein: 0.4, fat: 0.2, carbs: 19.4, fiber: 1.6, category: 'fruits', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
|
{ name: 'Mamão', nameEn: 'Papaya', info: { calories: 40, protein: 0.5, fat: 0.1, carbs: 10.4, fiber: 1, category: 'fruits', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
|
|
||||||
{ name: 'Maçã', nameEn: 'Apple', info: { calories: 56, protein: 0.3, fat: 0, carbs: 15.2, fiber: 1.3, category: 'fruits', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
{ name: 'Maçã', nameEn: 'Apple', info: { calories: 56, protein: 0.3, fat: 0, carbs: 15.2, fiber: 1.3, category: 'fruits', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
{ name: 'Melancia', nameEn: 'Watermelon', info: { calories: 33, protein: 0.9, fat: 0, carbs: 8.1, fiber: 0.1, category: 'fruits', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
{ name: 'Melancia', nameEn: 'Watermelon', info: { calories: 33, protein: 0.9, fat: 0, carbs: 8.1, fiber: 0.1, category: 'fruits', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
{ name: 'Melão', nameEn: 'Melon', info: { calories: 29, protein: 0.7, fat: 0, carbs: 7.5, fiber: 0.3, category: 'fruits', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
{ name: 'Melão', nameEn: 'Melon', info: { calories: 29, protein: 0.7, fat: 0, carbs: 7.5, fiber: 0.3, category: 'fruits', alkaline: true, bestFor: ['breakfast', 'brunch', 'snacks'] } },
|
||||||
|
|||||||
+165
@@ -420,6 +420,7 @@ function clearEditing() {
|
|||||||
getInputById('gramAmount').value = '100';
|
getInputById('gramAmount').value = '100';
|
||||||
showFoodPreview(false);
|
showFoodPreview(false);
|
||||||
hideAINutritionCard();
|
hideAINutritionCard();
|
||||||
|
hideAlternativesContainer();
|
||||||
getDivById('add-foot-title').innerHTML = 'Add Food Entry';
|
getDivById('add-foot-title').innerHTML = 'Add Food Entry';
|
||||||
getButtonById('add-food-btn').innerHTML = 'Add Food';
|
getButtonById('add-food-btn').innerHTML = 'Add Food';
|
||||||
getButtonById('cancel-food-btn').style.display = 'none';
|
getButtonById('cancel-food-btn').style.display = 'none';
|
||||||
@@ -476,6 +477,162 @@ const getFoodItemByName = (foodName: string): FoodItem => {
|
|||||||
return foodDataSearch[0];
|
return foodDataSearch[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AlternativeResult = {
|
||||||
|
food: FoodItem;
|
||||||
|
quantityGrams: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function findAlternatives(
|
||||||
|
current: FoodItem,
|
||||||
|
currentQuantityGrams: number,
|
||||||
|
allFoods: FoodItem[]
|
||||||
|
): AlternativeResult[] {
|
||||||
|
if (currentQuantityGrams <= 0) return [];
|
||||||
|
|
||||||
|
const ratio = currentQuantityGrams / 100;
|
||||||
|
const target = {
|
||||||
|
calories: current.info.calories * ratio,
|
||||||
|
protein: current.info.protein * ratio,
|
||||||
|
carbs: current.info.carbs * ratio,
|
||||||
|
fat: current.info.fat * ratio,
|
||||||
|
};
|
||||||
|
|
||||||
|
const sameBestFor = (a: string[], b: string[]) =>
|
||||||
|
[...a].sort().join(',') === [...b].sort().join(',');
|
||||||
|
|
||||||
|
// Consider different macros based in the food category
|
||||||
|
let filtered = allFoods
|
||||||
|
.filter(f =>
|
||||||
|
f.name !== current.name &&
|
||||||
|
f.info.category === current.info.category &&
|
||||||
|
sameBestFor(f.info.bestFor, current.info.bestFor)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Consider calories and fat
|
||||||
|
if (current.info.category === 'fats' || current.info.category === 'dairy') {
|
||||||
|
filtered = filtered
|
||||||
|
.flatMap(candidate => {
|
||||||
|
const p = candidate.info;
|
||||||
|
|
||||||
|
const maxGrams = Math.min(
|
||||||
|
p.calories > 0 ? (target.calories / p.calories) * 100 : Infinity,
|
||||||
|
p.fat > 0 ? (target.fat / p.fat) * 100 : Infinity,
|
||||||
|
);
|
||||||
|
|
||||||
|
return maxGrams > 0 ? [{ food: candidate, quantityGrams: Math.floor(maxGrams) }] : [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Consider calories and proteins
|
||||||
|
else if (current.info.category === 'proteins') {
|
||||||
|
filtered = filtered
|
||||||
|
.flatMap(candidate => {
|
||||||
|
const p = candidate.info;
|
||||||
|
|
||||||
|
const maxGrams = Math.min(
|
||||||
|
p.calories > 0 ? (target.calories / p.calories) * 100 : Infinity,
|
||||||
|
p.protein > 0 ? (target.protein / p.protein) * 100 : Infinity,
|
||||||
|
);
|
||||||
|
|
||||||
|
return maxGrams > 0 ? [{ food: candidate, quantityGrams: Math.floor(maxGrams) }] : [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Consider calories and carbs
|
||||||
|
else if (current.info.category === 'carbs' || current.info.category === 'fruits' || current.info.category === 'low carb') {
|
||||||
|
filtered = filtered
|
||||||
|
.flatMap(candidate => {
|
||||||
|
const p = candidate.info;
|
||||||
|
|
||||||
|
const maxGrams = Math.min(
|
||||||
|
p.calories > 0 ? (target.calories / p.calories) * 100 : Infinity,
|
||||||
|
p.carbs > 0 ? (target.carbs / p.carbs) * 100 : Infinity,
|
||||||
|
);
|
||||||
|
|
||||||
|
return maxGrams > 0 ? [{ food: candidate, quantityGrams: Math.floor(maxGrams) }] : [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Consider only calories
|
||||||
|
else if (current.info.category === 'leaves') {
|
||||||
|
filtered = filtered
|
||||||
|
.flatMap(candidate => {
|
||||||
|
const p = candidate.info;
|
||||||
|
|
||||||
|
const maxGrams = Math.min(
|
||||||
|
p.calories > 0 ? (target.calories / p.calories) * 100 : Infinity,
|
||||||
|
);
|
||||||
|
|
||||||
|
return maxGrams > 0 ? [{ food: candidate, quantityGrams: Math.floor(maxGrams) }] : [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
.filter(f => f.quantityGrams > 0)
|
||||||
|
.sort((a, b) => a.food.name.localeCompare(b.food.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideAlternativesContainer() {
|
||||||
|
const container = document.getElementById('alternatives-container');
|
||||||
|
if (container) {
|
||||||
|
container.classList.add('display-none');
|
||||||
|
container.classList.remove('display-block');
|
||||||
|
}
|
||||||
|
const list = document.getElementById('alternatives-list');
|
||||||
|
if (list) {
|
||||||
|
list.innerHTML = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateAlternativesDisplay() {
|
||||||
|
const checkbox = document.getElementById('enableAlternatives') as HTMLInputElement;
|
||||||
|
if (!checkbox || !checkbox.checked) return;
|
||||||
|
|
||||||
|
const list = document.getElementById('alternatives-list');
|
||||||
|
if (!list) return;
|
||||||
|
|
||||||
|
if (!selectedFood) {
|
||||||
|
list.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gramsInput = getInputById('gramAmount');
|
||||||
|
const grams = parseFloat(gramsInput.value);
|
||||||
|
|
||||||
|
if (isNaN(grams) || grams <= 0) {
|
||||||
|
list.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const alternatives = findAlternatives(selectedFood, grams, foodDatabase);
|
||||||
|
|
||||||
|
const escapeHtml = (text: string): string =>
|
||||||
|
text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"').replace(/'/g, ''');
|
||||||
|
|
||||||
|
if (alternatives.length === 0) {
|
||||||
|
list.innerHTML = '<div class="no-alternatives">No equivalent alternatives found</div>';
|
||||||
|
} else {
|
||||||
|
list.innerHTML = alternatives.map(alt => `
|
||||||
|
<div class="alternative-item">
|
||||||
|
<span class="alternative-name">${escapeHtml(alt.food.name)}</span>
|
||||||
|
<span class="alternative-quantity">${alt.quantityGrams}g</span>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAlternativesCheckboxChange(e: Event) {
|
||||||
|
const checkbox = e.target as HTMLInputElement;
|
||||||
|
const container = document.getElementById('alternatives-container');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
if (checkbox.checked) {
|
||||||
|
container.classList.remove('display-none');
|
||||||
|
container.classList.add('display-block');
|
||||||
|
updateAlternativesDisplay();
|
||||||
|
} else {
|
||||||
|
hideAlternativesContainer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const previewCalories = (foodSelected?: FoodItem) => {
|
const previewCalories = (foodSelected?: FoodItem) => {
|
||||||
const foodToPreview = foodSelected ?? selectedFood;
|
const foodToPreview = foodSelected ?? selectedFood;
|
||||||
const gramAmount = getInputById('gramAmount');
|
const gramAmount = getInputById('gramAmount');
|
||||||
@@ -500,6 +657,7 @@ const previewCalories = (foodSelected?: FoodItem) => {
|
|||||||
getDivById('fiberValuePreview').textContent = (Math.round(proportion.info.fiber * 10) / 10).toString ();
|
getDivById('fiberValuePreview').textContent = (Math.round(proportion.info.fiber * 10) / 10).toString ();
|
||||||
|
|
||||||
showFoodPreview(true);
|
showFoodPreview(true);
|
||||||
|
updateAlternativesDisplay();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Goal management ----
|
// ---- Goal management ----
|
||||||
@@ -724,6 +882,10 @@ const setupEventListeners = () => {
|
|||||||
previewCalories();
|
previewCalories();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
getInputById('gramAmount').addEventListener('input', () => {
|
||||||
|
updateAlternativesDisplay();
|
||||||
|
});
|
||||||
|
|
||||||
getButtonById('add-food-btn').addEventListener('click', () => {
|
getButtonById('add-food-btn').addEventListener('click', () => {
|
||||||
if (getButtonById('add-food-btn').innerHTML === 'Add Food') {
|
if (getButtonById('add-food-btn').innerHTML === 'Add Food') {
|
||||||
addFood();
|
addFood();
|
||||||
@@ -797,6 +959,9 @@ const setupEventListeners = () => {
|
|||||||
// AI nutrition checkbox
|
// AI nutrition checkbox
|
||||||
document.getElementById('enableAINutrition')?.addEventListener('change', handleAICheckboxChange);
|
document.getElementById('enableAINutrition')?.addEventListener('change', handleAICheckboxChange);
|
||||||
|
|
||||||
|
// Alternatives checkbox
|
||||||
|
document.getElementById('enableAlternatives')?.addEventListener('change', handleAlternativesCheckboxChange);
|
||||||
|
|
||||||
// Share button event listeners
|
// Share button event listeners
|
||||||
getButtonById('shareBtn').addEventListener('click', handleShareClick);
|
getButtonById('shareBtn').addEventListener('click', handleShareClick);
|
||||||
getButtonById('shareBtnMobile').addEventListener('click', handleShareClick);
|
getButtonById('shareBtnMobile').addEventListener('click', handleShareClick);
|
||||||
|
|||||||
@@ -1198,3 +1198,57 @@ label {
|
|||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Alternatives Card */
|
||||||
|
#alternatives-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
animation: fadeIn 0.3s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alternatives-list-container {
|
||||||
|
background: rgba(40, 40, 40, 0.6);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(107, 141, 214, 0.3);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alternatives-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alternative-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alternative-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alternative-name {
|
||||||
|
color: #e0e0e0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alternative-quantity {
|
||||||
|
color: #6B8DD6;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-alternatives {
|
||||||
|
color: #b0b0b0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px 0;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user