This commit is contained in:
Flatlogic Bot 2025-11-09 21:38:45 +00:00
parent d39bcc532b
commit 24d0513664
10 changed files with 666 additions and 171 deletions

24
api/delete_recipe.php Normal file
View File

@ -0,0 +1,24 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
$data = json_decode(file_get_contents('php://input'), true);
if (!$data || !isset($data['id'])) {
echo json_encode(['success' => false, 'error' => 'Invalid input. Recipe ID is missing.']);
exit;
}
$recipeId = $data['id'];
$pdo = db();
try {
$stmt = $pdo->prepare("DELETE FROM recipes WHERE id = ?");
$stmt->execute([$recipeId]);
echo json_encode(['success' => true]);
} catch (PDOException $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}

26
api/get_recipes.php Normal file
View File

@ -0,0 +1,26 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
try {
$pdo = db();
$recipes_stmt = $pdo->query('SELECT * FROM recipes ORDER BY created_at DESC');
$recipes = $recipes_stmt->fetchAll();
$ingredients_stmt = $pdo->query('SELECT * FROM ingredients');
$all_ingredients = $ingredients_stmt->fetchAll();
$ingredients_by_recipe = [];
foreach ($all_ingredients as $ingredient) {
$ingredients_by_recipe[$ingredient['recipe_id']][] = $ingredient;
}
foreach ($recipes as $i => $recipe) {
$recipes[$i]['ingredients'] = $ingredients_by_recipe[$recipe['id']] ?? [];
}
echo json_encode(['success' => true, 'recipes' => $recipes]);
} catch (PDOException $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}

43
api/save_recipe.php Normal file
View File

@ -0,0 +1,43 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
$data = json_decode(file_get_contents('php://input'), true);
if (!$data || !isset($data['name']) || !isset($data['guests']) || !isset($data['ingredients'])) {
echo json_encode(['success' => false, 'error' => 'Invalid input.']);
exit;
}
$pdo = db();
try {
$pdo->beginTransaction();
$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();
// Fetch the newly created recipe to return it to the client
$stmt = $pdo->prepare("SELECT * FROM recipes WHERE id = ?");
$stmt->execute([$recipeId]);
$recipe = $stmt->fetch();
$stmt = $pdo->prepare("SELECT * FROM ingredients WHERE recipe_id = ?");
$stmt->execute([$recipeId]);
$ingredients = $stmt->fetchAll();
$recipe['ingredients'] = $ingredients;
echo json_encode(['success' => true, 'recipe' => $recipe]);
} catch (PDOException $e) {
$pdo->rollBack();
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}

View File

@ -1,95 +1,245 @@
/* General Body Styles */
/* assets/css/custom.css */
@import url('https://fonts.googleapis.com/css2?family=Mountains+of+Christmas:wght@700&family=Lato:wght@400;700&display=swap');
body { body {
font-family: 'Lato', sans-serif; background-color: #0a2e36; /* Dark teal background */
background-color: #F0F8FF; color: #ffffff; /* White text */
color: #292B2C; font-family: 'Poppins', sans-serif;
overflow-x: hidden; padding-top: 40px; /* Make space for garland */
} }
h1, h2, h3, .h1, .h2, .h3 { /* Headings */
font-family: 'Mountains of Christmas', cursive; h1, h2, h3, h4, h5, h6 {
font-weight: 700; 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;
}
.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 {
content: '';
position: fixed;
top: 10px;
left: -5%;
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, #ffff24 4px, transparent 5px),
radial-gradient(circle, #ff24ff 4px, transparent 5px);
background-size: 100px 20px;
background-position: 0 0, 20px 0, 40px 0, 60px 0, 80px 0;
background-repeat: repeat-x;
z-index: 1031;
animation: garland-animation 1.5s infinite;
}
@keyframes garland-animation {
50% {
filter: brightness(0.7);
}
}
/* Navbar */
.navbar {
background-color: rgba(10, 46, 54, 0.8) !important; /* Semi-transparent dark teal */
backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
} }
.navbar-brand { .navbar-brand {
font-family: 'Mountains of Christmas', cursive; color: #ffffff !important;
font-weight: 600;
} }
/* Main Content */
.display-4 {
font-weight: 700;
color: #ffffff;
}
.lead {
color: #ffffff;
}
/* Cards */
.card { .card {
border-radius: 0.5rem; background-color: rgba(255, 255, 255, 0.05);
border: none; border: none;
box-shadow: 0 4px 15px rgba(0,0,0,0.05); border-radius: 15px;
} }
.card-body {
color: #ffffff;
}
/* Forms */
.form-label {
color: #ffffff;
}
.form-control {
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);
color: #ffffff;
}
.form-control::placeholder {
color: rgba(255, 255, 255, 0.5);
}
/* Buttons */
.btn-primary { .btn-primary {
background-color: #D9534F; background-color: #ff6f61; /* Coral red */
border-color: #D9534F; border-color: #ff6f61;
transition: background-color 0.3s ease; font-weight: 600;
padding: 12px 30px;
border-radius: 50px;
transition: all 0.3s ease;
} }
.btn-primary:hover { .btn-primary:hover {
background-color: #c9302c; background-color: #e65a50;
border-color: #c9302c; border-color: #e65a50;
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(255, 111, 97, 0.2);
}
.btn-outline-secondary {
border-color: rgba(255, 255, 255, 0.8);
color: #ffffff;
font-weight: 600;
padding: 12px 30px;
border-radius: 50px;
transition: all 0.3s ease;
}
.btn-outline-secondary:hover {
background-color: rgba(255, 255, 255, 0.1);
color: #ffffff;
border-color: #ffffff;
} }
.btn-secondary { .btn-secondary {
background-color: #5CB85C; background-color: rgba(255, 255, 255, 0.15);
border-color: #5CB85C; border: none;
transition: background-color 0.3s ease; color: #ffffff;
} }
.btn-secondary:hover { /* Shopping List & Recipe Cards */
background-color: #4cae4c; #shopping-list-container .text-muted,
border-color: #4cae4c; #recipe-cards-container .text-muted {
color: #ffffff !important;
} }
.btn-danger { .recipe-card {
background-color: #F0AD4E; background-color: rgba(255, 255, 255, 0.1);
border-color: #F0AD4E; border-radius: 10px;
transition: background-color 0.3s ease; padding: 15px;
margin-bottom: 15px;
} }
.btn-danger:hover { /* Footer */
background-color: #ec971f; footer.bg-light {
border-color: #ec971f; background-color: transparent !important;
border-top: 1px solid rgba(255, 255, 255, 0.1);
color: #ffffff;
} }
#shopping-list-container { /* Snow Effect */
background-color: #fff; #snow-container {
padding: 2rem;
border-radius: 0.5rem;
min-height: 300px;
}
.ingredient-row {
display: flex;
gap: 0.5rem;
align-items: center;
}
.ingredient-row .form-control {
flex: 1;
}
/* Snowflakes animation */
.snowflake {
color: #fff;
font-size: 1em;
font-family: Arial, sans-serif;
text-shadow: 0 0 5px #000;
position: fixed; position: fixed;
top: -5%; top: 0;
z-index: -1; left: 0;
user-select: none; width: 100%;
height: 100%;
pointer-events: none;
z-index: 1032;
overflow: hidden;
}
.snowflake {
position: absolute;
top: -20px;
background: #fff;
border-radius: 50%;
opacity: 0.8;
pointer-events: none;
animation: fall linear infinite; animation: fall linear infinite;
} }
@keyframes fall { @keyframes fall {
to { to {
transform: translateY(105vh); transform: translateY(105vh);
opacity: 0;
} }
} }

View File

@ -1,147 +1,319 @@
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
// --- Snowflakes Effect --- // --- Snowflakes Effect ---
function createSnowflakes() { function createSnowflakes() {
const snowflakeContainer = document.body; const snowContainer = document.getElementById('snow-container');
for (let i = 0; i < 50; i++) { if (!snowContainer) return;
snowContainer.innerHTML = '';
const numberOfSnowflakes = 50;
for (let i = 0; i < numberOfSnowflakes; i++) {
const snowflake = document.createElement('div'); const snowflake = document.createElement('div');
snowflake.className = 'snowflake'; snowflake.className = 'snowflake';
snowflake.textContent = '❄';
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'; snowflake.style.left = Math.random() * 100 + 'vw';
snowflake.style.animationDuration = (Math.random() * 3 + 2) + 's'; // 2-5 seconds
snowflake.style.animationDelay = Math.random() * 2 + 's'; const animationDuration = Math.random() * 5 + 5; // 5 to 10 seconds
snowflake.style.opacity = Math.random(); snowflake.style.animationDuration = `${animationDuration}s`;
snowflake.style.fontSize = Math.random() * 10 + 10 + 'px';
snowflakeContainer.appendChild(snowflake); 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(); createSnowflakes();
// --- Calculator Logic --- // --- DOM Elements ---
const recipeNameInput = document.getElementById('recipeName');
const guestCountInput = document.getElementById('guestCount');
const ingredientsContainer = document.getElementById('ingredients-container'); const ingredientsContainer = document.getElementById('ingredients-container');
const addIngredientBtn = document.getElementById('add-ingredient'); const addIngredientBtn = document.getElementById('add-ingredient');
const calculateBtn = document.getElementById('calculate-btn'); const calculateBtn = document.getElementById('calculate-btn');
const newRecipeBtn = document.getElementById('new-recipe-btn');
const shoppingListContainer = document.getElementById('shopping-list-container'); const shoppingListContainer = document.getElementById('shopping-list-container');
const recipeCardsContainer = document.getElementById('recipe-cards-container');
let ingredientIndex = 1; // --- Core Functions ---
function addIngredientRow() { async function loadRecipes() {
ingredientIndex++; 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'); const row = document.createElement('div');
row.className = 'ingredient-row mb-2'; row.className = 'ingredient-row mb-2';
row.innerHTML = ` row.innerHTML = `
<input type="text" class="form-control" placeholder="Ingredient Name" aria-label="Ingredient Name"> <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"> <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"> <input type="text" class="form-control" placeholder="Unit (e.g., grams, ml)" aria-label="Unit" value="${ingredient.unit}">
<button type="button" class="btn btn-danger btn-sm remove-ingredient">&times;</button> <button type="button" class="btn btn-danger btn-sm remove-ingredient">&times;</button>
`; `;
ingredientsContainer.appendChild(row); ingredientsContainer.appendChild(row);
} }
if (addIngredientBtn) { function clearForm() {
addIngredientBtn.addEventListener('click', addIngredientRow); 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>
`;
} }
if (ingredientsContainer) { function renderRecipeCards(recipes) {
ingredientsContainer.addEventListener('click', function(e) { recipeCardsContainer.innerHTML = '';
if (e.target.classList.contains('remove-ingredient')) { if (!recipes || recipes.length === 0) {
e.target.closest('.ingredient-row').remove(); 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);
}); });
} }
if (calculateBtn) { function renderShoppingList(list) {
calculateBtn.addEventListener('click', function() { let html = '<h3>Общий список покупок</h3><hr>';
const recipeName = document.getElementById('recipeName').value || 'My Festive Recipe';
const guestCount = parseInt(document.getElementById('guestCount').value, 10);
if (isNaN(guestCount) || guestCount <= 0) {
alert('Please enter a valid number of guests.');
return;
}
const ingredients = [];
const rows = ingredientsContainer.querySelectorAll('.ingredient-row');
rows.forEach(row => {
const name = row.children[0].value;
const qty = parseFloat(row.children[1].value);
const unit = row.children[2].value;
if (name && !isNaN(qty) && qty > 0) {
ingredients.push({ name, qty, unit });
}
});
if (ingredients.length === 0) {
alert('Please add at least one ingredient.');
return;
}
// Calculate totals
const shoppingList = {};
ingredients.forEach(ing => {
const totalQty = ing.qty * guestCount;
const key = ing.name.toLowerCase().trim() + '_' + (ing.unit || '').toLowerCase().trim();
if (shoppingList[key]) {
shoppingList[key].qty += totalQty;
} else {
shoppingList[key] = {
name: ing.name,
qty: totalQty,
unit: ing.unit
};
}
});
// Render shopping list
renderShoppingList(recipeName, guestCount, Object.values(shoppingList));
});
}
function renderShoppingList(recipeName, guestCount, list) {
let html = `<h3>${recipeName} - Shopping List for ${guestCount} Guests</h3><hr>`;
if (list.length === 0) { if (list.length === 0) {
html += '<p>No ingredients to show. Please fill out the recipe form.</p>'; html += '<p>Нет ингредиентов для расчета.</p>';
} else { } else {
html += '<ul class="list-group list-group-flush">'; html += '<ul class="list-group list-group-flush">';
list.forEach(item => { 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"> html += `<li class="list-group-item d-flex justify-content-between align-items-center">
<span>${item.name}</span> <span>${item.name}</span>
<span class="badge bg-primary rounded-pill">${formatQuantity(item.qty)} ${item.unit}</span> <span class="badge bg-primary rounded-pill">${quantityStr} ${item.unit}</span>
</li>`; </li>`;
}); });
html += '</ul>'; html += '</ul>';
} }
shoppingListContainer.innerHTML = html; shoppingListContainer.innerHTML = html;
} }
function formatQuantity(qty) { function getRecipeDataFromForm() {
// Simple formatting, can be expanded for fractions const recipeName = recipeNameInput.value.trim();
return parseFloat(qty.toFixed(2)); const guests = parseInt(guestCountInput.value, 10) || 0;
}
const newRecipeBtn = document.getElementById('new-recipe-btn'); const ingredients = [];
const rows = ingredientsContainer.querySelectorAll('.ingredient-row');
if (newRecipeBtn) { rows.forEach(row => {
newRecipeBtn.addEventListener('click', function() { const name = row.querySelector('input[placeholder="Ingredient Name"]').value.trim();
document.getElementById('recipeName').value = ''; const qty = parseFloat(row.querySelector('input[placeholder="Qty"]').value);
document.getElementById('guestCount').value = ''; const unit = row.querySelector('input[placeholder="Unit (e.g., grams, ml)"]').value.trim();
ingredientsContainer.innerHTML = ''; if (name && !isNaN(qty) && qty > 0) {
addIngredientRow(); // Add a fresh row ingredients.push({ name, quantity: qty, unit });
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>
`;
}); });
if (recipeName && guests > 0 && ingredients.length > 0) {
return { name: recipeName, guests, ingredients };
}
return null;
} }
// Add one ingredient row by default // --- 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);
}
}
}
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) {
try {
const response = await fetch('api/save_recipe.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(recipeData)
});
const data = await response.json();
if (data.success) {
await loadRecipes(); // Reload all recipes to show the new one
clearForm();
} else {
alert('Failed to save recipe: ' + data.error);
}
} catch (error) {
alert('Error: ' + error.message);
}
} else {
alert('Please fill out the recipe name, guests, and at least one ingredient before saving.');
}
});
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);
if (existing) {
existing.qty += (ing.quantity || 0) * multiplier;
} else {
combinedIngredients.set(key, {
name: ing.name,
qty: (ing.quantity || 0) * multiplier,
unit: ing.unit
});
}
});
});
renderShoppingList(Array.from(combinedIngredients.values()));
} catch(error) {
alert('Calculation Error: ' + error.message);
}
});
// --- Initial State ---
addIngredientRow(); addIngredientRow();
loadRecipes();
}); });

Binary file not shown.

After

Width:  |  Height:  |  Size: 983 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

26
db/migrate.php Normal file
View File

@ -0,0 +1,26 @@
<?php
require_once __DIR__ . '/config.php';
function run_migrations() {
$pdo = db();
$migrationsDir = __DIR__ . '/migrations';
$files = glob($migrationsDir . '/*.sql');
sort($files);
foreach ($files as $file) {
echo "Running migration: " . basename($file) . "\n";
$sql = file_get_contents($file);
try {
$pdo->exec($sql);
echo "Success.\n";
} catch (PDOException $e) {
echo "Error: " . $e->getMessage() . "\n";
// A simple way to track which migrations have been run is needed
// For now, we'll just stop on error.
exit(1);
}
}
}
run_migrations();

View File

@ -0,0 +1,16 @@
CREATE TABLE IF NOT EXISTS `recipes` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(255) NOT NULL,
`guests` INT NOT NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `ingredients` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`recipe_id` INT NOT NULL,
`name` VARCHAR(255) NOT NULL,
`quantity` FLOAT NOT NULL,
`unit` VARCHAR(50) NOT NULL,
FOREIGN KEY (`recipe_id`) REFERENCES `recipes`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View File

@ -23,6 +23,9 @@
<!-- Styles --> <!-- Styles -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<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="assets/css/custom.css?v=<?php echo time(); ?>"> <link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
</head> </head>
@ -30,22 +33,44 @@
<div id="snow-container"></div> <div id="snow-container"></div>
<nav class="navbar navbar-expand-lg navbar-light bg-light shadow-sm"> <!-- 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"> <div class="container">
<a class="navbar-brand" href="#">Christmas Recipe Calculator</a> <a class="navbar-brand" href="#">Recipe Calculator</a>
</div> </div>
</nav> </nav>
<main class="container my-5"> <main class="container my-5">
<div class="text-center mb-5"> <div class="text-center mb-5">
<h1 class="display-4">Holiday Recipe Planner</h1> <h1 class="display-4">Hey, it's Christmas time!</h1>
<p class="lead">Enter a recipe for one person, set your guest count, and we'll make your shopping list!</p> <p class="lead">Let's get your holiday recipes sorted.</p>
</div> </div>
<div class="row g-5"> <div class="row g-5">
<!-- Left Column: Recipe Input --> <!-- Left Column: Recipe Input -->
<div class="col-lg-6"> <div class="col-lg-6">
<div class="card p-4"> <div class="card p-4 shadow">
<h2 class="mb-4">Your Recipe</h2> <h2 class="mb-4">Your Recipe</h2>
<form id="recipe-form"> <form id="recipe-form">
<div class="mb-3"> <div class="mb-3">
@ -53,7 +78,7 @@
<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">
</div> </div>
<hr> <hr class="my-4 border-secondary">
<h3 class="h5 mb-3">Ingredients (for 1 person)</h3> <h3 class="h5 mb-3">Ingredients (for 1 person)</h3>
<div id="ingredients-container"> <div id="ingredients-container">
@ -61,16 +86,16 @@
</div> </div>
<button type="button" id="add-ingredient" class="btn btn-secondary btn-sm mt-2">+ Add Ingredient</button> <button type="button" id="add-ingredient" class="btn btn-secondary btn-sm mt-2">+ Add Ingredient</button>
<hr class="my-4"> <hr class="my-4 border-secondary">
<div class="mb-3"> <div class="mb-3">
<label for="guestCount" class="form-label">How many guests are coming?</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">
</div> </div>
<div class="d-grid gap-2 d-md-flex mt-4"> <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 btn-lg">Calculate Shopping List</button> <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 btn-lg">New Recipe</button> <button type="button" id="new-recipe-btn" class="btn btn-outline-secondary">New Recipe</button>
</div> </div>
</form> </form>
</div> </div>
@ -78,7 +103,7 @@
<!-- Right Column: Shopping List --> <!-- Right Column: Shopping List -->
<div class="col-lg-6"> <div class="col-lg-6">
<div class="card"> <div class="card shadow">
<div class="card-body" id="shopping-list-container"> <div class="card-body" id="shopping-list-container">
<div class="text-center text-muted p-5"> <div class="text-center text-muted p-5">
<h3 class="h4">Your Shopping List</h3> <h3 class="h4">Your Shopping List</h3>
@ -88,9 +113,22 @@
</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> </main>
<footer class="text-center py-4 mt-5 bg-light"> <footer class="text-center py-4 mt-5">
<p class="mb-0">&copy; <?php echo date("Y"); ?> Christmas Recipe Calculator. Happy Holidays!</p> <p class="mb-0">&copy; <?php echo date("Y"); ?> Christmas Recipe Calculator. Happy Holidays!</p>
</footer> </footer>