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,
sendNotification,
} 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 {
listNotifications,
@ -39,8 +39,12 @@ async function showSystemNotification(notification: OrchaiNotification) {
export default function NotificationCenter() {
const navigate = useNavigate();
const { projectId } = useParams();
const containerRef = useRef<HTMLDivElement | null>(null);
const [open, setOpen] = useState(false);
const [notifications, setNotifications] = useState<OrchaiNotification[]>([]);
const [filter, setFilter] = useState<"all" | "unread" | "errors" | "fixes">(
"all"
);
async function loadNotifications() {
if (!projectId) {
@ -89,11 +93,53 @@ export default function NotificationCenter() {
};
}, [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(
() => notifications.filter((n) => !n.read).length,
[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) {
if (!notification.read) {
try {
@ -130,7 +176,7 @@ export default function NotificationCenter() {
}
return (
<div className="relative">
<div className="relative" ref={containerRef}>
<button
type="button"
onClick={() => setOpen((v) => !v)}
@ -158,13 +204,37 @@ export default function NotificationCenter() {
</button>
</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">
{notifications.length === 0 ? (
{filteredNotifications.length === 0 ? (
<div className="px-3 py-6 text-center text-sm text-gray-400">
No notifications.
</div>
) : (
notifications.map((notification) => (
filteredNotifications.map((notification) => (
<button
key={notification.id}
type="button"