tahamajs/IE / Projects /front /src /components /notifications /SmartNotificationCenter.tsx
tahamajs's picture
download
raw
13.8 kB
// src/components/notifications/SmartNotificationCenter.tsx
import { useState, useMemo } from "react";
import { useNavigate } from "react-router-dom";
import { useSmartNotifications } from "@/hooks/useSmartNotifications";
import {
Bell, BellOff, Zap, Clock, Settings, X, CheckCheck,
AlertTriangle, MessageSquare, Heart, UserPlus, FileText,
Calendar, Filter, ChevronDown, Shield, Brain,
} from "lucide-react";
import { motion, AnimatePresence } from "framer-motion";
export function SmartNotificationCenter() {
const navigate = useNavigate();
const {
notifications, batches, currentContext, settings,
unreadCount, criticalCount, batchedCount,
showReviewPanel, setShowReviewPanel,
showSettings, setShowSettings,
acknowledgeNotification, clearAll, updateSettings,
} = useSmartNotifications();
const [filterPriority, setFilterPriority] = useState<string>("all");
const [showContextInfo, setShowContextInfo] = useState(false);
const filtered = useMemo(() => {
if (filterPriority === "all") return notifications;
return notifications.filter(n => n.priority === filterPriority);
}, [notifications, filterPriority]);
const contextColors: Record<string, string> = {
deep_work: "bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400",
focus: "bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-400",
available: "bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400",
idle: "bg-gray-100 dark:bg-gray-700 text-gray-500",
meeting: "bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400",
};
const contextIcons: Record<string, string> = {
deep_work: "🧠",
focus: "🎯",
available: "βœ…",
idle: "😴",
meeting: "πŸ“…",
};
return (
<>
{/* ── Trigger Button ──────────────────────────────── */}
<button
onClick={() => setShowReviewPanel(!showReviewPanel)}
className="relative p-2 text-gray-500 hover:text-primary hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition"
title="Notifications"
>
<Bell className="w-5 h-5" />
{unreadCount > 0 && (
<span className="absolute -top-0.5 -right-0.5 min-w-[18px] h-[18px] bg-red-500 text-white text-[10px] font-bold rounded-full flex items-center justify-center px-1">
{unreadCount > 99 ? "99+" : unreadCount}
</span>
)}
{criticalCount > 0 && (
<span className="absolute -bottom-0.5 -left-0.5 w-2.5 h-2.5 bg-amber-500 rounded-full border-2 border-white dark:border-gray-900" />
)}
</button>
{/* ── Panel ────────────────────────────────────────── */}
<AnimatePresence>
{showReviewPanel && (
<motion.div
initial={{ opacity: 0, x: 300 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 300 }}
className="fixed top-16 right-4 z-50 w-96 bg-white dark:bg-gray-900 rounded-2xl shadow-2xl border border-gray-200 dark:border-gray-800 overflow-hidden max-h-[80vh] flex flex-col"
>
{/* Header */}
<div className="p-4 border-b border-gray-100 dark:border-gray-800">
<div className="flex items-center justify-between mb-3">
<h3 className="font-bold text-lg flex items-center gap-2">
<Bell className="w-5 h-5 text-primary" />
Notifications
</h3>
<div className="flex items-center gap-1">
{unreadCount > 0 && (
<button onClick={clearAll} className="p-1.5 text-gray-400 hover:text-primary rounded-lg" title="Mark all read">
<CheckCheck className="w-4 h-4" />
</button>
)}
<button onClick={() => setShowSettings(true)} className="p-1.5 text-gray-400 hover:text-primary rounded-lg" title="Settings">
<Settings className="w-4 h-4" />
</button>
<button onClick={() => setShowReviewPanel(false)} className="p-1.5 text-gray-400 hover:text-gray-600 rounded-lg">
<X className="w-4 h-4" />
</button>
</div>
</div>
{/* Context badge */}
<button
onClick={() => setShowContextInfo(!showContextInfo)}
className={`flex items-center gap-2 px-3 py-1.5 rounded-full text-xs font-medium ${contextColors[currentContext] ?? "bg-gray-100 text-gray-600"}`}
>
<span>{contextIcons[currentContext] ?? "πŸ’¬"}</span>
<span className="capitalize">{currentContext.replace(/_/g, " ")}</span>
<span className="text-[10px] opacity-60">
Β· {currentContext === "deep_work" ? "Notifications batched" : currentContext === "available" ? "All notifications on" : "Non-critical batched"}
</span>
</button>
{showContextInfo && (
<div className="mt-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-xl text-xs text-gray-500">
<p className="flex items-center gap-1"><Brain className="w-3 h-3 text-primary"/> AI detects your context to filter notifications:</p>
<ul className="mt-1.5 space-y-1 ml-5 list-disc">
<li><strong>Deep Work</strong>: Only critical alerts come through</li>
<li><strong>Focus</strong>: Non‑urgent notifications are batched</li>
<li><strong>Available</strong>: All notifications delivered immediately</li>
</ul>
</div>
)}
</div>
{/* Filter pills */}
<div className="px-4 py-2 border-b border-gray-50 dark:border-gray-800 flex gap-1.5 overflow-x-auto">
{[
{ key: "all", label: "All", count: notifications.length },
{ key: "critical", label: "Critical", count: criticalCount },
{ key: "high", label: "High", count: notifications.filter(n => n.priority === "high").length },
{ key: "medium", label: "Medium", count: notifications.filter(n => n.priority === "medium").length },
].map(f => (
<button
key={f.key}
onClick={() => setFilterPriority(f.key)}
className={`px-3 py-1 rounded-full text-[10px] font-medium whitespace-nowrap transition ${
filterPriority === f.key ? "bg-primary text-white" : "bg-gray-100 dark:bg-gray-700 text-gray-500"
}`}
>
{f.label} {f.count > 0 && `(${f.count})`}
</button>
))}
</div>
{/* Notification list */}
<div className="flex-1 overflow-y-auto">
{filtered.length === 0 ? (
<div className="py-12 text-center text-gray-400">
<BellOff className="w-10 h-10 mx-auto mb-3 opacity-30" />
<p className="text-sm">All caught up!</p>
</div>
) : (
<div className="divide-y divide-gray-50 dark:divide-gray-800">
{filtered.map(notif => (
<div
key={notif.id}
onClick={() => {
acknowledgeNotification(notif.id);
if (notif.link) navigate(notif.link);
}}
className={`px-4 py-3 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50 transition ${
!notif.acknowledged ? "bg-blue-50/50 dark:bg-blue-900/5" : ""
}`}
>
<div className="flex items-start gap-3">
<span className="text-lg mt-0.5">
{{ message: "πŸ’¬", mention: "πŸ“’", follow: "πŸ‘€", like: "❀️", comment: "πŸ’­", system: "ℹ️", alert: "⚠️", reminder: "⏰", grant_deadline: "πŸ’°", review_request: "βœ…", paper_published: "πŸ“„" }[notif.type] ?? "πŸ””"}
</span>
<div className="flex-1 min-w-0">
<p className="text-sm font-semibold truncate">{notif.title}</p>
<p className="text-xs text-gray-500 mt-0.5 line-clamp-2">{notif.body}</p>
<div className="flex items-center gap-2 mt-1.5">
<span className={`text-[9px] px-1.5 py-0.5 rounded-full ${
notif.priority === "critical" ? "bg-red-100 dark:bg-red-900/30 text-red-700" :
notif.priority === "high" ? "bg-amber-100 dark:bg-amber-900/30 text-amber-700" :
"bg-gray-100 dark:bg-gray-700 text-gray-500"
}`}>
{notif.priority}
</span>
<span className="text-[9px] text-gray-400">
{new Date(notif.timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
</span>
{notif.deliveryStrategy === "batch" && (
<span className="text-[9px] text-primary">πŸ“¬ Batched</span>
)}
</div>
</div>
{!notif.acknowledged && <div className="w-2 h-2 rounded-full bg-blue-500 mt-1.5 shrink-0" />}
</div>
</div>
))}
</div>
)}
</div>
{/* Batched count footer */}
{batchedCount > 0 && (
<div className="px-4 py-2 border-t border-gray-100 dark:border-gray-800 text-center">
<p className="text-xs text-gray-400">
<Clock className="w-3 h-3 inline mr-1" />
{batchedCount} notification{batchedCount !== 1 ? "s" : ""} scheduled for later delivery
</p>
</div>
)}
</motion.div>
)}
</AnimatePresence>
{/* ── Settings Modal ────────────────────────────────── */}
<AnimatePresence>
{showSettings && (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4" onClick={() => setShowSettings(false)}>
<motion.div initial={{ scale: 0.95 }} animate={{ scale: 1 }} className="bg-white dark:bg-gray-800 rounded-2xl max-w-md w-full p-6 shadow-xl" onClick={e => e.stopPropagation()}>
<h2 className="text-lg font-bold mb-4 flex items-center gap-2"><Shield className="w-5 h-5 text-primary"/> Notification Settings</h2>
<div className="space-y-4">
{/* Deep Work Mute */}
<label className="flex items-center justify-between cursor-pointer">
<div>
<p className="text-sm font-semibold">Deep Work Mode</p>
<p className="text-xs text-gray-500">Mute all non‑critical notifications during deep work</p>
</div>
<input type="checkbox" checked={settings.deepWorkMuteAll}
onChange={e => updateSettings({ deepWorkMuteAll: e.target.checked })}
className="rounded border-gray-300 text-primary focus:ring-primary" />
</label>
{/* Batch interval */}
<div>
<label className="text-sm font-semibold block mb-1">Batch Delivery Interval</label>
<select value={settings.batchIntervalMinutes}
onChange={e => updateSettings({ batchIntervalMinutes: parseInt(e.target.value) })}
className="w-full text-sm border rounded-lg px-3 py-2 dark:bg-gray-700">
<option value={15}>Every 15 minutes</option>
<option value={30}>Every 30 minutes</option>
<option value={60}>Every hour</option>
<option value={120}>Every 2 hours</option>
</select>
</div>
{/* Quiet hours */}
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-sm font-semibold block mb-1">Quiet Hours Start</label>
<input type="time" value={settings.quietHoursStart}
onChange={e => updateSettings({ quietHoursStart: e.target.value })}
className="w-full text-sm border rounded-lg px-3 py-2 dark:bg-gray-700" />
</div>
<div>
<label className="text-sm font-semibold block mb-1">Quiet Hours End</label>
<input type="time" value={settings.quietHoursEnd}
onChange={e => updateSettings({ quietHoursEnd: e.target.value })}
className="w-full text-sm border rounded-lg px-3 py-2 dark:bg-gray-700" />
</div>
</div>
</div>
<button onClick={() => setShowSettings(false)} className="btn btn-secondary w-full mt-6">Done</button>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</>
);
}

Xet Storage Details

Size:
13.8 kB
Β·
Xet hash:
9dd813e4dcc5a35d48115fa1cfa9528577e47acc60b8381da7f2543f2ad3fd90

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.