chore: add notification to packages

This commit is contained in:
Nathnael
2026-07-06 06:52:47 +00:00
parent c8f65829d1
commit 01b92072aa
6 changed files with 445 additions and 35 deletions

View File

@@ -0,0 +1,166 @@
import {
ActionIcon,
Box,
Button,
Group,
Indicator,
Loader,
Menu,
ScrollArea,
Stack,
Text,
} from "@mantine/core";
import { Bell, CheckCheck } from "lucide-react";
/** Minimal shape the bell needs to render one row. */
export interface NotificationBellItem {
id: string;
title: string;
body: string;
createdAt: string;
isRead: boolean;
link?: string | null;
}
export interface NotificationBellProps {
items: NotificationBellItem[];
unreadCount: number;
loading?: boolean;
/** Fired when the dropdown opens — a good place to refetch. */
onOpen?: () => void;
onItemClick?: (item: NotificationBellItem) => void;
onMarkAllRead?: () => void;
emptyLabel?: string;
/** Extra className for the trigger button (e.g. the app's header "island"). */
triggerClassName?: string;
}
const timeAgo = (iso: string): string => {
const then = new Date(iso).getTime();
if (Number.isNaN(then)) return "";
const secs = Math.max(0, Math.floor((Date.now() - then) / 1000));
if (secs < 60) return "just now";
const mins = Math.floor(secs / 60);
if (mins < 60) return `${mins}m ago`;
const hrs = Math.floor(mins / 60);
if (hrs < 24) return `${hrs}h ago`;
const days = Math.floor(hrs / 24);
if (days < 7) return `${days}d ago`;
return new Date(iso).toLocaleDateString();
};
/**
* Presentational notification bell + dropdown. Owns no data or transport — the
* hosting app wires react-query/websocket and passes items + handlers. Built on
* Mantine primitives to match the freight app headers.
*/
export function NotificationBell({
items,
unreadCount,
loading = false,
onOpen,
onItemClick,
onMarkAllRead,
emptyLabel = "You're all caught up",
triggerClassName,
}: NotificationBellProps) {
const hasUnread = unreadCount > 0;
return (
<Menu
width={360}
position="bottom-end"
offset={8}
radius="md"
shadow="md"
withinPortal
onOpen={onOpen}
>
<Menu.Target>
<Indicator
color="red"
size={16}
offset={4}
disabled={!hasUnread}
label={unreadCount > 99 ? "99+" : unreadCount}
styles={{ indicator: { fontSize: 10, fontWeight: 700, padding: "0 4px" } }}
>
<ActionIcon
variant="subtle"
radius="xl"
size={36}
aria-label="Notifications"
className={triggerClassName}
>
<Bell size={17} strokeWidth={1.8} />
</ActionIcon>
</Indicator>
</Menu.Target>
<Menu.Dropdown p={0}>
<Group justify="space-between" px="sm" py="xs" wrap="nowrap">
<Text fw={600} size="sm">
Notifications
</Text>
{hasUnread && onMarkAllRead && (
<Button
variant="subtle"
size="compact-xs"
leftSection={<CheckCheck size={13} />}
onClick={onMarkAllRead}
>
Mark all read
</Button>
)}
</Group>
<Menu.Divider m={0} />
{loading && items.length === 0 ? (
<Group justify="center" py="lg">
<Loader size="sm" />
</Group>
) : items.length === 0 ? (
<Box py="xl" px="md">
<Text c="dimmed" size="sm" ta="center">
{emptyLabel}
</Text>
</Box>
) : (
<ScrollArea.Autosize mah={380} type="hover">
<Stack gap={0}>
{items.map((item) => (
<Box
key={item.id}
px="sm"
py="xs"
onClick={() => onItemClick?.(item)}
style={{
cursor: onItemClick ? "pointer" : "default",
background: item.isRead
? undefined
: "var(--mantine-color-blue-light)",
borderBottom: "1px solid var(--mantine-color-default-border)",
}}
>
<Group justify="space-between" wrap="nowrap" gap={8}>
<Text fw={item.isRead ? 500 : 700} size="sm" lineClamp={1}>
{item.title}
</Text>
<Text c="dimmed" size="xs" style={{ whiteSpace: "nowrap" }}>
{timeAgo(item.createdAt)}
</Text>
</Group>
<Text c="dimmed" size="xs" lineClamp={2}>
{item.body}
</Text>
</Box>
))}
</Stack>
</ScrollArea.Autosize>
)}
</Menu.Dropdown>
</Menu>
);
}
export default NotificationBell;

View File

@@ -0,0 +1,5 @@
export { NotificationBell, default } from "./NotificationBell";
export type {
NotificationBellProps,
NotificationBellItem,
} from "./NotificationBell";

View File

@@ -28,6 +28,12 @@ export type { OperationDatePickerProps } from "./components/OperationDatePicker"
export { CountdownTimer } from "./components/CountdownTimer";
export type { CountdownTimerProps } from "./components/CountdownTimer";
export { NotificationBell } from "./components/NotificationBell";
export type {
NotificationBellProps,
NotificationBellItem,
} from "./components/NotificationBell";
export { Badge } from "./components/badge";
// export type { BadgeProps } from "./components/badge";