template-saas/apps/frontend/src/components/notifications/NotificationDrawer.tsx
rckrdmrd 40d57f8124 feat: Add AI Integration, Notifications UI and Settings Page
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>
2026-01-07 07:04:29 -06:00

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>
</>
);
}