feat: polish notification center filters and interactions

This commit is contained in:
thibaud-leclere 2026-04-14 10:35:06 +02:00
parent 0e0ad80d90
commit 6912e51b14

View file

@ -4,7 +4,7 @@ import {
requestPermission, requestPermission,
sendNotification, sendNotification,
} from "@tauri-apps/plugin-notification"; } from "@tauri-apps/plugin-notification";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { import {
listNotifications, listNotifications,
@ -39,8 +39,12 @@ async function showSystemNotification(notification: OrchaiNotification) {
export default function NotificationCenter() { export default function NotificationCenter() {
const navigate = useNavigate(); const navigate = useNavigate();
const { projectId } = useParams(); const { projectId } = useParams();
const containerRef = useRef<HTMLDivElement | null>(null);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [notifications, setNotifications] = useState<OrchaiNotification[]>([]); const [notifications, setNotifications] = useState<OrchaiNotification[]>([]);
const [filter, setFilter] = useState<"all" | "unread" | "errors" | "fixes">(
"all"
);
async function loadNotifications() { async function loadNotifications() {
if (!projectId) { if (!projectId) {
@ -89,11 +93,53 @@ export default function NotificationCenter() {
}; };
}, [projectId]); }, [projectId]);
useEffect(() => {
if (!open) {
return;
}
function handleOutsideClick(event: MouseEvent) {
const target = event.target as Node | null;
if (!containerRef.current || !target) {
return;
}
if (!containerRef.current.contains(target)) {
setOpen(false);
}
}
function handleEscape(event: KeyboardEvent) {
if (event.key === "Escape") {
setOpen(false);
}
}
document.addEventListener("mousedown", handleOutsideClick);
document.addEventListener("keydown", handleEscape);
return () => {
document.removeEventListener("mousedown", handleOutsideClick);
document.removeEventListener("keydown", handleEscape);
};
}, [open]);
const unreadCount = useMemo( const unreadCount = useMemo(
() => notifications.filter((n) => !n.read).length, () => notifications.filter((n) => !n.read).length,
[notifications] [notifications]
); );
const filteredNotifications = useMemo(() => {
switch (filter) {
case "unread":
return notifications.filter((n) => !n.read);
case "errors":
return notifications.filter((n) => n.notification_type === "Error");
case "fixes":
return notifications.filter((n) => n.notification_type === "FixReady");
default:
return notifications;
}
}, [filter, notifications]);
async function handleOpenNotification(notification: OrchaiNotification) { async function handleOpenNotification(notification: OrchaiNotification) {
if (!notification.read) { if (!notification.read) {
try { try {
@ -130,7 +176,7 @@ export default function NotificationCenter() {
} }
return ( return (
<div className="relative"> <div className="relative" ref={containerRef}>
<button <button
type="button" type="button"
onClick={() => setOpen((v) => !v)} onClick={() => setOpen((v) => !v)}
@ -158,13 +204,37 @@ export default function NotificationCenter() {
</button> </button>
</div> </div>
<div className="flex gap-1 border-b border-gray-100 px-2 py-2">
{[
{ id: "all", label: "All" },
{ id: "unread", label: "Unread" },
{ id: "errors", label: "Errors" },
{ id: "fixes", label: "Fixes" },
].map((item) => (
<button
key={item.id}
type="button"
onClick={() =>
setFilter(item.id as "all" | "unread" | "errors" | "fixes")
}
className={`rounded px-2 py-1 text-xs ${
filter === item.id
? "bg-gray-900 text-white"
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
}`}
>
{item.label}
</button>
))}
</div>
<div className="max-h-[420px] overflow-y-auto"> <div className="max-h-[420px] overflow-y-auto">
{notifications.length === 0 ? ( {filteredNotifications.length === 0 ? (
<div className="px-3 py-6 text-center text-sm text-gray-400"> <div className="px-3 py-6 text-center text-sm text-gray-400">
No notifications. No notifications.
</div> </div>
) : ( ) : (
notifications.map((notification) => ( filteredNotifications.map((notification) => (
<button <button
key={notification.id} key={notification.id}
type="button" type="button"