This commit is contained in:
Flatlogic Bot 2025-11-12 19:46:05 +00:00
parent 24d0513664
commit 32cc8d6cd2
6 changed files with 690 additions and 445 deletions

View File

@ -14,6 +14,23 @@ $pdo = db();
try {
$pdo->beginTransaction();
if (isset($data['id']) && !empty($data['id'])) {
// Update existing recipe
$recipeId = $data['id'];
$stmt = $pdo->prepare("UPDATE recipes SET name = ?, guests = ? WHERE id = ?");
$stmt->execute([$data['name'], $data['guests'], $recipeId]);
// Easiest way to handle ingredients is to delete old ones and insert new ones
$stmt = $pdo->prepare("DELETE FROM ingredients WHERE recipe_id = ?");
$stmt->execute([$recipeId]);
$stmt = $pdo->prepare("INSERT INTO ingredients (recipe_id, name, quantity, unit) VALUES (?, ?, ?, ?)");
foreach ($data['ingredients'] as $ing) {
$stmt->execute([$recipeId, $ing['name'], $ing['quantity'], $ing['unit']]);
}
} else {
// Insert new recipe
$stmt = $pdo->prepare("INSERT INTO recipes (name, guests) VALUES (?, ?)");
$stmt->execute([$data['name'], $data['guests']]);
$recipeId = $pdo->lastInsertId();
@ -22,6 +39,7 @@ try {
foreach ($data['ingredients'] as $ing) {
$stmt->execute([$recipeId, $ing['name'], $ing['quantity'], $ing['unit']]);
}
}
$pdo->commit();

View File

@ -1,6 +1,6 @@
/* General Body Styles */
body {
background-color: #0a2e36; /* Dark teal background */
background-color: #013617; /* Dark green background */
color: #ffffff; /* White text */
font-family: 'Poppins', sans-serif;
padding-top: 40px; /* Make space for garland */
@ -11,67 +11,13 @@ h1, h2, h3, h4, h5, h6 {
color: #ffffff !important; /* Use !important to override other styles if necessary */
}
/* Christmas Tree */
.christmas-tree-container {
position: fixed;
bottom: 0;
width: 100px;
height: 150px;
z-index: 100;
/* Buttons and Inputs */
input,
button,
.btn {
border-radius: 8px !important;
}
.christmas-tree-container.left {
left: 20px;
}
.christmas-tree-container.right {
right: 20px;
}
.tree {
position: relative;
width: 100%;
height: 100%;
}
.tree::before { /* The tree itself */
content: '';
position: absolute;
bottom: 30px; /* Height of the trunk */
left: 0;
width: 0;
height: 0;
border-left: 50px solid transparent;
border-right: 50px solid transparent;
border-bottom: 120px solid #2C5F2D; /* Dark green */
}
.tree::after { /* The trunk */
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 20px;
height: 30px;
background: #5C3D2E; /* Brown */
}
.ornament {
position: absolute;
width: 10px;
height: 10px;
border-radius: 50%;
background: #ff6f61; /* Coral red */
}
.ornament.o1 { top: 50px; left: 45px; }
.ornament.o2 { top: 70px; left: 30px; background: #ffff24; }
.ornament.o3 { top: 75px; left: 60px; background: #2424ff; }
.ornament.o4 { top: 95px; left: 40px; }
.ornament.o5 { top: 100px; left: 15px; background: #24ff24;}
.ornament.o6 { top: 105px; left: 70px; background: #ff24ff;}
/* Garland */
body::before {
@ -82,9 +28,9 @@ body::before {
width: 110%;
height: 20px;
background:
radial-gradient(circle, #ff2424 4px, transparent 5px),
radial-gradient(circle, #24ff24 4px, transparent 5px),
radial-gradient(circle, #2424ff 4px, transparent 5px),
radial-gradient(circle, #FF3E1F 4px, transparent 5px),
radial-gradient(circle, #013617 4px, transparent 5px),
radial-gradient(circle, #FFAFCA 4px, transparent 5px),
radial-gradient(circle, #ffff24 4px, transparent 5px),
radial-gradient(circle, #ff24ff 4px, transparent 5px);
background-size: 100px 20px;
@ -103,7 +49,7 @@ body::before {
/* Navbar */
.navbar {
background-color: rgba(10, 46, 54, 0.8) !important; /* Semi-transparent dark teal */
background-color: rgba(1, 54, 23, 0.8) !important; /* Semi-transparent dark green */
backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
@ -143,13 +89,12 @@ body::before {
background-color: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #ffffff;
border-radius: 8px;
}
.form-control:focus {
background-color: rgba(0, 0, 0, 0.3);
border-color: #ff6f61; /* Coral red accent */
box-shadow: 0 0 0 0.25rem rgba(255, 111, 97, 0.25);
border-color: #FF3E1F; /* Coral red accent */
box-shadow: 0 0 0 0.25rem rgba(255, 62, 31, 0.25);
color: #ffffff;
}
@ -159,19 +104,18 @@ body::before {
/* Buttons */
.btn-primary {
background-color: #ff6f61; /* Coral red */
border-color: #ff6f61;
background-color: #FF3E1F; /* Coral red */
border-color: #FF3E1F;
font-weight: 600;
padding: 12px 30px;
border-radius: 50px;
transition: all 0.3s ease;
}
.btn-primary:hover {
background-color: #e65a50;
border-color: #e65a50;
background-color: #E6381A;
border-color: #E6381A;
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(255, 111, 97, 0.2);
box-shadow: 0 4px 15px rgba(255, 62, 31, 0.2);
}
.btn-outline-secondary {
@ -179,7 +123,6 @@ body::before {
color: #ffffff;
font-weight: 600;
padding: 12px 30px;
border-radius: 50px;
transition: all 0.3s ease;
}
@ -208,6 +151,36 @@ body::before {
margin-bottom: 15px;
}
/* 3-Column Layout Adjustments */
.row.g-4 > [class*='col-'] .card {
height: 100%; /* Make cards in columns equal height */
}
#recipe-cards-container, #shopping-list-container {
max-height: 60vh; /* Adjust as needed */
overflow-y: auto;
padding: 10px;
}
/* Custom scrollbar for webkit browsers */
#recipe-cards-container::-webkit-scrollbar, #shopping-list-container::-webkit-scrollbar {
width: 8px;
}
#recipe-cards-container::-webkit-scrollbar-track, #shopping-list-container::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.2);
border-radius: 4px;
}
#recipe-cards-container::-webkit-scrollbar-thumb, #shopping-list-container::-webkit-scrollbar-thumb {
background: rgba(255, 62, 31, 0.5); /* Coral red, semi-transparent */
border-radius: 4px;
}
#recipe-cards-container::-webkit-scrollbar-thumb:hover, #shopping-list-container::-webkit-scrollbar-thumb:hover {
background: #FF3E1F; /* Coral red */
}
/* Footer */
footer.bg-light {
background-color: transparent !important;
@ -234,7 +207,7 @@ footer.bg-light {
border-radius: 50%;
opacity: 0.8;
pointer-events: none;
animation: fall linear infinite;
animation: fall linear infinite;
}
@keyframes fall {
@ -243,3 +216,100 @@ footer.bg-light {
opacity: 0;
}
}
/* Shopping List Checkbox */
.list-group-item.checked .form-check-label {
text-decoration: line-through;
opacity: 0.6;
}
.unit-btn {
padding: 0.375rem 0.5rem;
font-size: 0.875rem;
}
/* Adjust padding for the remove button in the ingredient row */
.ingredient-row .remove-ingredient {
padding: 0.375rem 0.75rem;
}
/* Custom Shopping List Styles */
.col-md-4:has(#shopping-list-container) .card {
background-color: transparent;
box-shadow: none !important;
border: 1px solid rgba(255, 255, 255, 0.1);
}
#shopping-list-container .list-group-item {
background-color: transparent;
color: #ffffff;
border-color: rgba(255, 255, 255, 0.1) !important;
}
/* Custom Checkbox Styles */
.form-check-input {
background-color: transparent;
border: 1px solid #ffffff;
}
.form-check-input:checked {
background-color: #FF3E1F;
border-color: #FF3E1F;
}
.form-check-input:focus {
border-color: #FF3E1F;
box-shadow: 0 0 0 0.25rem rgba(255, 62, 31, 0.25);
}
@media print {
body * {
visibility: hidden;
}
#shopping-list-container, #shopping-list-container * {
visibility: visible;
}
#shopping-list-container {
position: absolute;
left: 0;
top: 0;
width: 100%;
}
.card {
border: none !important;
box-shadow: none !important;
}
.list-group-item {
color: #000 !important;
background-color: #fff !important;
}
.badge {
color: #000 !important;
background-color: #fff !important;
border: 1px solid #ccc;
}
.form-check-input {
border: 1px solid #000 !important;
}
h2, h3 {
color: #000 !important;
}
}
.bg-custom-green {
background-color: #013617 !important;
}
/* Modal Styles */
#recipe-form-modal .modal-content {
background-color: #013617;
color: white;
}
#recipe-form-modal .modal-header {
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
}
#recipe-form-modal .btn-close {
filter: invert(1);
}

View File

@ -1,100 +1,68 @@
document.addEventListener('DOMContentLoaded', function () {
// --- Snowflakes Effect ---
function createSnowflakes() {
const snowContainer = document.getElementById('snow-container');
if (!snowContainer) return;
snowContainer.innerHTML = '';
const numberOfSnowflakes = 50;
for (let i = 0; i < numberOfSnowflakes; i++) {
const snowflake = document.createElement('div');
snowflake.className = 'snowflake';
const size = Math.random() * 4 + 2; // size from 2px to 6px
snowflake.style.width = `${size}px`;
snowflake.style.height = `${size}px`;
snowflake.style.left = Math.random() * 100 + 'vw';
const animationDuration = Math.random() * 5 + 5; // 5 to 10 seconds
snowflake.style.animationDuration = `${animationDuration}s`;
const animationDelay = Math.random() * 5; // 0 to 5 seconds
snowflake.style.animationDelay = `${animationDelay}s`;
snowflake.style.opacity = Math.random() * 0.7 + 0.3; // 0.3 to 1.0
snowContainer.appendChild(snowflake);
}
}
createSnowflakes();
// --- DOM Elements ---
const recipeNameInput = document.getElementById('recipeName');
const guestCountInput = document.getElementById('guestCount');
const ingredientsContainer = document.getElementById('ingredients-container');
const addIngredientBtn = document.getElementById('add-ingredient');
const calculateBtn = document.getElementById('calculate-btn');
const newRecipeBtn = document.getElementById('new-recipe-btn');
const shoppingListContainer = document.getElementById('shopping-list-container');
const recipeCardsContainer = document.getElementById('recipe-cards-container');
// --- Core Functions ---
async function loadRecipes() {
const app = {
dom: {},
state: {
recipes: [],
},
api: {
async getRecipes() {
try {
const response = await fetch('api/get_recipes.php');
const data = await response.json();
if (data.success) {
renderRecipeCards(data.recipes);
app.state.recipes = data.recipes;
} else {
console.error('Failed to load recipes:', data.error);
recipeCardsContainer.innerHTML = '<div class="col-12"><p class="text-center text-danger">Error loading recipes.</p></div>';
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);
recipeCardsContainer.innerHTML = '<div class="col-12"><p class="text-center text-danger">Could not connect to the server.</p></div>';
app.dom.recipeCardsContainer.innerHTML = '<div class="col-12"><p class="text-center text-danger">Could not connect to the server.</p></div>';
}
},
async saveRecipe(recipeData) {
try {
const response = await fetch('api/save_recipe.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(recipeData)
});
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) {
await app.api.getRecipes();
app.ui.renderRecipeCards(app.state.recipes);
app.ui.updateShoppingList();
} else {
alert('Failed to delete recipe: ' + data.error);
}
} catch (error) {
alert('Error: ' + error.message);
}
}
function addIngredientRow(ingredient = { name: '', quantity: '', unit: '' }) {
const row = document.createElement('div');
row.className = 'ingredient-row mb-2';
row.innerHTML = `
<input type="text" class="form-control" placeholder="Ingredient Name" aria-label="Ingredient Name" value="${ingredient.name}">
<input type="number" class="form-control" placeholder="Qty" aria-label="Quantity" min="0" step="any" value="${ingredient.quantity}">
<input type="text" class="form-control" placeholder="Unit (e.g., grams, ml)" aria-label="Unit" value="${ingredient.unit}">
<button type="button" class="btn btn-danger btn-sm remove-ingredient">&times;</button>
`;
ingredientsContainer.appendChild(row);
}
function clearForm() {
recipeNameInput.value = '';
guestCountInput.value = '1';
ingredientsContainer.innerHTML = '';
addIngredientRow();
shoppingListContainer.innerHTML = `
<div class="text-center text-muted p-5">
<h3 class="h4">Your Shopping List</h3>
<p>Your calculated list will appear here.</p>
</div>
`;
}
function renderRecipeCards(recipes) {
recipeCardsContainer.innerHTML = '';
},
ui: {
renderRecipeCards(recipes) {
app.dom.recipeCardsContainer.innerHTML = '';
if (!recipes || recipes.length === 0) {
recipeCardsContainer.innerHTML = '<div class="col-12"><p class="text-center text-muted">Здесь будут появляться ваши сохраненные рецепты.</p></div>';
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 => {
const cardCol = document.createElement('div');
cardCol.className = 'col-lg-4 col-md-6 mb-4';
cardCol.className = 'col-12 mb-3';
cardCol.setAttribute('data-id', recipe.id);
const card = document.createElement('div');
@ -107,59 +75,193 @@ document.addEventListener('DOMContentLoaded', function () {
title.className = 'card-title';
title.textContent = recipe.name;
const subtitle = document.createElement('h6');
subtitle.className = 'card-subtitle mb-2 text-muted';
subtitle.textContent = `${recipe.guests} guest(s)`;
const text = document.createElement('p');
text.className = 'card-text';
text.className = 'card-text text-muted';
text.textContent = `${recipe.ingredients.length} ingredients`;
const buttonGroup = document.createElement('div');
buttonGroup.className = 'mt-auto';
buttonGroup.className = 'mt-auto pt-2';
buttonGroup.innerHTML = `
<button class="btn btn-sm btn-outline-primary edit-recipe-btn">Edit</button>
<button class="btn btn-sm btn-outline-danger delete-recipe-btn">Delete</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">Delete</button>
`;
cardBody.appendChild(title);
cardBody.appendChild(subtitle);
cardBody.appendChild(text);
cardBody.appendChild(buttonGroup);
card.appendChild(cardBody);
cardCol.appendChild(card);
recipeCardsContainer.appendChild(cardCol);
app.dom.recipeCardsContainer.appendChild(cardCol);
});
},
updateShoppingList() {
const guestCount = parseInt(app.dom.guestCountInput.value, 10) || 1;
const portionsPerGuest = parseInt(app.dom.portionsPerGuestInput.value, 10) || 1;
const totalMultiplier = guestCount * portionsPerGuest;
const groups = {
Weight: { units: ['g', 'kg'], ingredients: new Map() },
Volume: { units: ['ml', 'l'], ingredients: new Map() },
Count: { units: ['piece', 'pack'], ingredients: new Map() },
Other: { units: [], ingredients: new Map() }
};
app.state.recipes.forEach(recipe => {
if (recipe.ingredients) {
recipe.ingredients.forEach(ing => {
const ingName = ing.name || '';
const ingUnit = ing.unit || '';
const key = `${ingName.trim().toLowerCase()}|${ingUnit.trim().toLowerCase()}`;
let groupName = 'Other';
for (const name in groups) {
if (groups[name].units.includes(ingUnit)) {
groupName = name;
break;
}
}
const existing = groups[groupName].ingredients.get(key);
if (existing) {
existing.qty += (ing.quantity || 0);
} else {
groups[groupName].ingredients.set(key, {
name: ing.name,
qty: (ing.quantity || 0),
unit: ing.unit
});
}
});
}
});
let html = '';
let totalIngredients = 0;
if (app.state.additionalProducts) {
app.state.additionalProducts.forEach(prod => {
const key = `${prod.name.trim().toLowerCase()}|${prod.unit.trim().toLowerCase()}`;
let groupName = 'Other';
for (const name in groups) {
if (groups[name].units.includes(prod.unit)) {
groupName = name;
break;
}
}
const existing = groups[groupName].ingredients.get(key);
if (existing) {
existing.qty += prod.quantity;
} else {
groups[groupName].ingredients.set(key, {
name: prod.name,
qty: prod.quantity,
unit: prod.unit
});
}
});
}
function renderShoppingList(list) {
let html = '<h3>Общий список покупок</h3><hr>';
if (list.length === 0) {
html += '<p>Нет ингредиентов для расчета.</p>';
} else {
for (const groupName in groups) {
const group = groups[groupName];
const ingredientList = Array.from(group.ingredients.values());
if (ingredientList.length > 0) {
totalIngredients += ingredientList.length;
html += `<h4 class="mt-3">${groupName}</h4>`;
html += '<ul class="list-group list-group-flush">';
list.forEach(item => {
const quantityStr = Number.isInteger(item.qty) ? item.qty : parseFloat(item.qty.toFixed(2));
ingredientList.forEach((item, index) => {
const totalQty = item.qty * totalMultiplier;
const quantityStr = Number.isInteger(totalQty) ? totalQty : parseFloat(totalQty.toFixed(2));
const uniqueId = `shopping-item-${groupName}-${index}`;
html += `<li class="list-group-item d-flex justify-content-between align-items-center">
<span>${item.name}</span>
<span class="badge bg-primary rounded-pill">${quantityStr} ${item.unit}</span>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="${uniqueId}">
<label class="form-check-label" for="${uniqueId}">
${item.name}
</label>
</div>
<span class="badge bg-custom-green rounded-pill">${quantityStr} ${item.unit}</span>
</li>`;
});
html += '</ul>';
}
shoppingListContainer.innerHTML = html;
}
function getRecipeDataFromForm() {
const recipeName = recipeNameInput.value.trim();
const guests = parseInt(guestCountInput.value, 10) || 0;
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;
},
addIngredientRow(ingredient = { name: '', quantity: '', unit: 'g' }) {
const row = document.createElement('div');
row.className = 'ingredient-row mb-3';
const units = ['g', 'kg', 'ml', 'l', 'piece', '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.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 a 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.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();
},
getRecipeDataFromForm() {
const recipeName = app.dom.recipeNameInput.value.trim();
const guests = parseInt(app.dom.guestCountInput.value, 10) || 0;
const ingredients = [];
const rows = ingredientsContainer.querySelectorAll('.ingredient-row');
const rows = app.dom.ingredientsContainer.querySelectorAll('.ingredient-row');
rows.forEach(row => {
const name = row.querySelector('input[placeholder="Ingredient Name"]').value.trim();
const qty = parseFloat(row.querySelector('input[placeholder="Qty"]').value);
const unit = row.querySelector('input[placeholder="Unit (e.g., grams, ml)"]').value.trim();
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 });
}
@ -169,151 +271,196 @@ document.addEventListener('DOMContentLoaded', function () {
return { name: recipeName, guests, ingredients };
}
return null;
},
createSnowflakes() {
const snowContainer = document.getElementById('snow-container');
if (!snowContainer) return;
snowContainer.innerHTML = '';
const numberOfSnowflakes = 50;
for (let i = 0; i < numberOfSnowflakes; i++) {
const snowflake = document.createElement('div');
snowflake.className = 'snowflake';
const size = Math.random() * 4 + 2;
snowflake.style.width = `${size}px`;
snowflake.style.height = `${size}px`;
snowflake.style.left = Math.random() * 100 + 'vw';
const animationDuration = Math.random() * 5 + 5;
snowflake.style.animationDuration = `${animationDuration}s`;
const animationDelay = Math.random() * 5;
snowflake.style.animationDelay = `${animationDelay}s`;
snowflake.style.opacity = Math.random() * 0.7 + 0.3;
snowContainer.appendChild(snowflake);
}
}
},
events: {
attachEventListeners() {
app.dom.addIngredientBtn.addEventListener('click', () => app.ui.addIngredientRow());
// --- Event Listeners ---
addIngredientBtn.addEventListener('click', () => addIngredientRow());
ingredientsContainer.addEventListener('click', function(e) {
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');
}
}
});
recipeCardsContainer.addEventListener('click', async function(e) {
app.dom.newRecipeBtn.addEventListener('click', async function() {
const recipeData = app.ui.getRecipeDataFromForm();
if (!recipeData) {
alert('Please fill out the recipe name, guests, and at least one ingredient before saving.');
return;
}
const recipeId = app.dom.recipeIdInput.value;
if (recipeId) {
recipeData.id = recipeId;
}
const data = await app.api.saveRecipe(recipeData);
if (data.success && data.recipe) {
const savedRecipe = data.recipe;
if (recipeId) {
const index = app.state.recipes.findIndex(r => r.id == recipeId);
if (index !== -1) {
app.state.recipes[index] = savedRecipe;
}
const card = app.dom.recipeCardsContainer.querySelector(`[data-id="${recipeId}"]`);
if (card) {
card.querySelector('.card-title').textContent = savedRecipe.name;
card.querySelector('.card-text').textContent = `${savedRecipe.ingredients.length} ingredients`;
}
} else {
app.state.recipes.unshift(savedRecipe);
app.ui.renderRecipeCards(app.state.recipes);
}
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;
const card = target.closest('.col-lg-4');
const card = target.closest('.col-12[data-id]');
if (!card) return;
const recipeId = card.getAttribute('data-id');
if (target.classList.contains('delete-recipe-btn')) {
if (target.classList.contains('delete-recipe')) {
if (confirm('Are you sure you want to delete this recipe?')) {
try {
const response = await fetch('api/delete_recipe.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: recipeId })
app.api.deleteRecipe(recipeId);
}
}
if (target.classList.contains('edit-recipe')) {
app.ui.populateFormForEdit(recipeId);
}
});
const data = await response.json();
if (data.success) {
card.remove();
} else {
alert('Failed to delete recipe: ' + data.error);
}
} catch (error) {
alert('Error: ' + error.message);
}
}
}
if (target.classList.contains('edit-recipe-btn')) {
// Find the recipe data from the currently rendered cards
const response = await fetch('api/get_recipes.php');
const data = await response.json();
if(!data.success) return;
const recipeToEdit = data.recipes.find(r => r.id == recipeId);
if (recipeToEdit) {
// Populate form
recipeNameInput.value = recipeToEdit.name;
guestCountInput.value = recipeToEdit.guests;
ingredientsContainer.innerHTML = '';
recipeToEdit.ingredients.forEach(ing => addIngredientRow(ing));
// Delete the old recipe from DB
try {
await fetch('api/delete_recipe.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: recipeId })
});
card.remove(); // Remove from UI immediately
} catch (error) {
alert('Error preparing for edit: ' + error.message);
}
app.dom.shoppingListContainer.addEventListener('change', 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);
}
}
});
newRecipeBtn.addEventListener('click', async function() {
const recipeData = getRecipeDataFromForm();
if (recipeData) {
try {
const response = await fetch('api/save_recipe.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(recipeData)
});
const data = await response.json();
if (data.success) {
await loadRecipes(); // Reload all recipes to show the new one
clearForm();
} else {
alert('Failed to save recipe: ' + data.error);
}
} catch (error) {
alert('Error: ' + error.message);
}
} else {
alert('Please fill out the recipe name, guests, and at least one ingredient before saving.');
}
app.dom.guestCountInput.addEventListener('input', app.ui.updateShoppingList);
app.dom.portionsPerGuestInput.addEventListener('input', app.ui.updateShoppingList);
app.dom.cancelEditBtn.addEventListener('click', function() {
app.ui.clearForm();
app.dom.recipeFormModal.hide();
});
calculateBtn.addEventListener('click', async function() {
try {
const response = await fetch('api/get_recipes.php');
const data = await response.json();
if (!data.success) {
alert('Could not get recipes for calculation.');
app.dom.recipeSearchInput.addEventListener('input', function() {
const searchTerm = app.dom.recipeSearchInput.value.toLowerCase();
const filteredRecipes = app.state.recipes.filter(recipe => recipe.name.toLowerCase().includes(searchTerm));
app.ui.renderRecipeCards(filteredRecipes);
});
app.dom.printShoppingListBtn.addEventListener('click', function() {
window.print();
});
app.dom.addProductBtn.addEventListener('click', () => {
const name = prompt('Enter product name:');
if (!name) return;
const quantity = parseFloat(prompt('Enter quantity:'));
if (isNaN(quantity) || quantity <= 0) {
alert('Please enter a valid quantity.');
return;
}
const allRecipesToCalculate = data.recipes;
const currentRecipe = getRecipeDataFromForm();
const unit = prompt('Enter unit (e.g., g, kg, ml, l, piece, pack):');
if (!unit) return;
if (currentRecipe) {
// Give it a temporary ID to avoid collisions
currentRecipe.id = 'current';
allRecipesToCalculate.push(currentRecipe);
if (!app.state.additionalProducts) {
app.state.additionalProducts = [];
}
if (allRecipesToCalculate.length === 0) {
alert('There are no recipes to calculate. Please fill out the form or save a recipe.');
return;
}
const combinedIngredients = new Map();
allRecipesToCalculate.forEach(recipe => {
const multiplier = recipe.guests;
recipe.ingredients.forEach(ing => {
const ingName = ing.name || '';
const ingUnit = ing.unit || '';
const key = `${ingName.trim().toLowerCase()}|${ingUnit.trim().toLowerCase()}`;
const existing = combinedIngredients.get(key);
if (existing) {
existing.qty += (ing.quantity || 0) * multiplier;
} else {
combinedIngredients.set(key, {
name: ing.name,
qty: (ing.quantity || 0) * multiplier,
unit: ing.unit
app.state.additionalProducts.push({
name: name.trim(),
quantity: quantity,
unit: unit.trim()
});
app.ui.updateShoppingList();
});
document.getElementById('recipe-form-modal').addEventListener('show.bs.modal', function () {
if (!app.dom.recipeIdInput.value) {
app.ui.clearForm();
}
});
});
renderShoppingList(Array.from(combinedIngredients.values()));
} catch(error) {
alert('Calculation Error: ' + error.message);
}
});
},
init() {
app.dom = {
recipeNameInput: document.getElementById('recipeName'),
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'),
recipeSearchInput: document.getElementById('recipe-search'),
addProductBtn: document.getElementById('add-product-btn'),
// --- Initial State ---
addIngredientRow();
loadRecipes();
});
printShoppingListBtn: document.getElementById('print-shopping-list-btn'),
recipeFormModal: new bootstrap.Modal(document.getElementById('recipe-form-modal'))
};
app.ui.createSnowflakes();
app.events.attachEventListeners();
app.dom.cancelEditBtn.style.display = 'none';
app.ui.addIngredientRow();
app.api.getRecipes().then(() => {
app.ui.renderRecipeCards(app.state.recipes);
app.ui.updateShoppingList();
});
}
};
document.addEventListener('DOMContentLoaded', app.init);

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

118
index.php
View File

@ -26,6 +26,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
</head>
@ -33,28 +34,6 @@
<div id="snow-container"></div>
<!-- Christmas Trees -->
<div class="christmas-tree-container left">
<div class="tree">
<div class="ornament o1"></div>
<div class="ornament o2"></div>
<div class="ornament o3"></div>
<div class="ornament o4"></div>
<div class="ornament o5"></div>
<div class="ornament o6"></div>
</div>
</div>
<div class="christmas-tree-container right">
<div class="tree">
<div class="ornament o1"></div>
<div class="ornament o2"></div>
<div class="ornament o3"></div>
<div class="ornament o4"></div>
<div class="ornament o5"></div>
<div class="ornament o6"></div>
</div>
</div>
<nav class="navbar navbar-expand-lg navbar-dark shadow-sm">
<div class="container">
<a class="navbar-brand" href="#">Recipe Calculator</a>
@ -67,12 +46,57 @@
<p class="lead">Let's get your holiday recipes sorted.</p>
</div>
<div class="row g-5">
<!-- Left Column: Recipe Input -->
<div class="col-lg-6">
<div class="row g-4">
<!-- Left Column: All Recipes -->
<div class="col-md-6">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="text-center mb-0">All Recipes</h2>
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#recipe-form-modal">
<i class="bi bi-plus-lg"></i> Add Recipe
</button>
</div>
<div class="mb-3">
<input type="text" id="recipe-search" class="form-control" placeholder="Search recipes...">
</div>
<div id="recipe-cards-container" class="row">
<div class="col-12">
<p class="text-center text-muted">Your saved recipes will appear here.</p>
</div>
</div>
</div>
<!-- Right Column: Shopping List / Products -->
<div class="col-md-6">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="text-center mb-0">Shopping List</h2>
<div>
<button id="add-product-btn" class="btn btn-primary me-2"><i class="bi bi-plus-lg"></i> Add Product</button>
<button id="print-shopping-list-btn" class="btn btn-outline-secondary btn-sm"><i class="bi bi-printer"></i> Print</button>
</div>
</div>
<div class="card shadow">
<div class="card-body" id="shopping-list-container">
<div class="text-center text-muted p-5">
<p>Your calculated list will appear here.</p>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- Modal Recipe Form -->
<div class="modal fade" id="recipe-form-modal" tabindex="-1" aria-labelledby="recipe-form-modal-label" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="recipe-form-modal-label">Add a Recipe</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="card p-4 shadow">
<h2 class="mb-4">Your Recipe</h2>
<form id="recipe-form">
<input type="hidden" id="recipeId">
<div class="mb-3">
<label for="recipeName" class="form-label">Recipe Name</label>
<input type="text" class="form-control" id="recipeName" placeholder="e.g., Gingerbread Cookies">
@ -88,45 +112,31 @@
<hr class="my-4 border-secondary">
<div class="row">
<div class="col">
<div class="mb-3">
<label for="guestCount" class="form-label">How many guests?</label>
<input type="number" class="form-control" id="guestCount" placeholder="e.g., 8" min="1">
<input type="number" class="form-control" id="guestCount" placeholder="e.g., 8" min="1" value="1">
</div>
</div>
<div class="col">
<div class="mb-3">
<label for="portionsPerGuest" class="form-label">Portions/guest</label>
<input type="number" class="form-control" id="portionsPerGuest" placeholder="e.g., 2" min="1" value="1">
</div>
</div>
</div>
<div class="d-grid gap-3 d-md-flex justify-content-md-start mt-4">
<button type="button" id="calculate-btn" class="btn btn-primary">Calculate Shopping List</button>
<button type="button" id="new-recipe-btn" class="btn btn-outline-secondary">New Recipe</button>
<div class="d-grid gap-2 mt-4">
<button type="button" id="new-recipe-btn" class="btn btn-primary">Save Recipe</button>
<button type="button" id="cancel-edit-btn" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
</div>
</form>
</div>
</div>
<!-- Right Column: Shopping List -->
<div class="col-lg-6">
<div class="card shadow">
<div class="card-body" id="shopping-list-container">
<div class="text-center text-muted p-5">
<h3 class="h4">Your Shopping List</h3>
<p>Your calculated list will appear here.</p>
</div>
</div>
</div>
</div>
</div>
<hr class="my-5 border-secondary">
<div class="row">
<div class="col-12">
<h2 class="text-center mb-4">Saved Recipes</h2>
<div id="recipe-cards-container" class="row">
<div class="col-12">
<p class="text-center text-muted">Your saved recipes will appear here.</p>
</div>
</div>
</div>
</div>
</main>
<footer class="text-center py-4 mt-5">
<p class="mb-0">&copy; <?php echo date("Y"); ?> Christmas Recipe Calculator. Happy Holidays!</p>