diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1c78aeb0..05eb400f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,5 @@ import React, { Suspense } from 'react'; +import { ToastProvider } from './components/ui'; import { Routes, Route } from 'react-router-dom'; import { AuthGuard } from './components/auth/AuthGuard'; @@ -23,7 +24,8 @@ function PageLoader() { export default function App() { return ( - }> + + }> } /> } /> @@ -52,3 +54,4 @@ export default function App() { ); } + diff --git a/frontend/src/components/ui/Toast.tsx b/frontend/src/components/ui/Toast.tsx new file mode 100644 index 00000000..34c6db20 --- /dev/null +++ b/frontend/src/components/ui/Toast.tsx @@ -0,0 +1,97 @@ +import React, { createContext, useContext, useState, useCallback } from 'react'; +import { AnimatePresence, motion } from 'framer-motion'; +import { X, CheckCircle, AlertCircle, AlertTriangle, Info } from 'lucide-react'; + +type ToastType = 'success' | 'error' | 'warning' | 'info'; + +interface Toast { + id: string; + type: ToastType; + message: string; +} + +interface ToastContextValue { + showToast: (type: ToastType, message: string) => void; +} + +const ToastContext = createContext({ showToast: () => {} }); + +export const useToast = () => useContext(ToastContext); + +const TOAST_STYLES: Record = { + success: { + bg: 'bg-emerald-950/90', + border: 'border-emerald-500', + icon: + }, + error: { + bg: 'bg-red-950/90', + border: 'border-red-500', + icon: + }, + warning: { + bg: 'bg-amber-950/90', + border: 'border-amber-500', + icon: + }, + info: { + bg: 'bg-blue-950/90', + border: 'border-blue-500', + icon: + } +}; + +function ToastItem({ toast, onRemove }: { toast: Toast; onRemove: (id: string) => void }) { + const { bg, border, icon } = TOAST_STYLES[toast.type]; + + React.useEffect(() => { + const timer = setTimeout(() => onRemove(toast.id), 5000); + return () => clearTimeout(timer); + }, [toast.id, onRemove]); + + return ( + + {icon} +

{toast.message}

+ +
+ ); +} + +export function ToastProvider({ children }: { children: React.ReactNode }) { + const [toasts, setToasts] = useState([]); + + const showToast = useCallback((type: ToastType, message: string) => { + const id = Date.now().toString() + Math.random().toString(36); + setToasts(prev => [...prev, { id, type, message }]); + }, []); + + const removeToast = useCallback((id: string) => { + setToasts(prev => prev.filter(t => t.id !== id)); + }, []); + + return ( + + {children} +
+ + {toasts.map(toast => ( + + ))} + +
+
+ ); +} diff --git a/frontend/src/components/ui/index.ts b/frontend/src/components/ui/index.ts new file mode 100644 index 00000000..90446338 --- /dev/null +++ b/frontend/src/components/ui/index.ts @@ -0,0 +1 @@ +export { ToastProvider, useToast } from './Toast';