add feature to delete recipes when logged in

This commit is contained in:
2025-08-10 14:16:30 +01:00
parent fbb6ce0441
commit 85138a6575
6 changed files with 276 additions and 18 deletions

View File

@@ -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 });
}
});

View File

@@ -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

View File

@@ -52,13 +52,37 @@ const RecipeList: React.FC<RecipeListProps> = ({ onSelectionUpdate }) => {
const handleAddToSelection = async (recipeId: string) => {
try {
const response = await selectionsAPI.addRecipe(recipeId, 1);
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<RecipeListProps> = ({ onSelectionUpdate }) => {
recipe={selectedRecipe}
onClose={handleCloseModal}
onAddToSelection={handleAddToSelection}
onRecipeDeleted={handleRecipeDeleted}
isSelected={isRecipeSelected(selectedRecipe._id)}
selectedQuantity={getSelectedQuantity(selectedRecipe._id)}
/>

View File

@@ -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 {

View File

@@ -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<RecipeModalProps> = ({
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,6 +163,16 @@ const RecipeModal: React.FC<RecipeModalProps> = ({
</div>
<div className="modal-footer">
<div className="modal-footer-buttons">
{canDelete && (
<button
className="btn btn-danger btn-delete"
onClick={handleDeleteClick}
disabled={isDeleting}
>
{isDeleting ? 'Deleting...' : 'Delete Recipe'}
</button>
)}
<button
className={`btn ${isSelected ? 'btn-success' : 'btn-primary'} btn-large`}
onClick={() => onAddToSelection(recipe._id)}
@@ -139,6 +181,36 @@ const RecipeModal: React.FC<RecipeModalProps> = ({
</button>
</div>
</div>
{/* Delete Confirmation Dialog */}
{showDeleteConfirm && (
<div className="delete-confirm-overlay">
<div className="delete-confirm-dialog">
<h3>Delete Recipe</h3>
<p>Are you sure you want to delete "{recipe.title}"?</p>
<p className="delete-warning">
This action cannot be undone. The recipe will be removed from all users' menus.
</p>
<div className="delete-confirm-buttons">
<button
className="btn btn-secondary"
onClick={handleDeleteCancel}
disabled={isDeleting}
>
Cancel
</button>
<button
className="btn btn-danger"
onClick={handleDeleteConfirm}
disabled={isDeleting}
>
{isDeleting ? 'Deleting...' : 'Delete Recipe'}
</button>
</div>
</div>
</div>
)}
</div>
</div>
);
};

View File

@@ -51,6 +51,7 @@ export interface Recipe {
difficulty: 'easy' | 'medium' | 'hard';
imageUrl: string;
createdAt: string;
createdBy?: string;
}
export interface User {