From 243b63309cb858a993b524aeff13ded7d549a928 Mon Sep 17 00:00:00 2001 From: dvirlabs Date: Fri, 5 Dec 2025 04:52:27 +0200 Subject: [PATCH] Add toast notification --- frontend/src/App.css | 109 +++++++++++++++++++++ frontend/src/App.jsx | 49 ++++++++- frontend/src/components/Modal.jsx | 29 ++++++ frontend/src/components/RecipeDetails.jsx | 6 +- frontend/src/components/Toast.jsx | 24 +++++ frontend/src/components/ToastContainer.jsx | 20 ++++ 6 files changed, 231 insertions(+), 6 deletions(-) create mode 100644 frontend/src/components/Modal.jsx create mode 100644 frontend/src/components/Toast.jsx create mode 100644 frontend/src/components/ToastContainer.jsx diff --git a/frontend/src/App.css b/frontend/src/App.css index 929bceb..12a2536 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -193,6 +193,12 @@ select { border: 1px solid rgba(148, 163, 184, 0.5); } +.btn.danger { + background: linear-gradient(135deg, var(--danger), #e74c3c); + color: #fde8e8; + box-shadow: 0 12px 25px rgba(249, 115, 115, 0.4); +} + .btn.small { padding: 0.25rem 0.7rem; font-size: 0.8rem; @@ -442,3 +448,106 @@ select { grid-template-columns: repeat(2, minmax(0, 1fr)); } } + +/* Modal */ + +.modal-backdrop { + position: fixed; + inset: 0; + background: rgba(15, 23, 42, 0.8); + display: flex; + justify-content: center; + align-items: center; + z-index: 50; +} + +.modal { + background: var(--card); + border-radius: 18px; + padding: 1.5rem; + border: 1px solid var(--border-subtle); + box-shadow: 0 20px 50px rgba(0, 0, 0, 0.9); + max-width: 400px; + width: 90vw; +} + +.modal-header { + margin-bottom: 1rem; +} + +.modal-header h2 { + margin: 0; + font-size: 1.1rem; +} + +.modal-body { + margin-bottom: 1.2rem; + color: var(--text-muted); + line-height: 1.5; +} + +.modal-footer { + display: flex; + gap: 0.6rem; + justify-content: flex-end; +} + +.modal-footer .btn { + flex: 1; +} + +/* Toast Notifications */ + +.toast-container { + position: fixed; + bottom: 1.5rem; + right: 1.5rem; + z-index: 60; + display: flex; + flex-direction: column; + gap: 0.8rem; + max-width: 400px; + pointer-events: none; +} + +.toast { + padding: 1rem 1.2rem; + border-radius: 12px; + border: 1px solid rgba(148, 163, 184, 0.3); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + gap: 0.8rem; + animation: slideIn 0.3s ease-out; + pointer-events: auto; + font-size: 0.9rem; +} + +.toast.success { + background: rgba(34, 197, 94, 0.1); + border-color: rgba(34, 197, 94, 0.5); + color: #86efac; +} + +.toast.error { + background: rgba(249, 115, 115, 0.1); + border-color: rgba(249, 115, 115, 0.5); + color: #fca5a5; +} + +.toast.info { + background: rgba(59, 130, 246, 0.1); + border-color: rgba(59, 130, 246, 0.5); + color: #93c5fd; +} + +@keyframes slideIn { + from { + transform: translateX(400px); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 5fc96f7..1b04141 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -5,6 +5,8 @@ import TopBar from "./components/TopBar"; import RecipeList from "./components/RecipeList"; import RecipeDetails from "./components/RecipeDetails"; import RecipeFormDrawer from "./components/RecipeFormDrawer"; +import Modal from "./components/Modal"; +import ToastContainer from "./components/ToastContainer"; import { getRecipes, getRandomRecipe, createRecipe, updateRecipe, deleteRecipe } from "./api"; function App() { @@ -21,6 +23,9 @@ function App() { const [drawerOpen, setDrawerOpen] = useState(false); const [editingRecipe, setEditingRecipe] = useState(null); + const [deleteModal, setDeleteModal] = useState({ isOpen: false, recipeId: null, recipeName: "" }); + const [toasts, setToasts] = useState([]); + useEffect(() => { loadRecipes(); }, []); @@ -71,8 +76,10 @@ function App() { setEditingRecipe(null); await loadRecipes(); setSelectedRecipe(created); + addToast("המתכון החדש נוצר בהצלחה!", "success"); } catch { setError("שגיאה בשמירת המתכון החדש."); + addToast("שגיאה בשמירת המתכון החדש", "error"); } }; @@ -91,21 +98,45 @@ function App() { if (updated) { setSelectedRecipe(updated); } + addToast("המתכון עודכן בהצלחה!", "success"); } catch { setError("שגיאה בעדכון המתכון."); + addToast("שגיאה בעדכון המתכון", "error"); } }; - const handleDeleteRecipe = async (recipeId) => { + const handleShowDeleteModal = (recipeId, recipeName) => { + setDeleteModal({ isOpen: true, recipeId, recipeName }); + }; + + const handleConfirmDelete = async () => { + const recipeId = deleteModal.recipeId; + setDeleteModal({ isOpen: false, recipeId: null, recipeName: "" }); + try { await deleteRecipe(recipeId); await loadRecipes(); setSelectedRecipe(null); + addToast("המתכון נמחק בהצלחה!", "success"); } catch { setError("שגיאה במחיקת המתכון."); + addToast("שגיאה במחיקת המתכון", "error"); } }; + const handleCancelDelete = () => { + setDeleteModal({ isOpen: false, recipeId: null, recipeName: "" }); + }; + + const addToast = (message, type = "info", duration = 3000) => { + const id = Date.now(); + setToasts((prev) => [...prev, { id, message, type, duration }]); + }; + + const removeToast = (id) => { + setToasts((prev) => prev.filter((toast) => toast.id !== id)); + }; + const handleFormSubmit = (payload) => { if (editingRecipe) { handleUpdateRecipe(payload); @@ -179,7 +210,8 @@ function App() { @@ -193,6 +225,19 @@ function App() { onSubmit={handleFormSubmit} editingRecipe={editingRecipe} /> + + + + ); } diff --git a/frontend/src/components/Modal.jsx b/frontend/src/components/Modal.jsx new file mode 100644 index 0000000..cc902b1 --- /dev/null +++ b/frontend/src/components/Modal.jsx @@ -0,0 +1,29 @@ +function Modal({ isOpen, title, message, onConfirm, onCancel, confirmText = "מחק", cancelText = "ביטול", isDangerous = false }) { + if (!isOpen) return null; + + return ( +
+
e.stopPropagation()}> +
+

{title}

+
+
+ {message} +
+
+ + +
+
+
+ ); +} + +export default Modal; diff --git a/frontend/src/components/RecipeDetails.jsx b/frontend/src/components/RecipeDetails.jsx index f125984..b509203 100644 --- a/frontend/src/components/RecipeDetails.jsx +++ b/frontend/src/components/RecipeDetails.jsx @@ -1,4 +1,4 @@ -function RecipeDetails({ recipe, onEditClick, onDeleteClick }) { +function RecipeDetails({ recipe, onEditClick, onDeleteClick, onShowDeleteModal }) { if (!recipe) { return (
@@ -8,9 +8,7 @@ function RecipeDetails({ recipe, onEditClick, onDeleteClick }) { } const handleDelete = () => { - if (confirm("בטוח שאתה רוצה למחוק את המתכון הזה?")) { - onDeleteClick(recipe.id); - } + onShowDeleteModal(recipe.id, recipe.name); }; return ( diff --git a/frontend/src/components/Toast.jsx b/frontend/src/components/Toast.jsx new file mode 100644 index 0000000..6847709 --- /dev/null +++ b/frontend/src/components/Toast.jsx @@ -0,0 +1,24 @@ +import { useEffect } from "react"; + +function Toast({ id, message, type = "info", duration = 3000, onClose }) { + useEffect(() => { + const timer = setTimeout(() => { + onClose(id); + }, duration); + + return () => clearTimeout(timer); + }, [id, duration, onClose]); + + return ( +
+ + {type === "success" && "✓"} + {type === "error" && "✕"} + {type === "info" && "ℹ"} + + {message} +
+ ); +} + +export default Toast; diff --git a/frontend/src/components/ToastContainer.jsx b/frontend/src/components/ToastContainer.jsx new file mode 100644 index 0000000..baa61ed --- /dev/null +++ b/frontend/src/components/ToastContainer.jsx @@ -0,0 +1,20 @@ +import Toast from "./Toast"; + +function ToastContainer({ toasts, onRemove }) { + return ( +
+ {toasts.map((toast) => ( + + ))} +
+ ); +} + +export default ToastContainer;