SolyQuiz/components/dashboard/NotificationBell.tsx
corenthin-lebreton 28aa3b0e10 initial project
2026-02-26 20:10:14 +01:00

112 lines
4.5 KiB
TypeScript

'use client'
import { useState, useRef, useEffect } from 'react'
import { Bell, X } from 'lucide-react'
import { cn } from '@/lib/utils'
interface Notification {
id: string
icon: string
title: string
desc: string
time: string
read: boolean
}
const INITIAL_NOTIFICATIONS: Notification[] = [
{ id: '1', icon: '✅', title: 'Session finalisée', desc: 'Rapports disponibles pour JS Fundamentals.', time: 'il y a 2h', read: false },
{ id: '2', icon: '👥', title: 'Nouvelles inscriptions', desc: '12 étudiants ont rejoint "React Basics Quiz".', time: 'il y a 4h', read: false },
{ id: '3', icon: '🔧', title: 'Maintenance prévue', desc: 'Samedi à 22h. Pensez à sauvegarder votre travail.', time: 'il y a 1j', read: true },
]
export default function NotificationBell() {
const [open, setOpen] = useState(false)
const [notifications, setNotifications] = useState<Notification[]>(INITIAL_NOTIFICATIONS)
const ref = useRef<HTMLDivElement>(null)
const unreadCount = notifications.filter((n) => !n.read).length
useEffect(() => {
const handleClick = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
}
document.addEventListener('mousedown', handleClick)
return () => document.removeEventListener('mousedown', handleClick)
}, [])
const markRead = (id: string) =>
setNotifications((prev) => prev.map((n) => (n.id === id ? { ...n, read: true } : n)))
const markAllRead = () => setNotifications((prev) => prev.map((n) => ({ ...n, read: true })))
return (
<div ref={ref} className="relative">
<button
onClick={() => setOpen((v) => !v)}
className="relative p-2.5 bg-background-card border border-border rounded-lg hover:border-border-light transition-colors"
>
<Bell size={18} className="text-text-secondary" />
{unreadCount > 0 && (
<span className="absolute top-1.5 right-1.5 w-2 h-2 bg-primary rounded-full animate-pulse" />
)}
</button>
{open && (
<div className="absolute right-0 top-12 w-80 bg-background-card border border-border rounded-xl shadow-2xl shadow-black/50 z-50 overflow-hidden animate-in fade-in slide-in-from-top-2 duration-150">
<div className="flex items-center justify-between px-4 py-3 border-b border-border">
<div className="flex items-center gap-2">
<h3 className="font-semibold text-text-primary text-sm">Notifications</h3>
{unreadCount > 0 && (
<span className="bg-primary/20 text-primary text-xs px-2 py-0.5 rounded-full font-medium">
{unreadCount}
</span>
)}
</div>
<div className="flex items-center gap-2">
{unreadCount > 0 && (
<button
onClick={markAllRead}
className="text-xs text-primary hover:text-primary-light transition-colors"
>
Tout lire
</button>
)}
<button
onClick={() => setOpen(false)}
className="p-1 hover:bg-background-elevated rounded transition-colors"
>
<X size={14} className="text-text-muted" />
</button>
</div>
</div>
<div className="max-h-80 overflow-y-auto">
{notifications.map((notif) => (
<div
key={notif.id}
onClick={() => markRead(notif.id)}
className={cn(
'flex items-start gap-3 px-4 py-3 hover:bg-background-elevated/50 transition-colors cursor-pointer border-b border-border/50 last:border-0',
!notif.read && 'bg-primary/5'
)}
>
<div className="w-8 h-8 bg-background-elevated rounded-lg flex items-center justify-center text-sm flex-shrink-0">
{notif.icon}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2 mb-0.5">
<p className="text-sm font-medium text-text-primary truncate">{notif.title}</p>
{!notif.read && <div className="w-1.5 h-1.5 bg-primary rounded-full flex-shrink-0" />}
</div>
<p className="text-xs text-text-muted leading-relaxed">{notif.desc}</p>
<p className="text-xs text-text-muted/50 mt-1">{notif.time}</p>
</div>
</div>
))}
</div>
</div>
)}
</div>
)
}