feat: notification center frontend core (PR-3)
- Bell icon in icon registry (Phosphor Bell) - NotificationProvider context with polling (15s unread / 30s list) - useNotifications() hook with optimistic updates - NotificationCenter component: bell + badge + dropdown panel - NotificationItem component: severity icon, title, relative time, actions - AppShell integration: mount in header-actions, hidden on mobile - CSS styles: dropdown, items, unread/read states, empty state - formatRelativeTime utility (custom, no new deps) - 25 frontend tests (9 hook + 6 item + 10 center) Quality gates: vitest 25 passed, tsc clean, eslint clean
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
import { Icon } from "./icon";
|
||||
import { formatRelativeTime } from "../utils/time";
|
||||
import type { NotificationItem as NotificationItemType } from "../api/notifications";
|
||||
|
||||
export interface NotificationItemProps {
|
||||
notification: NotificationItemType;
|
||||
onMarkRead: (id: string) => void;
|
||||
onDismiss: (id: string) => void;
|
||||
}
|
||||
|
||||
import type { IconName } from "../utils/icons";
|
||||
|
||||
const severityIconMap: Record<string, IconName> = {
|
||||
info: "info",
|
||||
warning: "warning",
|
||||
error: "error",
|
||||
success: "success",
|
||||
};
|
||||
|
||||
export function NotificationItem({
|
||||
notification,
|
||||
onMarkRead,
|
||||
onDismiss,
|
||||
}: NotificationItemProps) {
|
||||
const isUnread = notification.read_at === null;
|
||||
const iconName = severityIconMap[notification.severity] ?? "info";
|
||||
|
||||
return (
|
||||
<li
|
||||
role="listitem"
|
||||
className={`notification-item ${isUnread ? "notification-item--unread" : "notification-item--read"}`}
|
||||
>
|
||||
<div className="notification-item-icon">
|
||||
<Icon name={iconName} size="md" />
|
||||
</div>
|
||||
<div className="notification-item-content">
|
||||
<div className="notification-item-title">{notification.title}</div>
|
||||
<div className="notification-item-time">
|
||||
{formatRelativeTime(notification.created_at)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="notification-item-actions">
|
||||
{isUnread && (
|
||||
<button
|
||||
type="button"
|
||||
className="notification-item-action"
|
||||
onClick={() => onMarkRead(notification.id)}
|
||||
aria-label="Mark read"
|
||||
>
|
||||
Mark read
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="notification-item-action"
|
||||
onClick={() => onDismiss(notification.id)}
|
||||
aria-label="Dismiss"
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user