4
This commit is contained in:
parent
24d0513664
commit
32cc8d6cd2
@ -14,6 +14,23 @@ $pdo = db();
|
|||||||
try {
|
try {
|
||||||
$pdo->beginTransaction();
|
$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 = $pdo->prepare("INSERT INTO recipes (name, guests) VALUES (?, ?)");
|
||||||
$stmt->execute([$data['name'], $data['guests']]);
|
$stmt->execute([$data['name'], $data['guests']]);
|
||||||
$recipeId = $pdo->lastInsertId();
|
$recipeId = $pdo->lastInsertId();
|
||||||
@ -22,6 +39,7 @@ try {
|
|||||||
foreach ($data['ingredients'] as $ing) {
|
foreach ($data['ingredients'] as $ing) {
|
||||||
$stmt->execute([$recipeId, $ing['name'], $ing['quantity'], $ing['unit']]);
|
$stmt->execute([$recipeId, $ing['name'], $ing['quantity'], $ing['unit']]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$pdo->commit();
|
$pdo->commit();
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/* General Body Styles */
|
/* General Body Styles */
|
||||||
body {
|
body {
|
||||||
background-color: #0a2e36; /* Dark teal background */
|
background-color: #013617; /* Dark green background */
|
||||||
color: #ffffff; /* White text */
|
color: #ffffff; /* White text */
|
||||||
font-family: 'Poppins', sans-serif;
|
font-family: 'Poppins', sans-serif;
|
||||||
padding-top: 40px; /* Make space for garland */
|
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 */
|
color: #ffffff !important; /* Use !important to override other styles if necessary */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Christmas Tree */
|
/* Buttons and Inputs */
|
||||||
.christmas-tree-container {
|
input,
|
||||||
position: fixed;
|
button,
|
||||||
bottom: 0;
|
.btn {
|
||||||
width: 100px;
|
border-radius: 8px !important;
|
||||||
height: 150px;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.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 */
|
/* Garland */
|
||||||
body::before {
|
body::before {
|
||||||
@ -82,9 +28,9 @@ body::before {
|
|||||||
width: 110%;
|
width: 110%;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle, #ff2424 4px, transparent 5px),
|
radial-gradient(circle, #FF3E1F 4px, transparent 5px),
|
||||||
radial-gradient(circle, #24ff24 4px, transparent 5px),
|
radial-gradient(circle, #013617 4px, transparent 5px),
|
||||||
radial-gradient(circle, #2424ff 4px, transparent 5px),
|
radial-gradient(circle, #FFAFCA 4px, transparent 5px),
|
||||||
radial-gradient(circle, #ffff24 4px, transparent 5px),
|
radial-gradient(circle, #ffff24 4px, transparent 5px),
|
||||||
radial-gradient(circle, #ff24ff 4px, transparent 5px);
|
radial-gradient(circle, #ff24ff 4px, transparent 5px);
|
||||||
background-size: 100px 20px;
|
background-size: 100px 20px;
|
||||||
@ -103,7 +49,7 @@ body::before {
|
|||||||
|
|
||||||
/* Navbar */
|
/* Navbar */
|
||||||
.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);
|
backdrop-filter: blur(10px);
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
}
|
}
|
||||||
@ -143,13 +89,12 @@ body::before {
|
|||||||
background-color: rgba(0, 0, 0, 0.2);
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
border-radius: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control:focus {
|
.form-control:focus {
|
||||||
background-color: rgba(0, 0, 0, 0.3);
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
border-color: #ff6f61; /* Coral red accent */
|
border-color: #FF3E1F; /* Coral red accent */
|
||||||
box-shadow: 0 0 0 0.25rem rgba(255, 111, 97, 0.25);
|
box-shadow: 0 0 0 0.25rem rgba(255, 62, 31, 0.25);
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,19 +104,18 @@ body::before {
|
|||||||
|
|
||||||
/* Buttons */
|
/* Buttons */
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background-color: #ff6f61; /* Coral red */
|
background-color: #FF3E1F; /* Coral red */
|
||||||
border-color: #ff6f61;
|
border-color: #FF3E1F;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding: 12px 30px;
|
padding: 12px 30px;
|
||||||
border-radius: 50px;
|
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:hover {
|
.btn-primary:hover {
|
||||||
background-color: #e65a50;
|
background-color: #E6381A;
|
||||||
border-color: #e65a50;
|
border-color: #E6381A;
|
||||||
transform: translateY(-2px);
|
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 {
|
.btn-outline-secondary {
|
||||||
@ -179,7 +123,6 @@ body::before {
|
|||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding: 12px 30px;
|
padding: 12px 30px;
|
||||||
border-radius: 50px;
|
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,6 +151,36 @@ body::before {
|
|||||||
margin-bottom: 15px;
|
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 */
|
||||||
footer.bg-light {
|
footer.bg-light {
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
@ -234,7 +207,7 @@ footer.bg-light {
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
animation: fall linear infinite;
|
animation: fall linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fall {
|
@keyframes fall {
|
||||||
@ -243,3 +216,100 @@ footer.bg-light {
|
|||||||
opacity: 0;
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,100 +1,68 @@
|
|||||||
document.addEventListener('DOMContentLoaded', function () {
|
const app = {
|
||||||
|
dom: {},
|
||||||
// --- Snowflakes Effect ---
|
state: {
|
||||||
function createSnowflakes() {
|
recipes: [],
|
||||||
const snowContainer = document.getElementById('snow-container');
|
},
|
||||||
if (!snowContainer) return;
|
api: {
|
||||||
|
async getRecipes() {
|
||||||
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() {
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('api/get_recipes.php');
|
const response = await fetch('api/get_recipes.php');
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
renderRecipeCards(data.recipes);
|
app.state.recipes = data.recipes;
|
||||||
} else {
|
} else {
|
||||||
console.error('Failed to load recipes:', data.error);
|
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) {
|
} catch (error) {
|
||||||
console.error('Error:', 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: '' }) {
|
ui: {
|
||||||
const row = document.createElement('div');
|
renderRecipeCards(recipes) {
|
||||||
row.className = 'ingredient-row mb-2';
|
app.dom.recipeCardsContainer.innerHTML = '';
|
||||||
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">×</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 = '';
|
|
||||||
if (!recipes || recipes.length === 0) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
recipes.forEach(recipe => {
|
recipes.forEach(recipe => {
|
||||||
const cardCol = document.createElement('div');
|
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);
|
cardCol.setAttribute('data-id', recipe.id);
|
||||||
|
|
||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
@ -107,59 +75,193 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
title.className = 'card-title';
|
title.className = 'card-title';
|
||||||
title.textContent = recipe.name;
|
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');
|
const text = document.createElement('p');
|
||||||
text.className = 'card-text';
|
text.className = 'card-text text-muted';
|
||||||
text.textContent = `${recipe.ingredients.length} ingredients`;
|
text.textContent = `${recipe.ingredients.length} ingredients`;
|
||||||
|
|
||||||
const buttonGroup = document.createElement('div');
|
const buttonGroup = document.createElement('div');
|
||||||
buttonGroup.className = 'mt-auto';
|
buttonGroup.className = 'mt-auto pt-2';
|
||||||
buttonGroup.innerHTML = `
|
buttonGroup.innerHTML = `
|
||||||
<button class="btn btn-sm btn-outline-primary edit-recipe-btn">Edit</button>
|
<button class="btn btn-light btn-sm edit-recipe"><i class="bi bi-pencil"></i> Edit</button>
|
||||||
<button class="btn btn-sm btn-outline-danger delete-recipe-btn">Delete</button>
|
<button class="btn btn-outline-danger btn-sm delete-recipe">Delete</button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
cardBody.appendChild(title);
|
cardBody.appendChild(title);
|
||||||
cardBody.appendChild(subtitle);
|
|
||||||
cardBody.appendChild(text);
|
cardBody.appendChild(text);
|
||||||
cardBody.appendChild(buttonGroup);
|
cardBody.appendChild(buttonGroup);
|
||||||
card.appendChild(cardBody);
|
card.appendChild(cardBody);
|
||||||
cardCol.appendChild(card);
|
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) {
|
for (const groupName in groups) {
|
||||||
let html = '<h3>Общий список покупок</h3><hr>';
|
const group = groups[groupName];
|
||||||
if (list.length === 0) {
|
const ingredientList = Array.from(group.ingredients.values());
|
||||||
html += '<p>Нет ингредиентов для расчета.</p>';
|
|
||||||
} else {
|
if (ingredientList.length > 0) {
|
||||||
|
totalIngredients += ingredientList.length;
|
||||||
|
html += `<h4 class="mt-3">${groupName}</h4>`;
|
||||||
html += '<ul class="list-group list-group-flush">';
|
html += '<ul class="list-group list-group-flush">';
|
||||||
list.forEach(item => {
|
ingredientList.forEach((item, index) => {
|
||||||
const quantityStr = Number.isInteger(item.qty) ? item.qty : parseFloat(item.qty.toFixed(2));
|
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">
|
html += `<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
<span>${item.name}</span>
|
<div class="form-check">
|
||||||
<span class="badge bg-primary rounded-pill">${quantityStr} ${item.unit}</span>
|
<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>`;
|
</li>`;
|
||||||
});
|
});
|
||||||
html += '</ul>';
|
html += '</ul>';
|
||||||
}
|
}
|
||||||
shoppingListContainer.innerHTML = html;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRecipeDataFromForm() {
|
if (totalIngredients === 0) {
|
||||||
const recipeName = recipeNameInput.value.trim();
|
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>';
|
||||||
const guests = parseInt(guestCountInput.value, 10) || 0;
|
}
|
||||||
|
|
||||||
|
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">×</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 ingredients = [];
|
||||||
const rows = ingredientsContainer.querySelectorAll('.ingredient-row');
|
const rows = app.dom.ingredientsContainer.querySelectorAll('.ingredient-row');
|
||||||
rows.forEach(row => {
|
rows.forEach(row => {
|
||||||
const name = row.querySelector('input[placeholder="Ingredient Name"]').value.trim();
|
const name = row.querySelector('input[placeholder="Ingredient Name"]').value.trim();
|
||||||
const qty = parseFloat(row.querySelector('input[placeholder="Qty"]').value);
|
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) {
|
if (name && !isNaN(qty) && qty > 0) {
|
||||||
ingredients.push({ name, quantity: qty, unit });
|
ingredients.push({ name, quantity: qty, unit });
|
||||||
}
|
}
|
||||||
@ -169,151 +271,196 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
return { name: recipeName, guests, ingredients };
|
return { name: recipeName, guests, ingredients };
|
||||||
}
|
}
|
||||||
return null;
|
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 ---
|
app.dom.ingredientsContainer.addEventListener('click', function(e) {
|
||||||
|
|
||||||
addIngredientBtn.addEventListener('click', () => addIngredientRow());
|
|
||||||
|
|
||||||
ingredientsContainer.addEventListener('click', function(e) {
|
|
||||||
if (e.target.classList.contains('remove-ingredient')) {
|
if (e.target.classList.contains('remove-ingredient')) {
|
||||||
e.target.closest('.ingredient-row').remove();
|
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 target = e.target;
|
||||||
const card = target.closest('.col-lg-4');
|
const card = target.closest('.col-12[data-id]');
|
||||||
if (!card) return;
|
if (!card) return;
|
||||||
|
|
||||||
const recipeId = card.getAttribute('data-id');
|
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?')) {
|
if (confirm('Are you sure you want to delete this recipe?')) {
|
||||||
try {
|
app.api.deleteRecipe(recipeId);
|
||||||
const response = await fetch('api/delete_recipe.php', {
|
}
|
||||||
method: 'POST',
|
}
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ id: 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')) {
|
app.dom.shoppingListContainer.addEventListener('change', function(e) {
|
||||||
// Find the recipe data from the currently rendered cards
|
if (e.target.matches('.form-check-input')) {
|
||||||
const response = await fetch('api/get_recipes.php');
|
const listItem = e.target.closest('.list-group-item');
|
||||||
const data = await response.json();
|
if (listItem) {
|
||||||
if(!data.success) return;
|
listItem.classList.toggle('checked', e.target.checked);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
newRecipeBtn.addEventListener('click', async function() {
|
app.dom.guestCountInput.addEventListener('input', app.ui.updateShoppingList);
|
||||||
const recipeData = getRecipeDataFromForm();
|
app.dom.portionsPerGuestInput.addEventListener('input', app.ui.updateShoppingList);
|
||||||
if (recipeData) {
|
|
||||||
try {
|
app.dom.cancelEditBtn.addEventListener('click', function() {
|
||||||
const response = await fetch('api/save_recipe.php', {
|
app.ui.clearForm();
|
||||||
method: 'POST',
|
app.dom.recipeFormModal.hide();
|
||||||
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.');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
calculateBtn.addEventListener('click', async function() {
|
app.dom.recipeSearchInput.addEventListener('input', function() {
|
||||||
try {
|
const searchTerm = app.dom.recipeSearchInput.value.toLowerCase();
|
||||||
const response = await fetch('api/get_recipes.php');
|
const filteredRecipes = app.state.recipes.filter(recipe => recipe.name.toLowerCase().includes(searchTerm));
|
||||||
const data = await response.json();
|
app.ui.renderRecipeCards(filteredRecipes);
|
||||||
if (!data.success) {
|
});
|
||||||
alert('Could not get recipes for calculation.');
|
|
||||||
|
|
||||||
|
|
||||||
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const allRecipesToCalculate = data.recipes;
|
const unit = prompt('Enter unit (e.g., g, kg, ml, l, piece, pack):');
|
||||||
const currentRecipe = getRecipeDataFromForm();
|
if (!unit) return;
|
||||||
|
|
||||||
if (currentRecipe) {
|
if (!app.state.additionalProducts) {
|
||||||
// Give it a temporary ID to avoid collisions
|
app.state.additionalProducts = [];
|
||||||
currentRecipe.id = 'current';
|
|
||||||
allRecipesToCalculate.push(currentRecipe);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allRecipesToCalculate.length === 0) {
|
app.state.additionalProducts.push({
|
||||||
alert('There are no recipes to calculate. Please fill out the form or save a recipe.');
|
name: name.trim(),
|
||||||
return;
|
quantity: quantity,
|
||||||
}
|
unit: unit.trim()
|
||||||
|
|
||||||
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.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 ---
|
printShoppingListBtn: document.getElementById('print-shopping-list-btn'),
|
||||||
addIngredientRow();
|
recipeFormModal: new bootstrap.Modal(document.getElementById('recipe-form-modal'))
|
||||||
loadRecipes();
|
};
|
||||||
});
|
|
||||||
|
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);
|
||||||
BIN
assets/pasted-20251109-215032-744d90f1.jpg
Normal file
BIN
assets/pasted-20251109-215032-744d90f1.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
BIN
assets/vm-shot-2025-11-09T21-50-13-547Z.jpg
Normal file
BIN
assets/vm-shot-2025-11-09T21-50-13-547Z.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 87 KiB |
118
index.php
118
index.php
@ -26,6 +26,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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 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(); ?>">
|
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
@ -33,28 +34,6 @@
|
|||||||
|
|
||||||
<div id="snow-container"></div>
|
<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">
|
<nav class="navbar navbar-expand-lg navbar-dark shadow-sm">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<a class="navbar-brand" href="#">Recipe Calculator</a>
|
<a class="navbar-brand" href="#">Recipe Calculator</a>
|
||||||
@ -67,12 +46,57 @@
|
|||||||
<p class="lead">Let's get your holiday recipes sorted.</p>
|
<p class="lead">Let's get your holiday recipes sorted.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-5">
|
<div class="row g-4">
|
||||||
<!-- Left Column: Recipe Input -->
|
<!-- Left Column: All Recipes -->
|
||||||
<div class="col-lg-6">
|
<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">
|
<div class="card p-4 shadow">
|
||||||
<h2 class="mb-4">Your Recipe</h2>
|
|
||||||
<form id="recipe-form">
|
<form id="recipe-form">
|
||||||
|
<input type="hidden" id="recipeId">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="recipeName" class="form-label">Recipe Name</label>
|
<label for="recipeName" class="form-label">Recipe Name</label>
|
||||||
<input type="text" class="form-control" id="recipeName" placeholder="e.g., Gingerbread Cookies">
|
<input type="text" class="form-control" id="recipeName" placeholder="e.g., Gingerbread Cookies">
|
||||||
@ -88,45 +112,31 @@
|
|||||||
|
|
||||||
<hr class="my-4 border-secondary">
|
<hr class="my-4 border-secondary">
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="guestCount" class="form-label">How many guests?</label>
|
<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>
|
||||||
|
|
||||||
<div class="d-grid gap-3 d-md-flex justify-content-md-start mt-4">
|
<div class="d-grid gap-2 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-primary">Save Recipe</button>
|
||||||
<button type="button" id="new-recipe-btn" class="btn btn-outline-secondary">New Recipe</button>
|
<button type="button" id="cancel-edit-btn" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</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">
|
<footer class="text-center py-4 mt-5">
|
||||||
<p class="mb-0">© <?php echo date("Y"); ?> Christmas Recipe Calculator. Happy Holidays!</p>
|
<p class="mb-0">© <?php echo date("Y"); ?> Christmas Recipe Calculator. Happy Holidays!</p>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user