14
This commit is contained in:
parent
71ee90fe50
commit
7368c83e9a
@ -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()]);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -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'),
|
||||
|
||||
BIN
assets/pasted-20251130-191602-0ffc27f4.png
Normal file
BIN
assets/pasted-20251130-191602-0ffc27f4.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 117 KiB |
@ -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();
|
||||
|
||||
|
||||
1
db/migrations/003_add_image_to_recipes.sql
Normal file
1
db/migrations/003_add_image_to_recipes.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER TABLE recipes ADD COLUMN image_url VARCHAR(255) DEFAULT NULL;
|
||||
40
index.php
40
index.php
@ -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">© <?php echo date("Y"); ?> Christmas Recipe Calculator. Happy Holidays!</p>
|
||||
</footer>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user