FASE 1: Notifications UI - Add NotificationBell, NotificationDrawer, NotificationItem components - Integrate notification bell in DashboardLayout header - Real-time unread count with polling FASE 2: AI Integration Backend - Add AI module with OpenRouter client - Endpoints: POST /ai/chat, GET /ai/models, GET/PATCH /ai/config - GET /ai/usage, GET /ai/usage/current, GET /ai/health - Database: schema ai with configs and usage tables - Token tracking and cost calculation FASE 3: Settings Page Refactor - Restructure with tabs navigation - GeneralSettings: profile, organization, appearance - NotificationSettings: channels and categories toggles - SecuritySettings: password change, 2FA placeholder, sessions Files created: 25+ Endpoints added: 7 Story Points completed: 21 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
118 lines
4.2 KiB
TypeScript
118 lines
4.2 KiB
TypeScript
import { X, Bell, CheckCheck, Loader2 } from 'lucide-react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import clsx from 'clsx';
|
|
import { useNotifications, useMarkNotificationAsRead, useMarkAllNotificationsAsRead } from '@/hooks/useData';
|
|
import { NotificationItem } from './NotificationItem';
|
|
|
|
interface NotificationDrawerProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
}
|
|
|
|
export function NotificationDrawer({ isOpen, onClose }: NotificationDrawerProps) {
|
|
const navigate = useNavigate();
|
|
const { data, isLoading } = useNotifications(1, 20);
|
|
const markAsRead = useMarkNotificationAsRead();
|
|
const markAllAsRead = useMarkAllNotificationsAsRead();
|
|
|
|
const notifications = data?.data ?? [];
|
|
const hasUnread = notifications.some((n) => !n.read_at);
|
|
|
|
const handleMarkAsRead = (id: string) => {
|
|
markAsRead.mutate(id);
|
|
};
|
|
|
|
const handleMarkAllAsRead = () => {
|
|
markAllAsRead.mutate();
|
|
};
|
|
|
|
const handleNavigate = (url: string) => {
|
|
onClose();
|
|
navigate(url);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
{/* Backdrop */}
|
|
{isOpen && (
|
|
<div
|
|
className="fixed inset-0 z-40 bg-black/20 dark:bg-black/40"
|
|
onClick={onClose}
|
|
/>
|
|
)}
|
|
|
|
{/* Drawer */}
|
|
<div
|
|
className={clsx(
|
|
'fixed top-0 right-0 z-50 h-full w-full sm:w-96 bg-white dark:bg-secondary-800 shadow-xl transform transition-transform duration-300 ease-in-out',
|
|
isOpen ? 'translate-x-0' : 'translate-x-full'
|
|
)}
|
|
>
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between h-16 px-4 border-b border-secondary-200 dark:border-secondary-700">
|
|
<div className="flex items-center gap-2">
|
|
<Bell className="w-5 h-5 text-secondary-600 dark:text-secondary-400" />
|
|
<h2 className="text-lg font-semibold text-secondary-900 dark:text-secondary-100">
|
|
Notifications
|
|
</h2>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{hasUnread && (
|
|
<button
|
|
onClick={handleMarkAllAsRead}
|
|
disabled={markAllAsRead.isPending}
|
|
className="flex items-center gap-1 px-2 py-1 text-xs font-medium text-primary-600 hover:bg-primary-50 dark:hover:bg-primary-900/20 rounded-md transition-colors disabled:opacity-50"
|
|
>
|
|
{markAllAsRead.isPending ? (
|
|
<Loader2 className="w-3 h-3 animate-spin" />
|
|
) : (
|
|
<CheckCheck className="w-3 h-3" />
|
|
)}
|
|
Mark all read
|
|
</button>
|
|
)}
|
|
<button
|
|
onClick={onClose}
|
|
className="p-2 rounded-lg hover:bg-secondary-100 dark:hover:bg-secondary-700 transition-colors"
|
|
>
|
|
<X className="w-5 h-5 text-secondary-600 dark:text-secondary-400" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="h-[calc(100%-4rem)] overflow-y-auto">
|
|
{isLoading ? (
|
|
<div className="flex items-center justify-center h-32">
|
|
<Loader2 className="w-6 h-6 animate-spin text-primary-500" />
|
|
</div>
|
|
) : notifications.length === 0 ? (
|
|
<div className="flex flex-col items-center justify-center h-64 text-center px-4">
|
|
<div className="w-16 h-16 bg-secondary-100 dark:bg-secondary-700 rounded-full flex items-center justify-center mb-4">
|
|
<Bell className="w-8 h-8 text-secondary-400" />
|
|
</div>
|
|
<p className="text-secondary-600 dark:text-secondary-400 font-medium">
|
|
No notifications
|
|
</p>
|
|
<p className="text-sm text-secondary-500 dark:text-secondary-500 mt-1">
|
|
You're all caught up!
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="divide-y divide-secondary-100 dark:divide-secondary-700">
|
|
{notifications.map((notification) => (
|
|
<NotificationItem
|
|
key={notification.id}
|
|
notification={notification}
|
|
onRead={handleMarkAsRead}
|
|
onNavigate={handleNavigate}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|