This commit is contained in:
Flatlogic Bot 2025-11-30 19:38:56 +00:00
parent 71ee90fe50
commit 7368c83e9a
7 changed files with 228 additions and 45 deletions

View File

@ -2,62 +2,93 @@
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
$data = json_decode(file_get_contents('php://input'), true);
// The request is now multipart/form-data, so we use $_POST and $_FILES
$data = $_POST;
$files = $_FILES;
if (!$data || !isset($data['name']) || !isset($data['guests']) || !isset($data['ingredients'])) {
if (!isset($data['name']) || !isset($data['guests']) || !isset($data['ingredients'])) {
echo json_encode(['success' => false, 'error' => 'Invalid input.']);
exit;
}
$ingredients = json_decode($data['ingredients'], true);
if (json_last_error() !== JSON_ERROR_NONE) {
echo json_encode(['success' => false, 'error' => 'Invalid ingredients format.']);
exit;
}
$pdo = db();
$imageUrl = null;
try {
// Handle file upload
if (isset($files['image']) && $files['image']['error'] === UPLOAD_ERR_OK) {
$uploadDir = __DIR__ . '/../assets/images/recipes/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0777, true);
}
$filename = uniqid() . '-' . basename($files['image']['name']);
$uploadFile = $uploadDir . $filename;
if (move_uploaded_file($files['image']['tmp_name'], $uploadFile)) {
$imageUrl = 'assets/images/recipes/' . $filename;
} else {
throw new Exception('Failed to move uploaded file.');
}
}
$pdo->beginTransaction();
if (isset($data['id']) && !empty($data['id'])) {
// Update existing recipe
$recipeId = $data['id'];
$category = !empty($data['category']) ? $data['category'] : 'No category';
$stmt = $pdo->prepare("UPDATE recipes SET name = ?, guests = ?, category = ? WHERE id = ?");
$stmt->execute([$data['name'], $data['guests'], $category, $recipeId]);
// Fetch existing image URL if a new one isn't uploaded
if ($imageUrl === null) {
$stmt = $pdo->prepare("SELECT image_url FROM recipes WHERE id = ?");
$stmt->execute([$recipeId]);
$existing = $stmt->fetch();
$imageUrl = $existing ? $existing['image_url'] : null;
}
$stmt = $pdo->prepare("UPDATE recipes SET name = ?, guests = ?, category = ?, image_url = ? WHERE id = ?");
$stmt->execute([$data['name'], $data['guests'], $category, $imageUrl, $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
$category = !empty($data['category']) ? $data['category'] : 'No category';
$stmt = $pdo->prepare("INSERT INTO recipes (name, guests, category) VALUES (?, ?, ?)");
$stmt->execute([$data['name'], $data['guests'], $category]);
$stmt = $pdo->prepare("INSERT INTO recipes (name, guests, category, image_url) VALUES (?, ?, ?, ?)");
$stmt->execute([$data['name'], $data['guests'], $category, $imageUrl]);
$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']]);
}
// Insert ingredients
$stmt = $pdo->prepare("INSERT INTO ingredients (recipe_id, name, quantity, unit) VALUES (?, ?, ?, ?)");
foreach ($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
// Fetch the newly created/updated 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;
$recipe['ingredients'] = $stmt->fetchAll();
echo json_encode(['success' => true, 'recipe' => $recipe]);
} catch (PDOException $e) {
} catch (Exception $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}

View File

@ -3,6 +3,7 @@ body {
background-color: #142E35; /* Dark green background */
color: #ffffff; /* White text */
font-family: 'Poppins', sans-serif;
padding-top: 50px;
}
/* Headings */
@ -27,7 +28,7 @@ body::before {
width: 110%;
height: 20px;
background:
radial-gradient(circle, #C83434 4px, transparent 5px),
radial-gradient(circle, #de4950 4px, transparent 5px),
radial-gradient(circle, #142E35 4px, transparent 5px),
radial-gradient(circle, #FFAFCA 4px, transparent 5px),
radial-gradient(circle, #ffff24 4px, transparent 5px),
@ -103,8 +104,8 @@ body::before {
/* Buttons */
.btn-primary, .btn-danger {
background-color: #C83434 !important; /* Coral red */
border-color: #C83434 !important;
background-color: #de4950 !important; /* Coral red */
border-color: #de4950 !important;
font-weight: 600;
padding: 12px 30px;
transition: all 0.3s ease;
@ -114,7 +115,7 @@ body::before {
background-color: #a02929 !important;
border-color: #a02929 !important;
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(200, 52, 52, 0.2);
box-shadow: 0 4px 15px rgba(222, 73, 80, 0.2);
}
.btn-outline-secondary {
@ -253,8 +254,8 @@ animation: fall linear infinite;
}
.form-check-input:checked {
background-color: #C83434;
border-color: #C83434;
background-color: #de4950;
border-color: #de4950;
}
.form-check-input:focus {
@ -386,5 +387,49 @@ animation: fall linear infinite;
padding: .25rem .5rem;
}
#christmas-decorations-right {
position: fixed;
top: 0px;
right: 250px;
width: 200px;
z-index: 1033;
}
#christmas-decorations-right img {
width: 100%;
}
/* Recipe Card Image */
.card .card-img-top {
height: 200px;
object-fit: cover;
}
/* Overlay category label */
.card .recipe-category-label {
top: 15px;
right: 15px;
background-color: rgba(20, 46, 53, 0.8);
backdrop-filter: blur(5px);
}
/* Recipe Card Animation */
.recipe-card-enter {
animation: fade-in 0.5s ease-in-out;
}
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

View File

@ -20,12 +20,11 @@ const app = {
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) {
async saveRecipe(formData) {
try {
const response = await fetch('api/save_recipe.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(recipeData)
body: formData
});
return await response.json();
} catch (error) {
@ -61,14 +60,23 @@ const app = {
return;
}
recipes.forEach(recipe => {
recipes.forEach((recipe, index) => {
const cardCol = document.createElement('div');
cardCol.className = 'col-12 mb-3';
cardCol.className = 'col-12 mb-3 recipe-card-enter';
cardCol.setAttribute('data-id', recipe.id);
cardCol.style.animationDelay = `${index * 0.1}s`;
const card = document.createElement('div');
card.className = 'card h-100';
if (recipe.image_url) {
const img = document.createElement('img');
img.src = recipe.image_url;
img.className = 'card-img-top';
img.alt = recipe.name;
card.appendChild(img);
}
if (recipe.category) {
const categoryLabel = document.createElement('div');
categoryLabel.className = 'recipe-category-label';
@ -90,6 +98,7 @@ const app = {
const buttonGroup = document.createElement('div');
buttonGroup.className = 'mt-auto pt-2';
buttonGroup.innerHTML = `
<button class="btn btn-light btn-sm view-recipe"><i class="bi bi-eye"></i> View</button>
<button class="btn btn-light btn-sm edit-recipe"><i class="bi bi-pencil"></i> Edit</button>
<button class="btn btn-danger btn-sm delete-recipe" title="Delete"><i class="bi bi-trash"></i></button>
`;
@ -273,6 +282,7 @@ const app = {
app.dom.recipeIdInput.value = '';
app.dom.recipeNameInput.value = '';
app.dom.recipeCategoryInput.value = '';
app.dom.recipeImage.value = '';
app.dom.guestCountInput.value = '1';
app.dom.ingredientsContainer.innerHTML = '';
app.ui.addIngredientRow();
@ -303,6 +313,42 @@ const app = {
app.dom.recipeNameInput.focus();
},
populateViewModal(recipeId) {
const recipe = app.state.recipes.find(r => r.id == recipeId);
if (!recipe) return;
const modal = document.getElementById('view-recipe-modal');
const img = modal.querySelector('img');
if (img) {
img.remove();
}
if (recipe.image_url) {
const newImg = document.createElement('img');
newImg.src = recipe.image_url;
newImg.className = 'card-img-top mb-3';
newImg.alt = recipe.name;
modal.querySelector('.modal-body').prepend(newImg);
}
document.getElementById('view-recipe-name').textContent = recipe.name;
document.getElementById('view-recipe-category').textContent = recipe.category || 'No category';
document.getElementById('view-recipe-guests').textContent = recipe.guests;
const ingredientsList = document.getElementById('view-recipe-ingredients');
ingredientsList.innerHTML = '';
if (recipe.ingredients) {
recipe.ingredients.forEach(ing => {
const li = document.createElement('li');
li.className = 'list-group-item';
li.textContent = `${ing.name} - ${ing.quantity} ${ing.unit}`;
ingredientsList.appendChild(li);
});
}
const viewRecipeModal = new bootstrap.Modal(modal);
viewRecipeModal.show();
},
getRecipeDataFromForm() {
const recipeName = app.dom.recipeNameInput.value.trim();
const guests = parseInt(app.dom.guestCountInput.value, 10) || 0;
@ -374,12 +420,23 @@ const app = {
return;
}
const formData = new FormData();
formData.append('name', recipeData.name);
formData.append('guests', recipeData.guests);
formData.append('category', recipeData.category);
formData.append('ingredients', JSON.stringify(recipeData.ingredients));
const recipeId = app.dom.recipeIdInput.value;
if (recipeId) {
recipeData.id = recipeId;
formData.append('id', recipeId);
}
const data = await app.api.saveRecipe(recipeData);
const imageInput = document.getElementById('recipeImage');
if (imageInput.files[0]) {
formData.append('image', imageInput.files[0]);
}
const data = await app.api.saveRecipe(formData);
if (data.success && data.recipe) {
app.api.getRecipes().then(() => {
@ -415,6 +472,10 @@ const app = {
if (target.classList.contains('edit-recipe')) {
app.ui.populateFormForEdit(recipeId);
}
if (target.classList.contains('view-recipe')) {
app.ui.populateViewModal(recipeId);
}
});
app.dom.shoppingListContainer.addEventListener('click', function(e) {
@ -652,6 +713,7 @@ const app = {
recipeCardsContainer: document.getElementById('recipe-cards-container'),
recipeIdInput: document.getElementById('recipeId'),
recipeCategoryInput: document.getElementById('recipeCategory'),
recipeImage: document.getElementById('recipeImage'),
recipeSearchInput: document.getElementById('recipe-search'),
categoryFilters: document.getElementById('category-filters'),
addProductBtn: document.getElementById('add-product-btn'),

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

View File

@ -3,24 +3,40 @@ require_once __DIR__ . '/config.php';
function run_migrations() {
$pdo = db();
// Ensure migrations table exists
$pdo->exec("CREATE TABLE IF NOT EXISTS migrations (migration VARCHAR(255) NOT NULL, PRIMARY KEY (migration))");
// Get executed migrations
$executedMigrations = $pdo->query("SELECT migration FROM migrations")->fetchAll(PDO::FETCH_COLUMN);
$migrationsDir = __DIR__ . '/migrations';
$files = glob($migrationsDir . '/*.sql');
sort($files);
foreach ($files as $file) {
echo "Running migration: " . basename($file) . "\n";
$migrationName = basename($file);
if (in_array($migrationName, $executedMigrations)) {
continue;
}
echo "Running migration: " . $migrationName . "\n";
$sql = file_get_contents($file);
try {
$pdo->exec($sql);
// Record migration
$stmt = $pdo->prepare("INSERT INTO migrations (migration) VALUES (?)");
$stmt->execute([$migrationName]);
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);
}
}
echo "All migrations have been run.\n";
}
run_migrations();

View File

@ -0,0 +1 @@
ALTER TABLE recipes ADD COLUMN image_url VARCHAR(255) DEFAULT NULL;

View File

@ -32,19 +32,19 @@
</head>
<body>
<div id="christmas-decorations-right">
<img src="assets/pasted-20251130-191602-0ffc27f4.png" alt="Christmas Decorations" />
</div>
<div id="snow-container"></div>
<nav class="navbar navbar-expand-lg navbar-dark shadow-sm">
<div class="container">
<a class="navbar-brand" href="#">Recipe Calculator</a>
</div>
</nav>
<main class="container my-5">
<div class="text-center mb-5">
<h1 class="display-4">Hey, it's Christmas time!</h1>
<h1 class="display-4 mt-4">Hey, it's Christmas time!</h1>
<p class="lead">Let's get your holiday recipes sorted.</p>
</div>
@ -121,6 +121,10 @@
<option value="Appetizers">Appetizers</option>
</select>
</div>
<div class="mb-3">
<label for="recipeImage" class="form-label">Recipe Image</label>
<input type="file" class="form-control" id="recipeImage">
</div>
<hr class="my-4 border-secondary">
@ -211,6 +215,30 @@
</div>
</div>
<!-- Modal View Recipe -->
<div class="modal fade" id="view-recipe-modal" tabindex="-1" aria-labelledby="view-recipe-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="view-recipe-modal-label">View Recipe</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<h2 id="view-recipe-name"></h2>
<p><strong>Category:</strong> <span id="view-recipe-category"></span></p>
<p><strong>Serves:</strong> <span id="view-recipe-guests"></span></p>
<hr>
<h3>Ingredients</h3>
<ul id="view-recipe-ingredients" class="list-group">
</ul>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<footer class="text-center py-4 mt-5">
<p class="mb-0">&copy; <?php echo date("Y"); ?> Christmas Recipe Calculator. Happy Holidays!</p>
</footer>