Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 60 additions & 59 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import React from 'react'
import { BrowserRouter as Router, Routes, Route, useLocation } from 'react-router-dom'
import { AnimatePresence, motion, useReducedMotion } from 'framer-motion'
import React, { Suspense, lazy } from 'react'
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
import { I18nextProvider } from 'react-i18next'
import i18n from './i18n'
import { WalletProvider } from './context/WalletContext'
import { ToastProvider } from './context/ToastContext'
import { NotificationProvider } from './context/NotificationContext'
import { ThemeProvider } from './context/ThemeContext'
import { RouteProgressProvider } from './context/RouteProgressContext'
import { NotFoundPage } from './pages/NotFoundPage'
import AppShell from './components/layout/AppShell'
import LandingPage from './pages/LandingPage'
import ErrorBoundary from './components/common/ErrorBoundary'
import { ProtectedRoute } from './components/auth/ProtectedRoute'
import { CommandPalette } from './components/common/CommandPalette'
import { useCommandPalette } from './hooks/useCommandPalette'
import { SkeletonCard } from './components/common/Skeleton'
import './components/common/Toast.css'

// Lazy-loaded pages
Expand Down Expand Up @@ -47,7 +46,7 @@ const RouteLoadingFallback: React.FC = () => (
* `<Router>` rather than beside it.
*/
const AppContent: React.FC = () => {
const { isOpen, closePalette, search, recentSearches } = useCommandPalette()
const { isOpen, closePalette, search, recentSearches, runRecentSearch } = useCommandPalette()

return (
<>
Expand All @@ -57,60 +56,62 @@ const AppContent: React.FC = () => {
path="/*"
element={
<AppShell>
<Routes>
<Route
path="/dashboard"
element={
<ProtectedRoute>
<DashboardPage />
</ProtectedRoute>
}
/>
<Route
path="/wallet"
element={
<ProtectedRoute>
<WalletPage />
</ProtectedRoute>
}
/>
<Route
path="/agents"
element={
<ProtectedRoute>
<AgentsPage />
</ProtectedRoute>
}
/>
<Route
path="/tasks/new"
element={
<ProtectedRoute>
<NewTaskPage />
</ProtectedRoute>
}
/>
<Route
path="/tasks/history"
element={
<ProtectedRoute>
<TaskHistoryPage />
</ProtectedRoute>
}
/>
<Route
path="/tasks/:id"
element={
<ProtectedRoute>
<TaskDetailPage />
</ProtectedRoute>
}
/>
{import.meta.env.DEV && (
<Route path="/renderer-demo" element={<RendererDemoPage />} />
)}
<Route path="*" element={<NotFoundPage />} />
</Routes>
<Suspense fallback={<RouteLoadingFallback />}>
<Routes>
<Route
path="/dashboard"
element={
<ProtectedRoute>
<DashboardPage />
</ProtectedRoute>
}
/>
<Route
path="/wallet"
element={
<ProtectedRoute>
<WalletPage />
</ProtectedRoute>
}
/>
<Route
path="/agents"
element={
<ProtectedRoute>
<AgentsPage />
</ProtectedRoute>
}
/>
<Route
path="/tasks/new"
element={
<ProtectedRoute>
<NewTaskPage />
</ProtectedRoute>
}
/>
<Route
path="/tasks/history"
element={
<ProtectedRoute>
<TaskHistoryPage />
</ProtectedRoute>
}
/>
<Route
path="/tasks/:id"
element={
<ProtectedRoute>
<TaskDetailPage />
</ProtectedRoute>
}
/>
{import.meta.env.DEV && (
<Route path="/renderer-demo" element={<RendererDemoPage />} />
)}
<Route path="*" element={<NotFoundPage />} />
</Routes>
</Suspense>
</AppShell>
}
/>
Expand Down
1 change: 0 additions & 1 deletion frontend/src/components/agents/TaskSubmissionForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { AlertCircle } from 'lucide-react';
import type { TFunction } from 'i18next';
import { DAGPreview } from './DAGPreview';
import { useTaskSubmit } from '../../hooks/useTaskSubmit';
import { useToast } from '../../hooks/useToast';
import { useToast } from '../../context/ToastContext';
import { useTaskDraft } from '../../hooks/useTaskDraft';
import { buildLiveDag } from '../../utils/buildLiveDag';
Expand Down
11 changes: 7 additions & 4 deletions frontend/src/components/landing/Hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,11 @@ const Hero: React.FC = () => {
const { canvasRef, prefersReducedMotion } = useParticles()

return (
<section
<motion.section
className={`flex flex-col items-center text-center pt-24 pb-20 px-4 max-w-4xl mx-auto ${styles.heroContainer}`}
variants={containerVariants}
initial="hidden"
animate="visible"
>
{!prefersReducedMotion ? (
<canvas ref={canvasRef} className={styles.particleCanvas} />
Expand Down Expand Up @@ -56,7 +59,7 @@ const Hero: React.FC = () => {
>
<div className="w-1.5 h-1.5 rounded-full bg-accent-green shadow-success-glow animate-pulse" />
<span className="text-xs font-semibold text-accent-green tracking-wide">{t('landing.hero.badge')}</span>
</div>
</motion.div>

{/* Headline */}
<motion.h1
Expand All @@ -72,15 +75,15 @@ const Hero: React.FC = () => {
</span>,
]}
/>
</h1>
</motion.h1>

{/* Subtext */}
<motion.p
variants={itemVariants}
className="text-base sm:text-lg text-text-secondary max-w-[540px] mx-auto mb-10 leading-[1.6]"
>
{t('landing.hero.subtitle')}
</p>
</motion.p>

{/* CTAs */}
<motion.div
Expand Down
35 changes: 3 additions & 32 deletions frontend/src/components/layout/TopNav.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import React, { useState, useRef } from 'react'
import React from 'react'
import { useLocation } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import { Bell } from 'lucide-react'
import { useWallet } from '../../context/WalletContext'
import { useNotifications } from '../../hooks/useNotifications'
import { NotificationCenter } from '../notifications/NotificationCenter'
import { NotificationBell } from '../notifications/NotificationBell'
import { SUPPORTED_LANGUAGES } from '../../i18n/options'
import { NAV_ITEMS } from './navigation'
import type { SupportedLanguage } from '../../i18n/options'
Expand Down Expand Up @@ -43,9 +41,6 @@ const TopNav: React.FC<TopNavProps> = ({
isDrawerOpen = false,
}) => {
const { publicKey, connected, ready, connectionMethod, disconnect } = useWallet()
const { unreadCount } = useNotifications()
const [isNotificationOpen, setIsNotificationOpen] = useState(false)
const bellButtonRef = useRef<HTMLButtonElement>(null)
const { t, i18n } = useTranslation()
const location = useLocation()
const { mode, setMode } = useTheme()
Expand Down Expand Up @@ -108,31 +103,7 @@ const TopNav: React.FC<TopNavProps> = ({
</div>

<div className="nav-right">
<div className="notification-wrapper">
<button
ref={bellButtonRef}
type="button"
className={`notification-bell-btn ${isNotificationOpen ? 'active' : ''}`}
onClick={() => setIsNotificationOpen(prev => !prev)}
aria-label="Notifications"
aria-expanded={isNotificationOpen}
id="btn-notifications"
data-testid="notification-bell-btn"
>
<Bell size={20} />
{unreadCount > 0 && (
<span className="notification-badge" data-testid="notification-badge">
{unreadCount > 99 ? '99+' : unreadCount}
</span>
)}
</button>

<NotificationCenter
isOpen={isNotificationOpen}
onClose={() => setIsNotificationOpen(false)}
anchorRef={bellButtonRef}
/>
</div>
<NotificationBell />

{/* Theme toggle: cycles light -> dark -> system */}
<button
Expand Down
106 changes: 106 additions & 0 deletions frontend/src/components/notifications/NotificationBell.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import React from 'react';
import { render, screen, fireEvent, act } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest';
import { NotificationProvider, NOTIFICATIONS_STORAGE_KEY } from '../../context/NotificationContext';
import NotificationBell from './NotificationBell';
import type { AppNotification } from '../../types/notification';

vi.mock('framer-motion', async () => {
const actual = await vi.importActual<typeof import('framer-motion')>('framer-motion');
return {
...actual,
AnimatePresence: ({ children }: { children: React.ReactNode }) => <>{children}</>,
motion: {
div: React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ children, ...props }, ref) => <div ref={ref} {...props}>{children}</div>
),
li: React.forwardRef<HTMLLIElement, React.LiHTMLAttributes<HTMLLIElement>>(
({ children, ...props }, ref) => <li ref={ref} {...props}>{children}</li>
),
},
};
});

const sampleNotifications: AppNotification[] = [
{
id: 'bell-notif-1',
type: 'task_completed',
title: 'Task Done',
message: 'Finished task 1',
timestamp: new Date().toISOString(),
read: false,
},
{
id: 'bell-notif-2',
type: 'payment_received',
title: 'Funds Received',
message: 'Received 10 XLM',
timestamp: new Date().toISOString(),
read: false,
},
{
id: 'bell-notif-3',
type: 'agent_registered',
title: 'Agent Added',
message: 'New agent ready',
timestamp: new Date().toISOString(),
read: true,
},
];

const renderBellWithProvider = (initial?: AppNotification[]) => {
if (initial) {
localStorage.setItem(NOTIFICATIONS_STORAGE_KEY, JSON.stringify(initial));
} else {
localStorage.removeItem(NOTIFICATIONS_STORAGE_KEY);
}

return render(
<MemoryRouter>
<NotificationProvider>
<NotificationBell />
</NotificationProvider>
</MemoryRouter>
);
};

describe('NotificationBell Component', () => {
beforeEach(() => {
localStorage.clear();
});

afterEach(() => {
localStorage.clear();
});

test('renders bell button and badge with unread count', () => {
renderBellWithProvider(sampleNotifications);

const bellBtn = screen.getByTestId('notification-bell-btn');
expect(bellBtn).toBeInTheDocument();

const badge = screen.getByTestId('notification-badge');
expect(badge).toBeInTheDocument();
expect(badge).toHaveTextContent('2');
});

test('toggles dropdown panel on button click', async () => {
renderBellWithProvider(sampleNotifications);

const bellBtn = screen.getByTestId('notification-bell-btn');
expect(screen.queryByTestId('notification-panel')).not.toBeInTheDocument();

await act(async () => {
fireEvent.click(bellBtn);
});

expect(screen.getByTestId('notification-panel')).toBeInTheDocument();

await act(async () => {
fireEvent.click(bellBtn);
});

expect(screen.queryByTestId('notification-panel')).not.toBeInTheDocument();
});
});
Loading