4
This commit is contained in:
parent
24d0513664
commit
32cc8d6cd2
@ -14,13 +14,31 @@ $pdo = db();
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
$stmt = $pdo->prepare("INSERT INTO recipes (name, guests) VALUES (?, ?)");
|
||||
$stmt->execute([$data['name'], $data['guests']]);
|
||||
$recipeId = $pdo->lastInsertId();
|
||||
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]);
|
||||
|
||||
$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']]);
|
||||
// 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();
|
||||
|
||||
$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']]);
|
||||
}
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -1,319 +1,466 @@
|
||||
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() {
|
||||
try {
|
||||
const response = await fetch('api/get_recipes.php');
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
renderRecipeCards(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>';
|
||||
}
|
||||
} 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>';
|
||||
}
|
||||
}
|
||||
|
||||
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">×</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) {
|
||||
recipeCardsContainer.innerHTML = '<div class="col-12"><p class="text-center text-muted">Здесь будут появляться ваши сохраненные рецепты.</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
recipes.forEach(recipe => {
|
||||
const cardCol = document.createElement('div');
|
||||
cardCol.className = 'col-lg-4 col-md-6 mb-4';
|
||||
cardCol.setAttribute('data-id', recipe.id);
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card h-100';
|
||||
|
||||
const cardBody = document.createElement('div');
|
||||
cardBody.className = 'card-body d-flex flex-column';
|
||||
|
||||
const title = document.createElement('h5');
|
||||
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.textContent = `${recipe.ingredients.length} ingredients`;
|
||||
|
||||
const buttonGroup = document.createElement('div');
|
||||
buttonGroup.className = 'mt-auto';
|
||||
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>
|
||||
`;
|
||||
|
||||
cardBody.appendChild(title);
|
||||
cardBody.appendChild(subtitle);
|
||||
cardBody.appendChild(text);
|
||||
cardBody.appendChild(buttonGroup);
|
||||
card.appendChild(cardBody);
|
||||
cardCol.appendChild(card);
|
||||
recipeCardsContainer.appendChild(cardCol);
|
||||
});
|
||||
}
|
||||
|
||||
function renderShoppingList(list) {
|
||||
let html = '<h3>Общий список покупок</h3><hr>';
|
||||
if (list.length === 0) {
|
||||
html += '<p>Нет ингредиентов для расчета.</p>';
|
||||
} else {
|
||||
html += '<ul class="list-group list-group-flush">';
|
||||
list.forEach(item => {
|
||||
const quantityStr = Number.isInteger(item.qty) ? item.qty : parseFloat(item.qty.toFixed(2));
|
||||
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>
|
||||
</li>`;
|
||||
});
|
||||
html += '</ul>';
|
||||
}
|
||||
shoppingListContainer.innerHTML = html;
|
||||
}
|
||||
|
||||
function getRecipeDataFromForm() {
|
||||
const recipeName = recipeNameInput.value.trim();
|
||||
const guests = parseInt(guestCountInput.value, 10) || 0;
|
||||
|
||||
const ingredients = [];
|
||||
const rows = 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();
|
||||
if (name && !isNaN(qty) && qty > 0) {
|
||||
ingredients.push({ name, quantity: qty, unit });
|
||||
}
|
||||
});
|
||||
|
||||
if (recipeName && guests > 0 && ingredients.length > 0) {
|
||||
return { name: recipeName, guests, ingredients };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// --- Event Listeners ---
|
||||
|
||||
addIngredientBtn.addEventListener('click', () => addIngredientRow());
|
||||
|
||||
ingredientsContainer.addEventListener('click', function(e) {
|
||||
if (e.target.classList.contains('remove-ingredient')) {
|
||||
e.target.closest('.ingredient-row').remove();
|
||||
}
|
||||
});
|
||||
|
||||
recipeCardsContainer.addEventListener('click', async function(e) {
|
||||
const target = e.target;
|
||||
const card = target.closest('.col-lg-4');
|
||||
if (!card) return;
|
||||
|
||||
const recipeId = card.getAttribute('data-id');
|
||||
|
||||
if (target.classList.contains('delete-recipe-btn')) {
|
||||
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 })
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
card.remove();
|
||||
} else {
|
||||
alert('Failed to delete recipe: ' + data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Error: ' + error.message);
|
||||
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) {
|
||||
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">Could not connect to the server.</p></div>';
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
newRecipeBtn.addEventListener('click', async function() {
|
||||
const recipeData = getRecipeDataFromForm();
|
||||
if (recipeData) {
|
||||
},
|
||||
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 loadRecipes(); // Reload all recipes to show the new one
|
||||
clearForm();
|
||||
await app.api.getRecipes();
|
||||
app.ui.renderRecipeCards(app.state.recipes);
|
||||
app.ui.updateShoppingList();
|
||||
} else {
|
||||
alert('Failed to save recipe: ' + data.error);
|
||||
alert('Failed to delete 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.');
|
||||
}
|
||||
});
|
||||
},
|
||||
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;
|
||||
}
|
||||
|
||||
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.');
|
||||
return;
|
||||
}
|
||||
|
||||
const allRecipesToCalculate = data.recipes;
|
||||
const currentRecipe = getRecipeDataFromForm();
|
||||
|
||||
if (currentRecipe) {
|
||||
// Give it a temporary ID to avoid collisions
|
||||
currentRecipe.id = 'current';
|
||||
allRecipesToCalculate.push(currentRecipe);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
recipes.forEach(recipe => {
|
||||
const cardCol = document.createElement('div');
|
||||
cardCol.className = 'col-12 mb-3';
|
||||
cardCol.setAttribute('data-id', recipe.id);
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card h-100';
|
||||
|
||||
const cardBody = document.createElement('div');
|
||||
cardBody.className = 'card-body d-flex flex-column';
|
||||
|
||||
const title = document.createElement('h5');
|
||||
title.className = 'card-title';
|
||||
title.textContent = recipe.name;
|
||||
|
||||
const text = document.createElement('p');
|
||||
text.className = 'card-text text-muted';
|
||||
text.textContent = `${recipe.ingredients.length} ingredients`;
|
||||
|
||||
const buttonGroup = document.createElement('div');
|
||||
buttonGroup.className = 'mt-auto pt-2';
|
||||
buttonGroup.innerHTML = `
|
||||
<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(text);
|
||||
cardBody.appendChild(buttonGroup);
|
||||
card.appendChild(cardBody);
|
||||
cardCol.appendChild(card);
|
||||
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 += (ing.quantity || 0) * multiplier;
|
||||
existing.qty += prod.quantity;
|
||||
} else {
|
||||
combinedIngredients.set(key, {
|
||||
name: ing.name,
|
||||
qty: (ing.quantity || 0) * multiplier,
|
||||
unit: ing.unit
|
||||
groups[groupName].ingredients.set(key, {
|
||||
name: prod.name,
|
||||
qty: prod.quantity,
|
||||
unit: prod.unit
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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">';
|
||||
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">
|
||||
<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>';
|
||||
}
|
||||
}
|
||||
|
||||
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">×</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 = 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 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, 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());
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
renderShoppingList(Array.from(combinedIngredients.values()));
|
||||
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;
|
||||
}
|
||||
|
||||
} catch(error) {
|
||||
alert('Calculation Error: ' + error.message);
|
||||
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-12[data-id]');
|
||||
if (!card) return;
|
||||
|
||||
const recipeId = card.getAttribute('data-id');
|
||||
|
||||
if (target.classList.contains('delete-recipe')) {
|
||||
if (confirm('Are you sure you want to delete this recipe?')) {
|
||||
app.api.deleteRecipe(recipeId);
|
||||
}
|
||||
}
|
||||
|
||||
if (target.classList.contains('edit-recipe')) {
|
||||
app.ui.populateFormForEdit(recipeId);
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
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 unit = prompt('Enter unit (e.g., g, kg, ml, l, piece, pack):');
|
||||
if (!unit) return;
|
||||
|
||||
if (!app.state.additionalProducts) {
|
||||
app.state.additionalProducts = [];
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
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'),
|
||||
|
||||
printShoppingListBtn: document.getElementById('print-shopping-list-btn'),
|
||||
recipeFormModal: new bootstrap.Modal(document.getElementById('recipe-form-modal'))
|
||||
};
|
||||
|
||||
// --- Initial State ---
|
||||
addIngredientRow();
|
||||
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 |
154
index.php
154
index.php
@ -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,67 +46,98 @@
|
||||
<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="card p-4 shadow">
|
||||
<h2 class="mb-4">Your Recipe</h2>
|
||||
<form id="recipe-form">
|
||||
<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">
|
||||
</div>
|
||||
|
||||
<hr class="my-4 border-secondary">
|
||||
|
||||
<h3 class="h5 mb-3">Ingredients (for 1 person)</h3>
|
||||
<div id="ingredients-container">
|
||||
<!-- Ingredient rows will be injected here by JS -->
|
||||
</div>
|
||||
<button type="button" id="add-ingredient" class="btn btn-secondary btn-sm mt-2">+ Add Ingredient</button>
|
||||
|
||||
<hr class="my-4 border-secondary">
|
||||
|
||||
<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">
|
||||
</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>
|
||||
</form>
|
||||
<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>
|
||||
|
||||
<!-- 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 class="mb-3">
|
||||
<input type="text" id="recipe-search" class="form-control" placeholder="Search recipes...">
|
||||
</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>
|
||||
|
||||
<!-- 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">
|
||||
<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">
|
||||
</div>
|
||||
|
||||
<hr class="my-4 border-secondary">
|
||||
|
||||
<h3 class="h5 mb-3">Ingredients (for 1 person)</h3>
|
||||
<div id="ingredients-container">
|
||||
<!-- Ingredient rows will be injected here by JS -->
|
||||
</div>
|
||||
<button type="button" id="add-ingredient" class="btn btn-secondary btn-sm mt-2">+ Add Ingredient</button>
|
||||
|
||||
<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" 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-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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="text-center py-4 mt-5">
|
||||
<p class="mb-0">© <?php echo date("Y"); ?> Christmas Recipe Calculator. Happy Holidays!</p>
|
||||
</footer>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user