add feature to delete recipes when logged in
This commit is contained in:
@@ -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 });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -52,13 +52,37 @@ const RecipeList: React.FC<RecipeListProps> = ({ 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<RecipeListProps> = ({ onSelectionUpdate }) => {
|
||||
recipe={selectedRecipe}
|
||||
onClose={handleCloseModal}
|
||||
onAddToSelection={handleAddToSelection}
|
||||
onRecipeDeleted={handleRecipeDeleted}
|
||||
isSelected={isRecipeSelected(selectedRecipe._id)}
|
||||
selectedQuantity={getSelectedQuantity(selectedRecipe._id)}
|
||||
/>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,13 +163,53 @@ const RecipeModal: React.FC<RecipeModalProps> = ({
|
||||
</div>
|
||||
|
||||
<div className="modal-footer">
|
||||
<button
|
||||
className={`btn ${isSelected ? 'btn-success' : 'btn-primary'} btn-large`}
|
||||
onClick={() => onAddToSelection(recipe._id)}
|
||||
>
|
||||
{isSelected ? `Added to Menu (${selectedQuantity})` : 'Add to Menu'}
|
||||
</button>
|
||||
<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)}
|
||||
>
|
||||
{isSelected ? `Added to Menu (${selectedQuantity})` : 'Add to Menu'}
|
||||
</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>
|
||||
);
|
||||
|
||||
@@ -51,6 +51,7 @@ export interface Recipe {
|
||||
difficulty: 'easy' | 'medium' | 'hard';
|
||||
imageUrl: string;
|
||||
createdAt: string;
|
||||
createdBy?: string;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
|
||||
Reference in New Issue
Block a user