feat: implement notification dropdown component and add localized notification strings

This commit is contained in:
Firman Ramdhani
2026-07-03 16:08:29 +07:00
parent 6df6880ba3
commit 1de110818a
4 changed files with 169 additions and 5 deletions
@@ -6,7 +6,6 @@ import {
Text, Text,
useCoreAppShell, useCoreAppShell,
ActionIcon, ActionIcon,
Indicator,
Menu, Menu,
Avatar, Avatar,
UnstyledButton, UnstyledButton,
@@ -14,6 +13,7 @@ import {
} from '@repo/ui/components'; } from '@repo/ui/components';
import { Globe, Bell, HelpCircle, Settings, User, Briefcase, LogOut, ChevronDown } from 'lucide-react'; import { Globe, Bell, HelpCircle, Settings, User, Briefcase, LogOut, ChevronDown } from 'lucide-react';
import { AppStorageKey, secureStorage } from '../../../../core/storage/local'; import { AppStorageKey, secureStorage } from '../../../../core/storage/local';
import { NotificationDropdown } from './notifications/notification-dropdown';
// Dummy user data for preview // Dummy user data for preview
const USER = { const USER = {
@@ -60,11 +60,11 @@ export default function HeaderLayout() {
<HelpCircle size={18} /> <HelpCircle size={18} />
</ActionIcon> </ActionIcon>
<Indicator inline size={9} offset={4} color="red" withBorder> <NotificationDropdown>
<ActionIcon variant="subtle" color="gray" size="lg" aria-label="Notifications"> <ActionIcon variant="subtle" color="gray" size="lg" aria-label="Notifications">
<Bell size={18} /> <Bell size={18} />
</ActionIcon> </ActionIcon>
</Indicator> </NotificationDropdown>
<Menu shadow="md" width={260} position="bottom-end" radius="md" withArrow> <Menu shadow="md" width={260} position="bottom-end" radius="md" withArrow>
<Menu.Target> <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>
);
}
@@ -19,6 +19,14 @@
"switchWorkspace": "Switch Workspace", "switchWorkspace": "Switch Workspace",
"signOut": "Sign Out", "signOut": "Sign Out",
"english": "English", "english": "English",
"indonesian": "Indonesia" "indonesian": "Indonesia",
"notifications_title": "Notifications",
"notifications_unread": "Unread",
"notifications_all": "All",
"notifications_markAllAsRead": "Mark all as read",
"notifications_viewAll": "View all notifications",
"notifications_emptyState": "No new notifications",
"notifications_justNow": "Just now",
"notifications_hoursAgo": "hours ago"
} }
} }
@@ -19,6 +19,14 @@
"switchWorkspace": "Ganti Ruang Kerja", "switchWorkspace": "Ganti Ruang Kerja",
"signOut": "Keluar", "signOut": "Keluar",
"english": "Inggris", "english": "Inggris",
"indonesian": "Indonesia" "indonesian": "Bahasa Indonesia",
"notifications_title": "Notifikasi",
"notifications_unread": "Belum dibaca",
"notifications_all": "Semua",
"notifications_markAllAsRead": "Tandai semua dibaca",
"notifications_viewAll": "Lihat semua notifikasi",
"notifications_emptyState": "Tidak ada notifikasi baru",
"notifications_justNow": "Baru saja",
"notifications_hoursAgo": "jam lalu"
} }
} }