feat: implement notification dropdown component and add localized notification strings
This commit is contained in:
@@ -6,7 +6,6 @@ import {
|
||||
Text,
|
||||
useCoreAppShell,
|
||||
ActionIcon,
|
||||
Indicator,
|
||||
Menu,
|
||||
Avatar,
|
||||
UnstyledButton,
|
||||
@@ -14,6 +13,7 @@ import {
|
||||
} from '@repo/ui/components';
|
||||
import { Globe, Bell, HelpCircle, Settings, User, Briefcase, LogOut, ChevronDown } from 'lucide-react';
|
||||
import { AppStorageKey, secureStorage } from '../../../../core/storage/local';
|
||||
import { NotificationDropdown } from './notifications/notification-dropdown';
|
||||
|
||||
// Dummy user data for preview
|
||||
const USER = {
|
||||
@@ -60,11 +60,11 @@ export default function HeaderLayout() {
|
||||
<HelpCircle size={18} />
|
||||
</ActionIcon>
|
||||
|
||||
<Indicator inline size={9} offset={4} color="red" withBorder>
|
||||
<NotificationDropdown>
|
||||
<ActionIcon variant="subtle" color="gray" size="lg" aria-label="Notifications">
|
||||
<Bell size={18} />
|
||||
</ActionIcon>
|
||||
</Indicator>
|
||||
</NotificationDropdown>
|
||||
|
||||
<Menu shadow="md" width={260} position="bottom-end" radius="md" withArrow>
|
||||
<Menu.Target>
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from '@repo/core-i18n';
|
||||
import {
|
||||
Popover,
|
||||
Group,
|
||||
Text,
|
||||
UnstyledButton,
|
||||
ScrollArea,
|
||||
Box,
|
||||
Indicator,
|
||||
Button,
|
||||
Center,
|
||||
Stack,
|
||||
NavLink,
|
||||
} from '@repo/ui/components';
|
||||
import { BellOff } from 'lucide-react';
|
||||
|
||||
interface NotificationItem {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
timestamp: string;
|
||||
isRead: boolean;
|
||||
}
|
||||
|
||||
interface NotificationDropdownProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function NotificationDropdown({ children }: NotificationDropdownProps) {
|
||||
const { t } = useTranslation();
|
||||
const [opened, setOpened] = useState(false);
|
||||
|
||||
const [notifications, setNotifications] = useState<NotificationItem[]>([
|
||||
{
|
||||
id: '1',
|
||||
title: 'New User Registration',
|
||||
description: 'John Doe has registered a new account in the system.',
|
||||
timestamp: t('common:notifications_justNow'),
|
||||
isRead: false,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'System Update Completed',
|
||||
description: 'The ERP database has been successfully updated to v2.4.',
|
||||
timestamp: `2 ${t('common:notifications_hoursAgo')}`,
|
||||
isRead: false,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'Weekly Report Ready',
|
||||
description: 'Your weekly financial summary is ready to be downloaded.',
|
||||
timestamp: `5 ${t('common:notifications_hoursAgo')}`,
|
||||
isRead: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const unreadCount = notifications.filter((n) => !n.isRead).length;
|
||||
|
||||
const handleMarkAllAsRead = () => {
|
||||
setNotifications((prev) => prev.map((n) => ({ ...n, isRead: true })));
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover opened={opened} onChange={setOpened} width={340} position="bottom-end" withArrow shadow="md" radius="md">
|
||||
<Popover.Target>
|
||||
{/* Trigger wrapped around the Bell icon */}
|
||||
<Box onClick={() => setOpened((o) => !o)} style={{ cursor: 'pointer' }}>
|
||||
<Indicator inline size={9} offset={4} color="red" withBorder disabled={unreadCount === 0}>
|
||||
{children}
|
||||
</Indicator>
|
||||
</Box>
|
||||
</Popover.Target>
|
||||
|
||||
<Popover.Dropdown p={0}>
|
||||
<Group
|
||||
justify="space-between"
|
||||
px="md"
|
||||
py="xs"
|
||||
style={{ borderBottom: '1px solid var(--mantine-color-default-border)' }}
|
||||
>
|
||||
<Text fw={600} size="sm">
|
||||
{t('common:notifications_title')}
|
||||
</Text>
|
||||
{unreadCount > 0 && (
|
||||
<UnstyledButton onClick={handleMarkAllAsRead}>
|
||||
<Text size="xs" c="blue" fw={500} style={{ '&:hover': { textDecoration: 'underline' } }}>
|
||||
{t('common:notifications_markAllAsRead')}
|
||||
</Text>
|
||||
</UnstyledButton>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
<ScrollArea.Autosize mah={300}>
|
||||
{notifications.length === 0 ? (
|
||||
<Center p="xl" style={{ minHeight: 150 }}>
|
||||
<Stack align="center" gap="xs">
|
||||
<BellOff size={32} style={{ opacity: 0.2 }} />
|
||||
<Text size="sm" c="dimmed">
|
||||
{t('common:notifications_emptyState')}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
) : (
|
||||
notifications.map((item) => (
|
||||
<NavLink
|
||||
key={item.id}
|
||||
label={
|
||||
<Box>
|
||||
<Text size="sm" fw={item.isRead ? 500 : 600} lh={1.2} mb={4}>
|
||||
{item.title}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" lh={1.3} mb={6} style={{ whiteSpace: 'normal' }}>
|
||||
{item.description}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" style={{ opacity: 0.7 }}>
|
||||
{item.timestamp}
|
||||
</Text>
|
||||
</Box>
|
||||
}
|
||||
leftSection={
|
||||
<Box style={{ alignSelf: 'flex-start', marginTop: '4px' }}>
|
||||
<Indicator size={8} color="blue" disabled={item.isRead}>
|
||||
<Box w={8} h={8} />
|
||||
</Indicator>
|
||||
</Box>
|
||||
}
|
||||
active={!item.isRead}
|
||||
variant="light"
|
||||
style={{
|
||||
borderBottom: '1px solid var(--mantine-color-default-border)',
|
||||
}}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</ScrollArea.Autosize>
|
||||
|
||||
{notifications.length > 0 && (
|
||||
<Box p="xs">
|
||||
<Button variant="light" color="gray" fullWidth size="xs" radius="md">
|
||||
{t('common:notifications_viewAll')}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user