Flatlogic Bot 87c4c5dcc3 goog modal
2026-02-01 19:28:07 +00:00

1191 lines
56 KiB
JavaScript

const app = {
dom: {},
state: {
recipes: [],
confirmedRecipeProducts: [],
checkedItems: [],
additionalProducts: [],
selectedRecipeIds: [],
user: null
},
api: {
async checkAuth() {
try {
const response = await fetch('api/check_auth.php');
const data = await response.json();
if (data.success && data.logged_in) {
app.state.user = data.user;
if (data.user.shopping_list) {
app.state.checkedItems = data.user.shopping_list.checkedItems || [];
app.state.additionalProducts = data.user.shopping_list.additionalProducts || [];
app.state.confirmedRecipeProducts = data.user.shopping_list.confirmedRecipeProducts || [];
app.state.selectedRecipeIds = data.user.shopping_list.selectedRecipeIds || [];
}
} else {
app.state.user = null;
}
} catch (error) {
console.error('Auth check failed:', error);
}
},
async saveShoppingList() {
if (!app.state.user) return;
try {
await fetch('api/save_shopping_list.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
shopping_list: {
checkedItems: app.state.checkedItems,
additionalProducts: app.state.additionalProducts,
confirmedRecipeProducts: app.state.confirmedRecipeProducts,
selectedRecipeIds: app.state.selectedRecipeIds
}
})
});
} catch (error) {
console.error('Failed to save shopping list:', error);
}
},
async login(email, password) {
try {
const response = await fetch('api/login.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const data = await response.json();
if (data.success) {
await app.api.checkAuth();
await app.api.getRecipes();
app.ui.updateAuthNav();
app.ui.renderRecipeCards(app.state.recipes);
app.ui.updateShoppingList();
return { success: true };
}
return data;
} catch (error) {
return { success: false, error: error.message };
}
},
async register(email, password, confirmPassword) {
try {
const response = await fetch('api/register.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password, confirm_password: confirmPassword })
});
const data = await response.json();
if (data.success) {
await app.api.checkAuth();
await app.api.getRecipes();
app.ui.updateAuthNav();
app.ui.renderRecipeCards(app.state.recipes);
app.ui.updateShoppingList();
return { success: true };
}
return data;
} catch (error) {
return { success: false, error: error.message };
}
},
async logout() {
try {
const response = await fetch('api/logout.php');
const data = await response.json();
if (data.success) {
app.state.user = null;
app.state.recipes = [];
app.state.checkedItems = [];
app.state.additionalProducts = [];
app.state.confirmedRecipeProducts = [];
await app.api.getRecipes();
app.ui.updateAuthNav();
app.ui.renderRecipeCards(app.state.recipes);
app.ui.updateShoppingList();
}
} catch (error) {
console.error('Logout failed:', error);
}
},
async getRecipes() {
try {
const response = await fetch('api/get_recipes.php');
const data = await response.json();
if (data.success) {
app.state.recipes = data.recipes;
} else {
console.error('Failed to load recipes:', data.error);
app.dom.recipeCardsContainer.innerHTML = '<div class="col-12"><p class="text-center text-danger">Error loading recipes.</p></div>';
}
} catch (error) {
console.error('Error:', error);
app.dom.recipeCardsContainer.innerHTML = '<div class="col-12"><p class="text-center text-danger">Failed to connect to the server.</p></div>';
}
},
async saveRecipe(formData) {
try {
const response = await fetch('api/save_recipe.php', {
method: 'POST',
body: formData
});
return await response.json();
} catch (error) {
alert('Error: ' + error.message);
return { success: false, error: error.message };
}
},
async deleteRecipe(id) {
try {
const response = await fetch('api/delete_recipe.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: id })
});
const data = await response.json();
if (data.success) {
// Remove from selectedRecipeIds if present
const index = app.state.selectedRecipeIds.indexOf(id.toString());
if (index > -1) {
app.state.selectedRecipeIds.splice(index, 1);
}
const indexNum = app.state.selectedRecipeIds.indexOf(Number(id));
if (indexNum > -1) {
app.state.selectedRecipeIds.splice(indexNum, 1);
}
await app.api.getRecipes();
app.ui.renderRecipeCards(app.state.recipes);
app.ui.updateShoppingList();
app.api.saveShoppingList();
} else {
alert('Failed to delete recipe: ' + data.error);
}
} catch (error) {
alert('Error: ' + error.message);
}
}
},
ui: {
renderRecipeCards(recipes) {
app.dom.recipeCardsContainer.innerHTML = '';
if (!recipes || recipes.length === 0) {
app.dom.recipeCardsContainer.innerHTML = '<div class="col-12"><p class="text-center text-muted">Your saved recipes will appear here.</p></div>';
return;
}
recipes.forEach((recipe, index) => {
const cardCol = document.createElement('div');
cardCol.className = 'col-md-6 mb-4 recipe-card-enter';
cardCol.setAttribute('data-id', recipe.id);
cardCol.style.animationDelay = `${index * 0.1}s`;
const isSelected = app.state.selectedRecipeIds.includes(recipe.id.toString()) || app.state.selectedRecipeIds.includes(Number(recipe.id));
const card = document.createElement('div');
card.className = `card h-100 recipe-selection-card ${isSelected ? 'selected' : ''}`;
if (recipe.image_url) {
const img = document.createElement('img');
img.src = recipe.image_url;
img.className = 'card-img-top';
img.alt = recipe.name;
card.appendChild(img);
}
const cardBody = document.createElement('div');
cardBody.className = 'card-body d-flex flex-column align-items-center';
const titleWrapper = document.createElement('div');
titleWrapper.className = 'd-flex flex-column align-items-center mb-2 gap-1 w-100';
const selectionWrapper = document.createElement('div');
selectionWrapper.className = 'form-check mb-0 d-flex align-items-center justify-content-center';
selectionWrapper.innerHTML = `
<input class="form-check-input select-recipe me-2" type="checkbox" value="${recipe.id}" id="select-recipe-${recipe.id}" ${isSelected ? 'checked' : ''}>
<label class="form-check-label fw-bold h5 mb-0" for="select-recipe-${recipe.id}">
${recipe.name}
</label>
`;
titleWrapper.appendChild(selectionWrapper);
if (recipe.category) {
const categoryLabel = document.createElement('div');
categoryLabel.className = 'recipe-category-label';
categoryLabel.textContent = recipe.category === 'Drinks' ? 'Drinks' :
recipe.category === 'Breakfast' ? 'Breakfast' :
recipe.category === 'Dinner' ? 'Lunch/Dinner' :
recipe.category === 'Appetizers' ? 'Appetizers' : recipe.category;
titleWrapper.appendChild(categoryLabel);
}
const text = document.createElement('p');
text.className = 'card-text text-muted mb-2 small';
text.textContent = `${recipe.ingredients.length} ingredients`;
const controlsWrapper = document.createElement('div');
controlsWrapper.className = 'recipe-controls mb-2 p-2 bg-light rounded';
controlsWrapper.innerHTML = `
<div class="row g-2 align-items-center">
<div class="col-6">
<label class="small text-muted mb-1 d-block">Guests</label>
<input type="number" class="form-control form-control-sm recipe-guests-input" value="${recipe.guests}" min="1">
</div>
<div class="col-6">
<label class="small text-muted mb-1 d-block">Portions</label>
<input type="number" class="form-control form-control-sm recipe-portions-input" value="1" min="1">
</div>
</div>
`;
const buttonGroup = document.createElement('div');
buttonGroup.className = 'mt-auto pt-2 d-flex gap-2 justify-content-center w-100';
buttonGroup.innerHTML = `
<button class="btn btn-light btn-sm view-recipe"><i class="bi bi-eye"></i> View</button>
<button class="btn btn-light btn-sm edit-recipe"><i class="bi bi-pencil"></i> Edit</button>
<button class="btn btn-outline-danger btn-sm delete-recipe" title="Delete"><i class="bi bi-trash"></i></button>
`;
cardBody.appendChild(titleWrapper);
cardBody.appendChild(text);
cardBody.appendChild(controlsWrapper);
cardBody.appendChild(buttonGroup);
card.appendChild(cardBody);
cardCol.appendChild(card);
app.dom.recipeCardsContainer.appendChild(cardCol);
});
},
updateShoppingList() {
const combinedIngredients = new Map();
// 1. Process recipe ingredients and calculate total based on per-recipe inputs
app.state.recipes.forEach(recipe => {
// Only include if selected
if (!app.state.selectedRecipeIds.includes(recipe.id.toString()) && !app.state.selectedRecipeIds.includes(Number(recipe.id))) {
return;
}
const card = app.dom.recipeCardsContainer.querySelector(`[data-id="${recipe.id}"]`);
if (!card) return;
const guestsInput = card.querySelector('.recipe-guests-input');
const portionsInput = card.querySelector('.recipe-portions-input');
const targetGuests = guestsInput ? parseInt(guestsInput.value, 10) : recipe.guests;
const targetPortions = portionsInput ? parseInt(portionsInput.value, 10) : 1;
if (isNaN(targetGuests) || targetGuests <= 0) return;
const baseGuests = parseInt(recipe.guests, 10) || 1;
if (recipe.ingredients) {
recipe.ingredients.forEach(ing => {
const name = ing.name.trim();
const unit = ing.unit.trim();
if (!name) return;
const key = `${name.toLowerCase()}|${unit.toLowerCase()}`;
if (!combinedIngredients.has(key)) {
combinedIngredients.set(key, {
name: name,
unit: unit,
recipeQty: 0,
additionalQty: 0,
sources: [],
category: ing.category
});
}
const item = combinedIngredients.get(key);
item.recipeQty += (ing.quantity || 0) * targetGuests * targetPortions;
if (!item.sources.includes(recipe.name)) {
item.sources.push(recipe.name);
}
});
}
});
// 2. Process additional products
if (app.state.additionalProducts) {
app.state.additionalProducts.forEach(prod => {
const name = prod.name.trim();
const unit = prod.unit.trim();
if (!name) return;
const key = `${name.toLowerCase()}|${unit.toLowerCase()}`;
if (!combinedIngredients.has(key)) {
combinedIngredients.set(key, {
name: name,
unit: unit,
recipeQty: 0,
additionalQty: 0,
sources: [],
category: prod.category
});
}
const item = combinedIngredients.get(key);
item.additionalQty += prod.quantity;
const source = prod.source || 'Add. product';
if (!item.sources.includes(source)) {
item.sources.push(source);
}
if (prod.category && !item.category) {
item.category = prod.category;
}
});
}
// 3. Group for display
const groups = {
'Food': { label: 'Food', ingredients: [] },
'Drinks': { label: 'Drinks', ingredients: [] },
'Cooking and serving': { label: 'Kitchen & serving', ingredients: [] },
'Tableware and consumables': { label: 'Tableware & consumables', ingredients: [] }
};
combinedIngredients.forEach((item, key) => {
let groupName = 'Food'; // Default to Food
if (item.category) {
const normalizedCategory = item.category.toLowerCase();
if (groups.hasOwnProperty(item.category)) {
groupName = item.category;
} else if (normalizedCategory === 'drinks' || normalizedCategory === 'drink') {
groupName = 'Drinks';
}
}
groups[groupName].ingredients.push(item);
});
// 4. Render HTML
let html = '';
let totalIngredients = 0;
for (const groupName in groups) {
const group = groups[groupName];
if (group.ingredients.length > 0) {
totalIngredients += group.ingredients.length;
html += `<h4 class="mt-3">${group.label}</h4>`;
html += '<ul class="list-group list-group-flush">';
group.ingredients.forEach((item, index) => {
const totalQty = item.recipeQty + item.additionalQty;
if (totalQty <= 0) return;
const quantityStr = Number.isInteger(totalQty) ? totalQty : parseFloat(totalQty.toFixed(2));
const uniqueId = `shopping-item-${groupName.replace(/\s/g, '-')}-${index}`;
const tooltipText = item.sources.join(', ');
const itemKey = `${item.name.toLowerCase()}|${item.unit.toLowerCase()}`;
const isChecked = app.state.checkedItems.includes(itemKey);
html += `<li class="list-group-item d-flex justify-content-between align-items-center ${isChecked ? 'checked' : ''}">
<div class="form-check d-flex align-items-center">
<input class="form-check-input" type="checkbox" id="${uniqueId}" data-key="${itemKey}" ${isChecked ? 'checked' : ''}>
<label class="form-check-label ms-2" for="${uniqueId}">
${item.name}
</label>
<i class="bi bi-info-circle ms-2" data-bs-toggle="tooltip" data-bs-placement="top" title="${tooltipText}"></i>
</div>`;
html += `<div>
<button class="btn btn-quantity-modifier btn-quantity-minus decrement-item" data-key="${itemKey}">-</button>
<span class="badge bg-custom-green rounded-pill mx-2">${quantityStr} ${item.unit}</span>
<button class="btn btn-quantity-modifier btn-quantity-plus increment-item" data-key="${itemKey}">+</button>
</div>`;
html += `</li>`;
});
html += '</ul>';
}
}
if (totalIngredients === 0) {
html += '<div class="text-center text-muted p-4"><p>Your shopping list is empty. Add a recipe, and its ingredients will appear here.</p></div>';
}
app.dom.shoppingListContainer.innerHTML = html;
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
});
},
addIngredientRow(ingredient = { name: '', quantity: '', unit: 'g' }) {
const row = document.createElement('div');
row.className = 'ingredient-row mb-3';
const units = ['g', 'kg', 'ml', 'l', 'pcs', 'pack'];
const unitButtons = units.map(u =>
`<button type="button" class="btn ${ingredient.unit === u ? 'btn-secondary' : 'btn-outline-secondary'} unit-btn">${u}</button>`
).join('');
row.innerHTML = `
<div class="mb-2">
<input type="text" class="form-control" placeholder="Ingredient name" aria-label="Ingredient Name" value="${ingredient.name}">
</div>
<div class="d-flex align-items-center">
<input type="number" class="form-control me-2" placeholder="Qty" aria-label="Quantity" min="0" step="any" value="${ingredient.quantity}" style="width: 100px;">
<div class="btn-group unit-selector me-2" role="group" aria-label="Unit selector">
${unitButtons}
</div>
<button type="button" class="btn btn-danger btn-sm remove-ingredient ms-auto">&times;</button>
</div>
`;
app.dom.ingredientsContainer.appendChild(row);
},
clearForm() {
app.dom.recipeIdInput.value = '';
app.dom.recipeNameInput.value = '';
app.dom.recipeInstructionsInput.value = '';
app.dom.recipeCategoryInput.value = '';
app.dom.recipeImage.value = '';
app.dom.guestCountInput.value = '1';
app.dom.ingredientsContainer.innerHTML = '';
app.ui.addIngredientRow();
app.dom.newRecipeBtn.textContent = 'Save recipe';
app.dom.cancelEditBtn.style.display = 'none';
document.getElementById('recipe-form-modal-label').textContent = 'Add recipe';
},
populateFormForEdit(recipeId) {
const recipe = app.state.recipes.find(r => r.id == recipeId);
if (!recipe) return;
app.dom.recipeIdInput.value = recipe.id;
app.dom.recipeNameInput.value = recipe.name;
app.dom.recipeInstructionsInput.value = recipe.instructions || '';
app.dom.recipeCategoryInput.value = recipe.category || '';
app.dom.guestCountInput.value = recipe.guests;
app.dom.ingredientsContainer.innerHTML = '';
if (recipe.ingredients) {
recipe.ingredients.forEach(ing => app.ui.addIngredientRow(ing));
} else {
app.ui.addIngredientRow();
}
app.dom.newRecipeBtn.textContent = 'Update recipe';
app.dom.cancelEditBtn.style.display = 'block';
document.getElementById('recipe-form-modal-label').textContent = 'Edit recipe';
app.dom.recipeFormModal.show();
app.dom.recipeNameInput.focus();
},
populateViewModal(recipeId) {
const recipe = app.state.recipes.find(r => r.id == recipeId);
if (!recipe) return;
const modal = document.getElementById('view-recipe-modal');
const img = modal.querySelector('img');
if (img) {
img.remove();
}
if (recipe.image_url) {
const newImg = document.createElement('img');
newImg.src = recipe.image_url;
newImg.className = 'card-img-top mb-3';
newImg.alt = recipe.name;
modal.querySelector('.modal-body').prepend(newImg);
}
document.getElementById('view-recipe-name').textContent = recipe.name;
document.getElementById('view-recipe-category').textContent = recipe.category === 'Drinks' ? 'Drinks' :
recipe.category === 'Breakfast' ? 'Breakfast' :
recipe.category === 'Dinner' ? 'Lunch/Dinner' :
recipe.category === 'Appetizers' ? 'Appetizers' : 'No category';
document.getElementById('view-recipe-guests').textContent = recipe.guests;
document.getElementById('view-recipe-instructions').textContent = recipe.instructions || 'No instructions provided.';
const ingredientsList = document.getElementById('view-recipe-ingredients');
ingredientsList.innerHTML = '';
if (recipe.ingredients) {
recipe.ingredients.forEach(ing => {
const li = document.createElement('li');
li.className = 'list-group-item';
li.textContent = `${ing.name} - ${ing.quantity} ${ing.unit}`;
ingredientsList.appendChild(li);
});
}
app.dom.viewRecipeModal.show();
},
getRecipeDataFromForm() {
const recipeName = app.dom.recipeNameInput.value.trim();
const instructions = app.dom.recipeInstructionsInput.value.trim();
const guests = parseInt(app.dom.guestCountInput.value, 10) || 0;
const category = app.dom.recipeCategoryInput.value;
const ingredients = [];
const rows = app.dom.ingredientsContainer.querySelectorAll('.ingredient-row');
rows.forEach(row => {
const nameInput = row.querySelector('input[placeholder="Ingredient name"]');
const name = nameInput ? nameInput.value.trim() : '';
const qtyInput = row.querySelector('input[placeholder="Qty"]');
const qty = qtyInput ? parseFloat(qtyInput.value) : NaN;
const activeButton = row.querySelector('.unit-selector .btn-secondary');
const unit = activeButton ? activeButton.textContent.trim() : 'g';
if (name && !isNaN(qty) && qty > 0) {
ingredients.push({ name, quantity: qty, unit });
}
});
if (recipeName && guests > 0 && ingredients.length > 0) {
return { name: recipeName, instructions, guests, ingredients, category };
}
return null;
},
loadCheckedItems() {
const stored = localStorage.getItem('checkedItems');
if (stored) {
app.state.checkedItems = JSON.parse(stored);
}
},
saveCheckedItems() {
localStorage.setItem('checkedItems', JSON.stringify(app.state.checkedItems));
app.api.saveShoppingList();
},
updateAuthNav() {
const nav = document.getElementById('auth-nav');
const guestView = document.getElementById('guest-view');
const appView = document.getElementById('app-view');
if (!nav) return;
if (app.state.user) {
nav.innerHTML = `
<li class="nav-item me-3 d-none d-lg-block">
<span class="text-muted">Welcome, ${app.state.user.email}</span>
</li>
<li class="nav-item">
<button class="btn btn-outline-primary btn-sm" id="logout-btn">Log Out</button>
</li>
`;
if (guestView) guestView.classList.add('d-none');
if (appView) appView.classList.remove('d-none');
} else {
nav.innerHTML = ''; // Landing page handles login/register
if (guestView) guestView.classList.remove('d-none');
if (appView) appView.classList.add('d-none');
}
}
},
events: {
attachEventListeners() {
// Auth events (Landing Page)
const loginFormLanding = document.getElementById('login-form-landing');
if (loginFormLanding) {
loginFormLanding.addEventListener('submit', async (e) => {
e.preventDefault();
const email = loginFormLanding.querySelector('[name="email"]').value;
const password = loginFormLanding.querySelector('[name="password"]').value;
const result = await app.api.login(email, password);
if (result.success) {
loginFormLanding.reset();
} else {
alert(result.error);
}
});
}
const registerFormLanding = document.getElementById('register-form-landing');
if (registerFormLanding) {
registerFormLanding.addEventListener('submit', async (e) => {
e.preventDefault();
const email = registerFormLanding.querySelector('[name="email"]').value;
const password = registerFormLanding.querySelector('[name="password"]').value;
const confirmPassword = registerFormLanding.querySelector('[name="confirm_password"]').value;
if (password !== confirmPassword) {
alert('Passwords do not match.');
return;
}
const result = await app.api.register(email, password, confirmPassword);
if (result.success) {
registerFormLanding.reset();
} else {
alert(result.error);
}
});
}
// Toggle login/register on landing
const showRegister = document.getElementById('show-register');
const showLogin = document.getElementById('show-login');
const loginContainer = document.getElementById('login-container');
const registerContainer = document.getElementById('register-container');
if (showRegister && showLogin) {
showRegister.addEventListener('click', (e) => {
e.preventDefault();
loginContainer.classList.add('d-none');
registerContainer.classList.remove('d-none');
});
showLogin.addEventListener('click', (e) => {
e.preventDefault();
registerContainer.classList.add('d-none');
loginContainer.classList.remove('d-none');
});
}
document.getElementById('auth-nav').addEventListener('click', (e) => {
if (e.target.id === 'logout-btn') {
app.api.logout();
}
});
document.getElementById('add-recipe-options-modal').addEventListener('click', (e) => {
const btn = e.target.closest('.add-option-btn');
if (!btn) return;
const method = btn.dataset.method;
app.dom.addRecipeOptionsModal.hide();
if (method === 'scratch') {
app.ui.clearForm();
app.dom.recipeFormModal.show();
} else if (method === 'photo') {
app.dom.addRecipePhotoModal.show();
} else if (method === 'link') {
app.dom.addRecipeLinkModal.show();
} else if (method === 'ingredients') {
app.dom.addRecipeIngredientsModal.show();
}
});
// Specific Modal Handlers
document.getElementById('take-photo-btn').addEventListener('click', () => {
document.getElementById('photo-camera-input').click();
});
document.getElementById('upload-image-btn').addEventListener('click', () => {
document.getElementById('photo-upload-input').click();
});
const handlePhotoInput = async (e) => {
const file = e.target.files[0];
if (!file) return;
app.dom.addRecipePhotoModal.hide();
app.ui.clearForm();
app.dom.recipeFormModal.show();
// Trigger AI Scan logic
app.dom.aiScanBtn.disabled = true;
app.dom.aiScanLoading.classList.remove('d-none');
const formData = new FormData();
formData.append('image', file);
try {
const response = await fetch('api/scan_recipe.php', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
const data = result.data;
if (data.name) app.dom.recipeNameInput.value = data.name;
if (data.instructions) app.dom.recipeInstructionsInput.value = data.instructions;
if (data.category) app.dom.recipeCategoryInput.value = data.category;
if (data.guests) app.dom.guestCountInput.value = data.guests;
if (data.ingredients && Array.isArray(data.ingredients)) {
app.dom.ingredientsContainer.innerHTML = '';
data.ingredients.forEach(ing => {
app.ui.addIngredientRow({
name: ing.name || '',
quantity: ing.quantity || '',
unit: ing.unit || 'g'
});
});
}
} else {
alert('AI scan error: ' + result.error);
}
} catch (error) {
console.error('Error during AI scan:', error);
} finally {
app.dom.aiScanBtn.disabled = false;
app.dom.aiScanLoading.classList.add('d-none');
}
};
document.getElementById('photo-camera-input').addEventListener('change', handlePhotoInput);
document.getElementById('photo-upload-input').addEventListener('change', handlePhotoInput);
document.getElementById('save-from-link-confirm').addEventListener('click', () => {
const url = document.getElementById('link-recipe-url').value;
if (!url) { alert('Please enter a link'); return; }
alert('Extracting recipe from: ' + url + '... (Integration in progress)');
app.dom.addRecipeLinkModal.hide();
});
document.getElementById('take-products-photo-btn').addEventListener('click', () => {
document.getElementById('products-camera-input').click();
});
document.getElementById('products-camera-input').addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
alert('AI is analyzing your products... (Integration in progress)');
app.dom.addRecipeIngredientsModal.hide();
});
document.getElementById('generate-from-ingredients-btn').addEventListener('click', () => {
const list = document.getElementById('manual-ingredients-list').value;
if (!list) { alert('Please enter some ingredients'); return; }
alert('Generating recipe from: ' + list + '... (Integration in progress)');
app.dom.addRecipeIngredientsModal.hide();
});
app.dom.addIngredientBtn.addEventListener('click', () => app.ui.addIngredientRow());
app.dom.aiScanBtn.addEventListener('click', async function() {
const file = app.dom.recipeImage.files[0];
if (!file) {
alert('Please select an image first.');
return;
}
app.dom.aiScanBtn.disabled = true;
app.dom.aiScanLoading.classList.remove('d-none');
const formData = new FormData();
formData.append('image', file);
try {
const response = await fetch('api/scan_recipe.php', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
const data = result.data;
if (data.name) app.dom.recipeNameInput.value = data.name;
if (data.instructions) app.dom.recipeInstructionsInput.value = data.instructions;
if (data.category) app.dom.recipeCategoryInput.value = data.category;
if (data.guests) app.dom.guestCountInput.value = data.guests;
if (data.ingredients && Array.isArray(data.ingredients)) {
app.dom.ingredientsContainer.innerHTML = '';
data.ingredients.forEach(ing => {
app.ui.addIngredientRow({
name: ing.name || '',
quantity: ing.quantity || '',
unit: ing.unit || 'g'
});
});
}
} else {
alert('AI scan error: ' + result.error);
}
} catch (error) {
console.error('Error during AI scan:', error);
alert('An error occurred during AI scan.');
} finally {
app.dom.aiScanBtn.disabled = false;
app.dom.aiScanLoading.classList.add('d-none');
}
});
app.dom.ingredientsContainer.addEventListener('click', function(e) {
if (e.target.classList.contains('remove-ingredient')) {
e.target.closest('.ingredient-row').remove();
} else if (e.target.classList.contains('unit-btn')) {
const group = e.target.closest('.unit-selector');
if (group) {
group.querySelectorAll('.unit-btn').forEach(btn => {
btn.classList.remove('btn-secondary');
btn.classList.add('btn-outline-secondary');
});
e.target.classList.remove('btn-outline-secondary');
e.target.classList.add('btn-secondary');
}
}
});
app.dom.newRecipeBtn.addEventListener('click', async function() {
const recipeData = app.ui.getRecipeDataFromForm();
if (!recipeData) {
alert('Please fill in recipe name, category, guest count, and at least one ingredient before saving.');
return;
}
const formData = new FormData();
formData.append('name', recipeData.name);
formData.append('instructions', recipeData.instructions);
formData.append('guests', recipeData.guests);
formData.append('category', recipeData.category);
formData.append('ingredients', JSON.stringify(recipeData.ingredients));
const recipeId = app.dom.recipeIdInput.value;
if (recipeId) {
formData.append('id', recipeId);
}
const imageInput = document.getElementById('recipeImage');
if (imageInput.files[0]) {
formData.append('image', imageInput.files[0]);
}
const data = await app.api.saveRecipe(formData);
if (data.success && data.recipe) {
app.api.getRecipes().then(() => {
const currentCategory = app.dom.categoryFilters.querySelector('.active').dataset.category;
if (currentCategory === 'all') {
app.ui.renderRecipeCards(app.state.recipes);
} else {
const filteredRecipes = app.state.recipes.filter(recipe => recipe.category === currentCategory);
app.ui.renderRecipeCards(filteredRecipes);
}
app.ui.updateShoppingList();
app.ui.clearForm();
app.dom.recipeFormModal.hide();
});
} else {
alert('Failed to save recipe: ' + data.error);
}
});
app.dom.recipeCardsContainer.addEventListener('click', function(e) {
const target = e.target.closest('button');
const checkbox = e.target.closest('.select-recipe');
const cardCol = e.target.closest('[data-id]');
if (!cardCol) return;
const recipeId = cardCol.getAttribute('data-id');
if (checkbox) {
const card = cardCol.querySelector('.card');
if (checkbox.checked) {
if (!app.state.selectedRecipeIds.includes(recipeId)) {
app.state.selectedRecipeIds.push(recipeId);
}
card.classList.add('selected');
} else {
const index = app.state.selectedRecipeIds.indexOf(recipeId);
if (index > -1) {
app.state.selectedRecipeIds.splice(index, 1);
}
const indexNum = app.state.selectedRecipeIds.indexOf(Number(recipeId));
if (indexNum > -1) {
app.state.selectedRecipeIds.splice(indexNum, 1);
}
card.classList.remove('selected');
}
app.ui.updateShoppingList();
app.api.saveShoppingList();
return; // Don't trigger other actions
}
if (target && target.classList.contains('delete-recipe')) {
if (confirm('Are you sure you want to delete this recipe?')) {
app.api.deleteRecipe(recipeId);
}
}
if (target && target.classList.contains('edit-recipe')) {
app.ui.populateFormForEdit(recipeId);
}
if (target && target.classList.contains('view-recipe')) {
app.ui.populateViewModal(recipeId);
}
});
app.dom.recipeCardsContainer.addEventListener('input', function(e) {
if (e.target.classList.contains('recipe-guests-input') || e.target.classList.contains('recipe-portions-input')) {
app.ui.updateShoppingList();
}
});
app.dom.shoppingListContainer.addEventListener('click', function(e) {
if (e.target.matches('.form-check-input')) {
const listItem = e.target.closest('.list-group-item');
if (listItem) {
listItem.classList.toggle('checked', e.target.checked);
const itemKey = e.target.dataset.key;
if (e.target.checked) {
if (!app.state.checkedItems.includes(itemKey)) {
app.state.checkedItems.push(itemKey);
}
} else {
const index = app.state.checkedItems.indexOf(itemKey);
if (index > -1) {
app.state.checkedItems.splice(index, 1);
}
}
app.ui.saveCheckedItems();
}
} else if (e.target.matches('.increment-item')) {
const key = e.target.dataset.key;
const [name, unit] = key.split('|');
if (!app.state.additionalProducts) {
app.state.additionalProducts = [];
}
let productToModify = app.state.additionalProducts.find(p => p.name.toLowerCase() === name.toLowerCase() && p.unit.toLowerCase() === unit.toLowerCase());
if (!productToModify) {
const properName = name.charAt(0).toUpperCase() + name.slice(1);
productToModify = {
name: properName,
quantity: 0,
unit: unit,
source: 'Add. product'
};
app.state.additionalProducts.push(productToModify);
}
productToModify.quantity++;
app.ui.updateShoppingList();
app.api.saveShoppingList();
} else if (e.target.matches('.decrement-item')) {
const key = e.target.dataset.key;
const [name, unit] = key.split('|');
// Find or create the item in additionalProducts to track adjustments
if (!app.state.additionalProducts) {
app.state.additionalProducts = [];
}
let productToModify = app.state.additionalProducts.find(p => p.name.toLowerCase() === name.toLowerCase() && p.unit.toLowerCase() === unit.toLowerCase());
if (!productToModify) {
const properName = name.charAt(0).toUpperCase() + name.slice(1);
productToModify = {
name: properName,
quantity: 0,
unit: unit,
source: 'Add. product'
};
app.state.additionalProducts.push(productToModify);
}
// Check if this ingredient is part of any recipe
let isRecipeIngredient = false;
let recipeNameForModal = '';
for (const recipe of app.state.recipes) {
if (recipe.ingredients.some(ing => ing.name.toLowerCase() === name.toLowerCase() && ing.unit.toLowerCase() === unit.toLowerCase())) {
isRecipeIngredient = true;
recipeNameForModal = recipe.name; // Just need one name for the modal
break;
}
}
// If it's a recipe ingredient and we're about to remove from the recipe's contribution
if (isRecipeIngredient && productToModify.quantity <= 0) {
const confirmationKey = `${name.toLowerCase()}-${unit.toLowerCase()}`;
if (app.state.confirmedRecipeProducts.includes(confirmationKey)) {
productToModify.quantity--;
app.ui.updateShoppingList();
app.api.saveShoppingList();
} else {
document.getElementById('modal-recipe-name').textContent = recipeNameForModal;
document.getElementById('modal-ingredient-name').textContent = productToModify.name;
const confirmModal = new bootstrap.Modal(document.getElementById('confirmRemoveModal'));
confirmModal.show();
document.getElementById('confirm-remove-btn').onclick = () => {
app.state.confirmedRecipeProducts.push(confirmationKey);
productToModify.quantity--;
app.ui.updateShoppingList();
app.api.saveShoppingList();
confirmModal.hide();
};
}
} else if (productToModify.quantity > 0) {
// It's not a recipe ingredient about to be removed, but has been added via '+'
productToModify.quantity--;
app.ui.updateShoppingList();
app.api.saveShoppingList();
}
// If not a recipe ingredient and quantity is 0, do nothing.
}
});
app.dom.cancelEditBtn.addEventListener('click', function() {
app.ui.clearForm();
app.dom.recipeFormModal.hide();
});
app.dom.recipeSearchInput.addEventListener('input', function() {
const searchTerm = app.dom.recipeSearchInput.value.toLowerCase();
const filteredRecipes = app.state.recipes.filter(recipe => {
const recipeName = recipe.name.toLowerCase();
const ingredients = recipe.ingredients.map(ing => ing.name.toLowerCase()).join(' ');
return recipeName.includes(searchTerm) || ingredients.includes(searchTerm);
});
app.ui.renderRecipeCards(filteredRecipes);
});
app.dom.categoryFilters.addEventListener('click', function(e) {
if (e.target.tagName === 'BUTTON') {
const category = e.target.dataset.category;
app.dom.categoryFilters.querySelectorAll('button').forEach(btn => {
btn.classList.remove('active', 'btn-secondary');
btn.classList.add('btn-outline-secondary');
});
e.target.classList.add('active', 'btn-secondary');
e.target.classList.remove('btn-outline-secondary');
if (category === 'all') {
app.ui.renderRecipeCards(app.state.recipes);
} else {
const filteredRecipes = app.state.recipes.filter(recipe => recipe.category === category);
app.ui.renderRecipeCards(filteredRecipes);
}
}
});
app.dom.printShoppingListBtn.addEventListener('click', function() {
window.print();
});
app.dom.addProductModal._element.addEventListener('click', function(e) {
if (e.target.classList.contains('unit-btn')) {
const group = e.target.closest('.unit-selector');
if (group) {
group.querySelectorAll('.unit-btn').forEach(btn => {
btn.classList.remove('btn-secondary');
btn.classList.add('btn-outline-secondary');
});
e.target.classList.remove('btn-outline-secondary');
e.target.classList.add('btn-secondary');
}
}
});
app.dom.addProductForm.addEventListener('submit', (e) => {
e.preventDefault();
const name = app.dom.productNameInput.value.trim();
const quantity = parseFloat(app.dom.productQuantityInput.value);
const unitButton = app.dom.addProductModal._element.querySelector('.unit-selector .btn-secondary');
const unit = unitButton ? unitButton.textContent.trim() : 'g';
const category = app.dom.productCategory.value;
if (!name || isNaN(quantity) || quantity <= 0 || !unit) {
alert('Please fill in all fields with valid values.');
return;
}
if (!app.state.additionalProducts) {
app.state.additionalProducts = [];
}
const key = `${name.toLowerCase()}|${unit.toLowerCase()}`;
const existingProduct = app.state.additionalProducts.find(p => `${p.name.toLowerCase()}|${p.unit.toLowerCase()}` === key);
if (existingProduct) {
existingProduct.quantity += quantity;
} else {
const newProduct = { name, quantity, unit, source: 'Add. product', category: category };
app.state.additionalProducts.push(newProduct);
}
app.ui.updateShoppingList();
app.api.saveShoppingList();
// Reset form
app.dom.productNameInput.value = '';
app.dom.productQuantityInput.value = '1';
app.dom.productCategory.value = 'Food';
const unitButtons = app.dom.addProductModal._element.querySelectorAll('.unit-selector .unit-btn');
unitButtons.forEach((btn, index) => {
if (index === 0) {
btn.classList.add('btn-secondary');
btn.classList.remove('btn-outline-secondary');
} else {
btn.classList.remove('btn-secondary');
btn.classList.add('btn-outline-secondary');
}
});
app.dom.addProductModal.hide();
});
document.getElementById('recipe-form-modal').addEventListener('show.bs.modal', function () {
if (!app.dom.recipeIdInput.value) {
app.ui.clearForm();
}
});
}
},
init() {
app.dom = {
recipeNameInput: document.getElementById('recipeName'),
recipeInstructionsInput: document.getElementById('recipeInstructions'),
guestCountInput: document.getElementById('guestCount'),
portionsPerGuestInput: document.getElementById('portionsPerGuest'),
ingredientsContainer: document.getElementById('ingredients-container'),
addIngredientBtn: document.getElementById('add-ingredient'),
newRecipeBtn: document.getElementById('new-recipe-btn'),
cancelEditBtn: document.getElementById('cancel-edit-btn'),
shoppingListContainer: document.getElementById('shopping-list-container'),
recipeCardsContainer: document.getElementById('recipe-cards-container'),
recipeIdInput: document.getElementById('recipeId'),
recipeCategoryInput: document.getElementById('recipeCategory'),
recipeImage: document.getElementById('recipeImage'),
aiScanBtn: document.getElementById('ai-scan-btn'),
aiScanLoading: document.getElementById('ai-scan-loading'),
recipeSearchInput: document.getElementById('recipe-search'),
categoryFilters: document.getElementById('category-filters'),
addProductBtn: document.getElementById('add-product-btn'),
printShoppingListBtn: document.getElementById('print-shopping-list-btn'),
recipeFormModal: new bootstrap.Modal(document.getElementById('recipe-form-modal')),
viewRecipeModal: new bootstrap.Modal(document.getElementById('view-recipe-modal')),
addProductModal: new bootstrap.Modal(document.getElementById('add-product-modal')),
addProductForm: document.getElementById('add-product-form'),
productNameInput: document.getElementById('productName'),
productQuantityInput: document.getElementById('productQuantity'),
productCategoryWrapper: document.getElementById('product-category-wrapper'),
productCategory: document.getElementById('productCategory'),
addRecipeOptionsModal: new bootstrap.Modal(document.getElementById('add-recipe-options-modal')),
addRecipePhotoModal: new bootstrap.Modal(document.getElementById('add-recipe-photo-modal')),
addRecipeLinkModal: new bootstrap.Modal(document.getElementById('add-recipe-link-modal')),
addRecipeIngredientsModal: new bootstrap.Modal(document.getElementById('add-recipe-ingredients-modal')),
};
app.ui.loadCheckedItems();
app.events.attachEventListeners();
app.dom.cancelEditBtn.style.display = 'none';
app.ui.addIngredientRow();
app.api.checkAuth().then(() => {
app.ui.updateAuthNav();
app.api.getRecipes().then(() => {
app.ui.renderRecipeCards(app.state.recipes);
app.ui.updateShoppingList();
});
});
}
};
document.addEventListener('DOMContentLoaded', app.init);