"use client"; import { useState, useEffect, useRef, useCallback } from "react"; import { Bell, Check, CheckCheck, Upload, Share2, MessageCircle, Sparkles, Database, X, BellOff, type LucideIcon } from "lucide-react"; import { motion, AnimatePresence } from "framer-motion"; interface NotificationData { id: string; type: string; title: string; message: string; read: boolean; data: string | null; createdAt: string; } const typeIcons: Record = { upload_complete: Upload, share: Share2, comment: MessageCircle, ai_complete: Sparkles, backup_complete: Database, }; const typeTones: Record = { upload_complete: "text-emerald-600 bg-emerald-500/10 dark:text-emerald-300", share: "text-purple-600 bg-purple-500/10 dark:text-purple-300", comment: "text-blue-600 bg-blue-500/10 dark:text-blue-300", ai_complete: "text-blue-600 bg-blue-500/10 dark:text-blue-300", backup_complete: "text-cyan-600 bg-cyan-500/10 dark:text-cyan-300", }; function timeAgo(date: string): string { const seconds = Math.floor((Date.now() - new Date(date).getTime()) / 1000); if (seconds < 60) return "à l'instant"; const minutes = Math.floor(seconds / 60); if (minutes < 60) return `il y a ${minutes} min`; const hours = Math.floor(minutes / 60); if (hours < 24) return `il y a ${hours} h`; const days = Math.floor(hours / 24); if (days < 30) return `il y a ${days} j`; const months = Math.floor(days / 30); return `il y a ${months} mois`; } export default function NotificationBell() { const [notifications, setNotifications] = useState([]); const [unreadCount, setUnreadCount] = useState(0); const [open, setOpen] = useState(false); const [loaded, setLoaded] = useState(false); const ref = useRef(null); const fetchCount = useCallback(async () => { try { const res = await fetch("/api/notifications?count=true"); if (res.ok) { const data = await res.json(); setUnreadCount(data.count); } } catch { // silencieux } }, []); const fetchNotifications = useCallback(async () => { try { const res = await fetch("/api/notifications"); if (res.ok) { const data = await res.json(); if (Array.isArray(data)) { setNotifications(data); setUnreadCount(data.filter((n: NotificationData) => !n.read).length); } } } catch { // silencieux } finally { setLoaded(true); } }, []); // Sondage toutes les 15 s useEffect(() => { fetchCount(); const interval = setInterval(fetchCount, 15000); return () => clearInterval(interval); }, [fetchCount]); useEffect(() => { if (!open) return; fetchNotifications(); const interval = setInterval(fetchNotifications, 15000); return () => clearInterval(interval); }, [open, fetchNotifications]); // Fermer au clic extérieur / Échap useEffect(() => { if (!open) return; const onClick = (e: MouseEvent) => { if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); }; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") setOpen(false); }; document.addEventListener("mousedown", onClick); window.addEventListener("keydown", onKey); return () => { document.removeEventListener("mousedown", onClick); window.removeEventListener("keydown", onKey); }; }, [open]); const markAllRead = async () => { await fetch("/api/notifications", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "mark_all_read" }), }); setUnreadCount(0); setNotifications((prev) => prev.map((n) => ({ ...n, read: true }))); }; const markRead = async (id: string) => { await fetch(`/api/notifications/${id}`, { method: "PATCH" }); setNotifications((prev) => prev.map((n) => (n.id === id ? { ...n, read: true } : n))); setUnreadCount((prev) => Math.max(0, prev - 1)); }; return (
{open && ( <> {/* Voile mobile */} setOpen(false)} /> {/* Poignée mobile */}
{/* En-tête */}

Notifications

{unreadCount > 0 && ( {unreadCount} )}
{unreadCount > 0 && ( )}
{/* Liste */}
{!loaded ? (
{[...Array(3)].map((_, i) => (
))}
) : notifications.length === 0 ? (

Tout est calme

Les téléversements, partages et analyses IA terminés apparaîtront ici.

) : (
    {notifications.map((n, i) => { const Icon = typeIcons[n.type] || Bell; const tone = typeTones[n.type] || "text-gray-500 bg-gray-500/10 dark:text-gray-300"; return (
    { if (!n.read) markRead(n.id); }} onKeyDown={(e) => { if ((e.key === "Enter" || e.key === " ") && !n.read) { e.preventDefault(); markRead(n.id); } }} className={`relative flex items-start gap-3 p-2.5 rounded-xl transition-colors cursor-pointer ${n.read ? "hover:bg-gray-100/70 dark:hover:bg-white/[0.04]" : "bg-blue-500/[0.06] hover:bg-blue-500/10"}`} >

    {n.title}

    {!n.read && }

    {n.message}

    {timeAgo(n.createdAt)}

    {!n.read && ( )}
    ); })}
)}
)}
); }