diff --git a/api/save_recipe.php b/api/save_recipe.php index e3d4581..8fb2603 100644 --- a/api/save_recipe.php +++ b/api/save_recipe.php @@ -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) { - $pdo->rollBack(); +} catch (Exception $e) { + if ($pdo->inTransaction()) { + $pdo->rollBack(); + } echo json_encode(['success' => false, 'error' => $e->getMessage()]); -} +} \ No newline at end of file diff --git a/assets/css/custom.css b/assets/css/custom.css index 4831251..e408343 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -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); + } +} + + + diff --git a/assets/js/main.js b/assets/js/main.js index 654379d..70f8a5f 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -20,12 +20,11 @@ const app = { app.dom.recipeCardsContainer.innerHTML = '

Could not connect to the server.

'; } }, - 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 = ` + `; @@ -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'), diff --git a/assets/pasted-20251130-191602-0ffc27f4.png b/assets/pasted-20251130-191602-0ffc27f4.png new file mode 100644 index 0000000..ecf4ef6 Binary files /dev/null and b/assets/pasted-20251130-191602-0ffc27f4.png differ diff --git a/db/migrate.php b/db/migrate.php index 73ea3bb..61a9901 100644 --- a/db/migrate.php +++ b/db/migrate.php @@ -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(); - +run_migrations(); \ No newline at end of file diff --git a/db/migrations/003_add_image_to_recipes.sql b/db/migrations/003_add_image_to_recipes.sql new file mode 100644 index 0000000..70d2066 --- /dev/null +++ b/db/migrations/003_add_image_to_recipes.sql @@ -0,0 +1 @@ +ALTER TABLE recipes ADD COLUMN image_url VARCHAR(255) DEFAULT NULL; \ No newline at end of file diff --git a/index.php b/index.php index 2b5c707..319f4f5 100644 --- a/index.php +++ b/index.php @@ -32,19 +32,19 @@ +
+ Christmas Decorations +
+
- +
-

Hey, it's Christmas time!

+

Hey, it's Christmas time!

Let's get your holiday recipes sorted.

@@ -121,6 +121,10 @@ +
+ + +

@@ -211,6 +215,30 @@ + + +