diff --git a/frontend/src/components/home/HeroSection.tsx b/frontend/src/components/home/HeroSection.tsx
index e37307166..46417bcdc 100644
--- a/frontend/src/components/home/HeroSection.tsx
+++ b/frontend/src/components/home/HeroSection.tsx
@@ -114,7 +114,7 @@ export function HeroSection() {
$
-
+
forge bounty --reward 100 --lang typescript --tier 2
{typewriterDone && (
diff --git a/frontend/src/components/ui/Toast.tsx b/frontend/src/components/ui/Toast.tsx
new file mode 100644
index 000000000..d858887cf
--- /dev/null
+++ b/frontend/src/components/ui/Toast.tsx
@@ -0,0 +1,212 @@
+'use client';
+
+import React, { createContext, useContext, useState, useCallback, useEffect, useRef } from 'react';
+
+// ─── Types ───────────────────────────────────────────────────────────────────
+
+export type ToastVariant = 'success' | 'error' | 'warning' | 'info';
+
+export interface Toast {
+ id: string;
+ title: string;
+ description?: string;
+ variant: ToastVariant;
+ duration: number;
+}
+
+interface ToastContextValue {
+ toasts: Toast[];
+ addToast: (toast: Omit) => void;
+ removeToast: (id: string) => void;
+}
+
+// ─── Context ─────────────────────────────────────────────────────────────────
+
+const ToastContext = createContext(null);
+
+export function useToast() {
+ const ctx = useContext(ToastContext);
+ if (!ctx) throw new Error('useToast must be used within ToastProvider');
+ return ctx;
+}
+
+// ─── Variant Styles ───────────────────────────────────────────────────────────
+
+const variantStyles: Record = {
+ success: {
+ bg: 'bg-forge-900',
+ border: 'border-emerald/30',
+ icon: '✓',
+ iconColor: 'text-emerald',
+ },
+ error: {
+ bg: 'bg-forge-900',
+ border: 'border-status-error/30',
+ icon: '✕',
+ iconColor: 'text-status-error',
+ },
+ warning: {
+ bg: 'bg-forge-900',
+ border: 'border-status-warning/30',
+ icon: '⚠',
+ iconColor: 'text-status-warning',
+ },
+ info: {
+ bg: 'bg-forge-900',
+ border: 'border-status-info/30',
+ icon: 'ℹ',
+ iconColor: 'text-status-info',
+ },
+};
+
+// ─── Individual Toast Item ─────────────────────────────────────────────────────
+
+function ToastItem({ toast, onRemove }: { toast: Toast; onRemove: (id: string) => void }) {
+ const { bg, border, icon, iconColor } = variantStyles[toast.variant];
+ const [visible, setVisible] = useState(false);
+ const [progress, setProgress] = useState(100);
+
+ useEffect(() => {
+ // Trigger entrance animation
+ requestAnimationFrame(() => setVisible(true));
+
+ const startTime = Date.now();
+ const interval = setInterval(() => {
+ const elapsed = Date.now() - startTime;
+ const remaining = Math.max(0, 100 - (elapsed / toast.duration) * 100);
+ setProgress(remaining);
+ if (remaining === 0) {
+ clearInterval(interval);
+ handleRemove();
+ }
+ }, 50);
+
+ return () => clearInterval(interval);
+ }, [toast.duration]);
+
+ const handleRemove = () => {
+ setVisible(false);
+ setTimeout(() => onRemove(toast.id), 300);
+ };
+
+ return (
+
+ {/* Progress bar */}
+
+
+
+ {/* Icon */}
+
{icon}
+
+ {/* Content */}
+
+
{toast.title}
+ {toast.description && (
+
{toast.description}
+ )}
+
+
+ {/* Close button */}
+
+
+
+ );
+}
+
+// ─── Toast Container ──────────────────────────────────────────────────────────
+
+function ToastContainer({ toasts, onRemove }: { toasts: Toast[]; onRemove: (id: string) => void }) {
+ return (
+
+ {toasts.map((toast) => (
+
+ ))}
+
+ );
+}
+
+// ─── Provider ─────────────────────────────────────────────────────────────────
+
+let toastIdCounter = 0;
+
+export function ToastProvider({ children }: { children: React.ReactNode }) {
+ const [toasts, setToasts] = useState([]);
+ const toastsRef = useRef(toasts);
+ toastsRef.current = toasts;
+
+ const addToast = useCallback((toast: Omit) => {
+ const id = `toast-${++toastIdCounter}-${Date.now()}`;
+ setToasts((prev) => [...prev.slice(-4), { ...toast, id }]); // max 5 toasts
+ }, []);
+
+ const removeToast = useCallback((id: string) => {
+ setToasts((prev) => prev.filter((t) => t.id !== id));
+ }, []);
+
+ // Listen for global show-toast events
+ useEffect(() => {
+ const handler = (e: Event) => {
+ const detail = (e as CustomEvent).detail;
+ addToast(detail);
+ };
+ window.addEventListener('show-toast', handler);
+ return () => window.removeEventListener('show-toast', handler);
+ }, [addToast]);
+
+ return (
+
+ {children}
+
+
+ );
+}
+
+// ─── Convenience functions (use via hook or import toast fn) ──────────────────
+
+export function showToast(
+ variant: ToastVariant,
+ title: string,
+ description?: string,
+ duration = 5000
+) {
+ // Dispatches a custom event that the ToastProvider listens to
+ window.dispatchEvent(
+ new CustomEvent('show-toast', { detail: { variant, title, description, duration } })
+ );
+}
+
+export const toast = {
+ success: (title: string, description?: string, duration = 5000) =>
+ showToast('success', title, description, duration),
+ error: (title: string, description?: string, duration = 5000) =>
+ showToast('error', title, description, duration),
+ warning: (title: string, description?: string, duration = 5000) =>
+ showToast('warning', title, description, duration),
+ info: (title: string, description?: string, duration = 5000) =>
+ showToast('info', title, description, duration),
+};
diff --git a/frontend/src/index.css b/frontend/src/index.css
index 33799d725..399fba074 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -115,6 +115,7 @@
html {
scroll-behavior: smooth;
font-size: 16px;
+ overflow-x: hidden;
}
body {
@@ -123,6 +124,7 @@ body {
-moz-osx-font-smoothing: grayscale;
background-color: #050505;
color: #F0F0F5;
+ overflow-x: hidden;
}
/* Custom scrollbar */
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
index b20036806..bb603ae28 100644
--- a/frontend/src/main.tsx
+++ b/frontend/src/main.tsx
@@ -2,6 +2,7 @@ import React, { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { QueryClientProvider } from '@tanstack/react-query';
+import { ToastProvider } from './components/ui/Toast';
import { AuthProvider } from './contexts/AuthContext';
import { queryClient } from './services/queryClient';
import App from './App';
@@ -14,9 +15,11 @@ createRoot(root).render(
-
-
-
+
+
+
+
+