feat: enhance HeaderLayout with user profile menu and notifications, update translations for common terms

This commit is contained in:
Firman Ramdhani
2026-07-03 15:51:00 +07:00
parent 5040d41152
commit f9df63a27d
3 changed files with 174 additions and 23 deletions
@@ -1,31 +1,162 @@
import { useTranslation } from '@repo/core-i18n';
import { Burger, Group, Select, Text, useCoreAppShell } from '@repo/ui/components';
import { Globe } from 'lucide-react';
import {
Burger,
Group,
Select,
Text,
useCoreAppShell,
ActionIcon,
Indicator,
Menu,
Avatar,
UnstyledButton,
Box,
} from '@repo/ui/components';
import { Globe, Bell, HelpCircle, Settings, User, Briefcase, LogOut, ChevronDown } from 'lucide-react';
import { AppStorageKey, secureStorage } from '../../../../core/storage/local';
// Dummy user data for preview
const USER = {
name: 'Jane Doe',
email: 'jane.doe@enterprise.com',
role: 'System Administrator',
// avatar: null as string | null, // Simulated null for testing fallback
avatar: 'https://i.pravatar.cc/150?u=jane.doe',
};
const getInitials = (name: string): string => {
const parts = name.trim().split(/\s+/);
if (parts.length === 0) return '';
if (parts.length === 1) return parts[0].substring(0, 2).toUpperCase();
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
};
export default function HeaderLayout() {
const { i18n } = useTranslation();
const { i18n, t } = useTranslation();
const { mobileOpened, toggleMobile } = useCoreAppShell();
return (
<Group h="100%" px="md" justify="space-between">
<Group>
<Group
h="100%"
px="md"
justify="space-between"
bg="var(--mantine-color-body)"
style={{ borderBottom: '1px solid var(--mantine-color-default-border)' }}
wrap="nowrap"
>
{/* 1. LEFT SECTION (Navigation & Context) */}
<Group wrap="nowrap" gap="sm">
<Burger opened={mobileOpened} onClick={toggleMobile} hiddenFrom="sm" size="sm" />
<Text>LOGO</Text>
<Select
size="xs"
variant="filled"
leftSection={<Globe size={16} />}
data={[
{ value: 'en', label: 'English' },
{ value: 'id', label: 'Bahasa Indonesia' },
]}
value={i18n.resolvedLanguage || i18n.language}
onChange={async (val) => {
if (!val) return;
await i18n.changeLanguage(val);
await secureStorage.setItem(AppStorageKey.LOCALE, val);
}}
/>
{/* Contextual Title / Breadcrumb Placeholder */}
<Text fw={600} size="sm" c="dimmed" visibleFrom="sm" style={{ letterSpacing: '0.5px' }}>
LOGO
</Text>
</Group>
{/* 3. RIGHT SECTION (Actions & Profile) */}
<Group wrap="nowrap" gap="xs">
<ActionIcon variant="subtle" color="gray" size="lg" aria-label="Help">
<HelpCircle size={18} />
</ActionIcon>
<Indicator inline size={9} offset={4} color="red" withBorder>
<ActionIcon variant="subtle" color="gray" size="lg" aria-label="Notifications">
<Bell size={18} />
</ActionIcon>
</Indicator>
<Menu shadow="md" width={260} position="bottom-end" radius="md" withArrow>
<Menu.Target>
<UnstyledButton
style={{
padding: '4px',
borderRadius: 'var(--mantine-radius-xl)',
transition: 'background-color 150ms ease',
}}
>
<Group gap="sm" wrap="nowrap">
<Avatar src={USER.avatar} size={28} radius="xl" color="blue">
{getInitials(USER.name)}
</Avatar>
<Box visibleFrom="sm" style={{ textAlign: 'left' }}>
<Text size="sm" fw={600} lh={1.1} maw={140} truncate="end">
{USER.name}
</Text>
<Text size="xs" c="dimmed" maw={140} truncate="end">
{USER.role}
</Text>
</Box>
<ChevronDown size={14} style={{ opacity: 0.5 }} className="mantine-visible-from-sm" />
</Group>
</UnstyledButton>
</Menu.Target>
<Menu.Dropdown>
<Box p="xs" hiddenFrom="sm">
<Text size="sm" fw={600} truncate="end">
{USER.name}
</Text>
<Text size="xs" c="dimmed" truncate="end">
{USER.email}
</Text>
</Box>
<Menu.Divider hiddenFrom="sm" />
<Box px="sm">
<Menu.Label>{t('common:preferences', { ns: 'common' })}</Menu.Label>
<Group px="sm" justify="space-between" wrap="nowrap">
<Group gap="xs" wrap="nowrap">
<Globe size={14} style={{ opacity: 0.8 }} />
<Text size="sm">{t('common:language', { ns: 'common' })}</Text>
</Group>
<Select
size="xs"
variant="unstyled"
w={110}
data={[
{ value: 'en', label: t('common:english', { ns: 'common' }) },
{ value: 'id', label: t('common:indonesian', { ns: 'common' }) },
]}
value={i18n.resolvedLanguage || i18n.language}
onChange={async (val) => {
if (!val) return;
await i18n.changeLanguage(val);
await secureStorage.setItem(AppStorageKey.LOCALE, val);
}}
styles={{ input: { textAlign: 'right' } }}
/>
</Group>
</Box>
<Menu.Divider />
<Box px="sm">
<Menu.Label>{t('common:application', { ns: 'common' })}</Menu.Label>
<Menu.Item leftSection={<Settings size={14} />}>
{t('common:accountSettings', { ns: 'common' })}
</Menu.Item>
<Menu.Item leftSection={<User size={14} />}>{t('common:myProfile', { ns: 'common' })}</Menu.Item>
<Menu.Divider />
<Menu.Label>{t('common:workspace', { ns: 'common' })}</Menu.Label>
<Menu.Item leftSection={<Briefcase size={14} />}>
{t('common:switchWorkspace', { ns: 'common' })}
</Menu.Item>
</Box>
<Box px="sm">
<Menu.Divider />
<Menu.Item color="red" leftSection={<LogOut size={14} />}>
{t('common:signOut', { ns: 'common' })}
</Menu.Item>
</Box>
</Menu.Dropdown>
</Menu>
</Group>
</Group>
);