feat: polish notification center filters and interactions
This commit is contained in:
parent
0e0ad80d90
commit
6912e51b14
1 changed files with 74 additions and 4 deletions
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue