From 85138a657583788f0cdec396ba2847b721e3b7a7 Mon Sep 17 00:00:00 2001 From: Foohoo Date: Sun, 10 Aug 2025 14:16:30 +0100 Subject: [PATCH] add feature to delete recipes when logged in --- backend/routes/recipes.js | 55 ++++++++++- docker-compose.yml | 2 +- frontend/src/components/RecipeList.tsx | 31 ++++++- frontend/src/components/RecipeModal.css | 117 +++++++++++++++++++++++- frontend/src/components/RecipeModal.tsx | 88 ++++++++++++++++-- frontend/src/services/api.ts | 1 + 6 files changed, 276 insertions(+), 18 deletions(-) diff --git a/backend/routes/recipes.js b/backend/routes/recipes.js index 8fb8b48..fb8fcd5 100644 --- a/backend/routes/recipes.js +++ b/backend/routes/recipes.js @@ -177,15 +177,62 @@ router.put('/:id', async (req, res) => { } }); -// Delete recipe -router.delete('/:id', async (req, res) => { +// Delete recipe (protected route) +router.delete('/:id', authenticateToken, async (req, res) => { try { - const recipe = await Recipe.findByIdAndDelete(req.params.id); + const recipeId = req.params.id; + + // Find the recipe first to check ownership + const recipe = await Recipe.findById(recipeId); if (!recipe) { return res.status(404).json({ error: 'Recipe not found' }); } - res.json({ message: 'Recipe deleted successfully' }); + + // Check if user owns the recipe (only recipe creator can delete) + if (recipe.createdBy.toString() !== req.userId) { + return res.status(403).json({ error: 'You can only delete recipes you created' }); + } + + // Delete the recipe + await Recipe.findByIdAndDelete(recipeId); + + // Remove recipe from all user selections + const UserSelection = require('../models/UserSelection'); + await UserSelection.updateMany( + { 'selectedRecipes.recipeId': recipeId }, + { + $pull: { + selectedRecipes: { recipeId: recipeId } + } + } + ); + + // Also clean up aggregated ingredients that reference this recipe + await UserSelection.updateMany( + { 'aggregatedIngredients.recipes.recipeId': recipeId }, + { + $pull: { + 'aggregatedIngredients.$[].recipes': { recipeId: recipeId } + } + } + ); + + // Remove empty aggregated ingredients (those with no recipes left) + await UserSelection.updateMany( + {}, + { + $pull: { + aggregatedIngredients: { recipes: { $size: 0 } } + } + } + ); + + res.json({ + message: 'Recipe deleted successfully and removed from all user menus', + deletedRecipeId: recipeId + }); } catch (error) { + console.error('Error deleting recipe:', error); res.status(500).json({ error: error.message }); } }); diff --git a/docker-compose.yml b/docker-compose.yml index ef331a9..ec41733 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -72,7 +72,7 @@ services: - "${MONGO_EXPRESS_PORT:-8081}:8081" environment: ME_CONFIG_MONGODB_URL: mongodb://mongodb:27017/ - ME_CONFIG_BASICAUTH: false + ME_CONFIG_BASICAUTH: "false" depends_on: mongodb: condition: service_healthy diff --git a/frontend/src/components/RecipeList.tsx b/frontend/src/components/RecipeList.tsx index 3db50dc..fb7ade7 100644 --- a/frontend/src/components/RecipeList.tsx +++ b/frontend/src/components/RecipeList.tsx @@ -52,13 +52,37 @@ const RecipeList: React.FC = ({ onSelectionUpdate }) => { const handleAddToSelection = async (recipeId: string) => { try { - const response = await selectionsAPI.addRecipe(recipeId, 1); - setUserSelection(response.data as UserSelection); + const response = await selectionsAPI.addRecipe(recipeId); + setUserSelection(response.data as UserSelection); if (onSelectionUpdate) { onSelectionUpdate(response.data as UserSelection); } } catch (error: any) { - setError(error.response?.data?.error || 'Failed to add recipe to selection'); + console.error('Error adding recipe to selection:', error); + } + }; + + const handleRecipeDeleted = (deletedRecipeId: string) => { + // Remove the deleted recipe from the recipes list + setRecipes(prevRecipes => prevRecipes.filter(recipe => recipe._id !== deletedRecipeId)); + + // Close the modal if the deleted recipe was being viewed + if (selectedRecipe && selectedRecipe._id === deletedRecipeId) { + setSelectedRecipe(null); + } + + // Update user selection to remove the deleted recipe (this should already be handled by backend) + if (userSelection) { + const updatedSelection = { + ...userSelection, + selectedRecipes: userSelection.selectedRecipes.filter( + selection => selection.recipeId._id !== deletedRecipeId + ) + }; + setUserSelection(updatedSelection); + if (onSelectionUpdate) { + onSelectionUpdate(updatedSelection); + } } }; @@ -183,6 +207,7 @@ const RecipeList: React.FC = ({ onSelectionUpdate }) => { recipe={selectedRecipe} onClose={handleCloseModal} onAddToSelection={handleAddToSelection} + onRecipeDeleted={handleRecipeDeleted} isSelected={isRecipeSelected(selectedRecipe._id)} selectedQuantity={getSelectedQuantity(selectedRecipe._id)} /> diff --git a/frontend/src/components/RecipeModal.css b/frontend/src/components/RecipeModal.css index c05d41a..e1eb05e 100644 --- a/frontend/src/components/RecipeModal.css +++ b/frontend/src/components/RecipeModal.css @@ -218,8 +218,121 @@ .modal-footer { padding: 20px; - border-top: 1px solid #e0e0e0; - background: #f8f9fa; + border-top: 1px solid #eee; + display: flex; + justify-content: center; +} + +.modal-footer-buttons { + display: flex; + gap: 12px; + align-items: center; + width: 100%; + justify-content: space-between; +} + +.btn-delete { + background: #dc3545; + color: white; + border: none; + padding: 10px 20px; + border-radius: 8px; + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.btn-delete:hover:not(:disabled) { + background: #c82333; + transform: translateY(-1px); +} + +.btn-delete:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + +.delete-confirm-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.8); + display: flex; + align-items: center; + justify-content: center; + z-index: 1100; + padding: 20px; +} + +.delete-confirm-dialog { + background: white; + border-radius: 12px; + padding: 24px; + max-width: 400px; + width: 100%; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); +} + +.delete-confirm-dialog h3 { + margin: 0 0 16px 0; + color: #dc3545; + font-size: 1.25rem; +} + +.delete-confirm-dialog p { + margin: 0 0 12px 0; + color: #333; + line-height: 1.5; +} + +.delete-warning { + color: #666; + font-size: 0.9rem; + font-style: italic; + margin-bottom: 20px !important; +} + +.delete-confirm-buttons { + display: flex; + gap: 12px; + justify-content: flex-end; +} + +.delete-confirm-buttons .btn { + padding: 8px 16px; + border-radius: 6px; + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; + border: none; + transition: all 0.2s ease; +} + +.delete-confirm-buttons .btn-secondary { + background: #6c757d; + color: white; +} + +.delete-confirm-buttons .btn-secondary:hover:not(:disabled) { + background: #5a6268; +} + +.delete-confirm-buttons .btn-danger { + background: #dc3545; + color: white; +} + +.delete-confirm-buttons .btn-danger:hover:not(:disabled) { + background: #c82333; +} + +.delete-confirm-buttons .btn:disabled { + opacity: 0.6; + cursor: not-allowed; } .btn-large { diff --git a/frontend/src/components/RecipeModal.tsx b/frontend/src/components/RecipeModal.tsx index 242887f..ab1c0f3 100644 --- a/frontend/src/components/RecipeModal.tsx +++ b/frontend/src/components/RecipeModal.tsx @@ -1,11 +1,13 @@ -import React from 'react'; -import { Recipe } from '../services/api'; +import React, { useState } from 'react'; +import { Recipe, recipesAPI } from '../services/api'; +import { useAuth } from '../context/AuthContext'; import './RecipeModal.css'; interface RecipeModalProps { recipe: Recipe; onClose: () => void; onAddToSelection: (recipeId: string) => void; + onRecipeDeleted?: (recipeId: string) => void; isSelected: boolean; selectedQuantity: number; } @@ -14,15 +16,45 @@ const RecipeModal: React.FC = ({ recipe, onClose, onAddToSelection, + onRecipeDeleted, isSelected, selectedQuantity, }) => { + const { user } = useAuth(); + const [isDeleting, setIsDeleting] = useState(false); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const handleBackdropClick = (e: React.MouseEvent) => { if (e.target === e.currentTarget) { onClose(); } }; + const handleDeleteClick = () => { + setShowDeleteConfirm(true); + }; + + const handleDeleteConfirm = async () => { + setIsDeleting(true); + try { + await recipesAPI.delete(recipe._id); + onRecipeDeleted?.(recipe._id); + onClose(); + } catch (error) { + console.error('Error deleting recipe:', error); + alert('Failed to delete recipe. Please try again.'); + } finally { + setIsDeleting(false); + setShowDeleteConfirm(false); + } + }; + + const handleDeleteCancel = () => { + setShowDeleteConfirm(false); + }; + + // Check if current user is the creator of this recipe + const canDelete = user && recipe.createdBy === user.id; + const getDifficultyColor = (difficulty: string) => { switch (difficulty) { case 'easy': return '#4CAF50'; @@ -131,13 +163,53 @@ const RecipeModal: React.FC = ({
- +
+ {canDelete && ( + + )} + +
+ + {/* Delete Confirmation Dialog */} + {showDeleteConfirm && ( +
+
+

Delete Recipe

+

Are you sure you want to delete "{recipe.title}"?

+

+ This action cannot be undone. The recipe will be removed from all users' menus. +

+
+ + +
+
+
+ )} ); diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 651a6a6..1b84f6f 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -51,6 +51,7 @@ export interface Recipe { difficulty: 'easy' | 'medium' | 'hard'; imageUrl: string; createdAt: string; + createdBy?: string; } export interface User {