diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index dec300c..b403f34 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -17,6 +17,7 @@ import FeatureCards from "./components/FeatureCards" import HowItWorks from "./components/HowItWorks" import ExampleRepos from "./components/ExampleRepos" import { usePrefersReducedMotion } from "./hooks/usePrefersReducedMotion" +import { useToast } from "./components/Toast" import { useHistory } from "./hooks/useHistory" import HistoryPanel from "./components/HistoryPanel" @@ -41,6 +42,7 @@ type TabKey = (typeof TABS)[number] export default function App() { const { dark, toggle } = useTheme() const reducedMotion = usePrefersReducedMotion() + const { addToast } = useToast() const { history, addEntry, clearHistory } = useHistory() const [loading, setLoading] = useState(false) const [error, setError] = useState("") @@ -65,6 +67,11 @@ export default function App() { try { const data = await analyze(url, br) setResult(data) + if (data.total_violations === 0) { + addToast("No violations found — architecture looks clean!", "success") + } else { + addToast(`Found ${data.total_violations} violation(s)`, "info") + } addEntry({ url, branch: br, @@ -72,7 +79,9 @@ export default function App() { drift: data.drift_score, }) } catch (err) { - setError(err instanceof Error ? err.message : "Analysis failed") + const msg = err instanceof Error ? err.message : "Analysis failed" + setError(msg) + addToast(msg, "error") } finally { setLoading(false) } diff --git a/frontend/src/components/Toast.tsx b/frontend/src/components/Toast.tsx new file mode 100644 index 0000000..7206b4c --- /dev/null +++ b/frontend/src/components/Toast.tsx @@ -0,0 +1,89 @@ +import { useState, useCallback, createContext, useContext, useEffect } from "react" +import { X, CheckCircle, AlertTriangle, Info } from "lucide-react" + +export interface Toast { + id: string + message: string + type: "success" | "error" | "info" +} + +interface ToastContextValue { + toasts: Toast[] + addToast: (message: string, type?: Toast["type"]) => void + removeToast: (id: string) => void +} + +const ToastContext = createContext(null) + +export function useToast() { + const ctx = useContext(ToastContext) + if (!ctx) throw new Error("useToast must be used within ToastProvider") + return ctx +} + +export function ToastProvider({ children }: { children: React.ReactNode }) { + const [toasts, setToasts] = useState([]) + + const addToast = useCallback((message: string, type: Toast["type"] = "info") => { + const id = Math.random().toString(36).slice(2) + setToasts((prev) => [...prev, { id, message, type }]) + }, []) + + const removeToast = useCallback((id: string) => { + setToasts((prev) => prev.filter((t) => t.id !== id)) + }, []) + + return ( + + {children} + + + ) +} + +function ToastContainer({ toasts, onRemove }: { toasts: Toast[]; onRemove: (id: string) => void }) { + if (toasts.length === 0) return null + + return ( +
+ {toasts.map((toast) => ( + + ))} +
+ ) +} + +function ToastItem({ toast, onRemove }: { toast: Toast; onRemove: (id: string) => void }) { + useEffect(() => { + const timer = setTimeout(() => onRemove(toast.id), 4000) + return () => clearTimeout(timer) + }, [toast.id, onRemove]) + + const icons = { + success: , + error: , + info: , + } + + const bg = { + success: "border-green-400/30 bg-green-400/10", + error: "border-error/30 bg-error/10", + info: "border-brand/30 bg-brand/10", + } + + return ( +
+ {icons[toast.type]} + {toast.message} + +
+ ) +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 6fef298..a6fb2e4 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -3,11 +3,14 @@ import { createRoot } from 'react-dom/client' import './index.css' import App from './App.tsx' import ErrorBoundary from './components/ErrorBoundary' +import { ToastProvider } from './components/Toast' createRoot(document.getElementById('root')!).render( - + + + , )