feat: implement notification dropdown component and add localized notification strings
This commit is contained in:
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user