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:
2026-05-29 13:14:50 +02:00
parent cbd3436ff7
commit 2bec205a30
15 changed files with 1974 additions and 402 deletions
+64
View File
@@ -0,0 +1,64 @@
import { apiClient } from "./client";
export interface NotificationItem {
id: string;
user_id: string;
category: string;
severity: "info" | "warning" | "error" | "success";
title: string;
message: string | null;
source_type: string | null;
source_id: string | null;
metadata: Record<string, unknown>;
read_at: string | null;
dismissed_at: string | null;
created_at: string;
}
export interface NotificationListResponse {
items: NotificationItem[];
total: number;
limit: number;
offset: number;
}
export interface UnreadCountResponse {
count: number;
}
export interface MarkAllReadResponse {
marked_count: number;
}
export const getNotifications = async (): Promise<NotificationListResponse> => {
const response =
await apiClient.get<NotificationListResponse>("/notifications");
return response.data;
};
export const getUnreadCount = async (): Promise<number> => {
const response = await apiClient.get<UnreadCountResponse>(
"/notifications/unread",
);
return response.data.count;
};
export const markNotificationRead = async (
id: string,
): Promise<NotificationItem> => {
const response = await apiClient.patch<NotificationItem>(
`/notifications/${id}/read`,
);
return response.data;
};
export const markAllNotificationsRead = async (): Promise<number> => {
const response = await apiClient.post<MarkAllReadResponse>(
"/notifications/mark-all-read",
);
return response.data.marked_count;
};
export const dismissNotification = async (id: string): Promise<void> => {
await apiClient.delete(`/notifications/${id}`);
};