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 // Delete recipe (protected route)
router.delete('/:id', async (req, res) => { router.delete('/:id', authenticateToken, async (req, res) => {
try { 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) { if (!recipe) {
return res.status(404).json({ error: 'Recipe not found' }); 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) { } catch (error) {
console.error('Error deleting recipe:', error);
res.status(500).json({ error: error.message }); res.status(500).json({ error: error.message });
} }
}); });

View File

@@ -72,7 +72,7 @@ services:
- "${MONGO_EXPRESS_PORT:-8081}:8081" - "${MONGO_EXPRESS_PORT:-8081}:8081"
environment: environment:
ME_CONFIG_MONGODB_URL: mongodb://mongodb:27017/ ME_CONFIG_MONGODB_URL: mongodb://mongodb:27017/
ME_CONFIG_BASICAUTH: false ME_CONFIG_BASICAUTH: "false"
depends_on: depends_on:
mongodb: mongodb:
condition: service_healthy condition: service_healthy

View File

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

View File

@@ -218,8 +218,121 @@
.modal-footer { .modal-footer {
padding: 20px; padding: 20px;
border-top: 1px solid #e0e0e0; border-top: 1px solid #eee;
background: #f8f9fa; 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 { .btn-large {

View File

@@ -1,11 +1,13 @@
import React from 'react'; import React, { useState } from 'react';
import { Recipe } from '../services/api'; import { Recipe, recipesAPI } from '../services/api';
import { useAuth } from '../context/AuthContext';
import './RecipeModal.css'; import './RecipeModal.css';
interface RecipeModalProps { interface RecipeModalProps {
recipe: Recipe; recipe: Recipe;
onClose: () => void; onClose: () => void;
onAddToSelection: (recipeId: string) => void; onAddToSelection: (recipeId: string) => void;
onRecipeDeleted?: (recipeId: string) => void;
isSelected: boolean; isSelected: boolean;
selectedQuantity: number; selectedQuantity: number;
} }
@@ -14,15 +16,45 @@ const RecipeModal: React.FC<RecipeModalProps> = ({
recipe, recipe,
onClose, onClose,
onAddToSelection, onAddToSelection,
onRecipeDeleted,
isSelected, isSelected,
selectedQuantity, selectedQuantity,
}) => { }) => {
const { user } = useAuth();
const [isDeleting, setIsDeleting] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const handleBackdropClick = (e: React.MouseEvent) => { const handleBackdropClick = (e: React.MouseEvent) => {
if (e.target === e.currentTarget) { if (e.target === e.currentTarget) {
onClose(); 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) => { const getDifficultyColor = (difficulty: string) => {
switch (difficulty) { switch (difficulty) {
case 'easy': return '#4CAF50'; case 'easy': return '#4CAF50';
@@ -131,13 +163,53 @@ const RecipeModal: React.FC<RecipeModalProps> = ({
</div> </div>
<div className="modal-footer"> <div className="modal-footer">
<button <div className="modal-footer-buttons">
className={`btn ${isSelected ? 'btn-success' : 'btn-primary'} btn-large`} {canDelete && (
onClick={() => onAddToSelection(recipe._id)} <button
> className="btn btn-danger btn-delete"
{isSelected ? `Added to Menu (${selectedQuantity})` : 'Add to Menu'} onClick={handleDeleteClick}
</button> disabled={isDeleting}
>
{isDeleting ? 'Deleting...' : 'Delete Recipe'}
</button>
)}
<button
className={`btn ${isSelected ? 'btn-success' : 'btn-primary'} btn-large`}
onClick={() => onAddToSelection(recipe._id)}
>
{isSelected ? `Added to Menu (${selectedQuantity})` : 'Add to Menu'}
</button>
</div>
</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>
</div> </div>
); );

View File

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