umami/src/app/(main)/SideNav.tsx
Mike Cao 50edb71687 Simplify i18n: remove old react-intl artifacts, rename formatMessage to t, replace FormattedMessage with t.rich().
- Rewrite messages.ts to plain string key maps (remove MessageDescriptor)
- Rewrite useMessages hook to expose t from useTranslations() directly
- Rename formatMessage → t across 193 consumer files
- Replace custom FormattedMessage component with next-intl t.rich()
- Update 52 language files to use rich text tags (<b>, <a>)
- Remove all direct imports from @/components/messages in favor of useMessages()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 11:19:04 -08:00

151 lines
4.4 KiB
TypeScript

import {
Button,
type ButtonProps,
Column,
Focusable,
Icon,
Row,
Text,
ThemeButton,
Tooltip,
TooltipTrigger,
} from '@umami/react-zen';
import Link from 'next/link';
import type { Key } from 'react';
import { WebsiteNav } from '@/app/(main)/websites/[websiteId]/WebsiteNav';
import { IconLabel } from '@/components/common/IconLabel';
import { useGlobalState, useMessages, useNavigation } from '@/components/hooks';
import { Globe, Grid2x2, LayoutDashboard, LinkIcon, PanelLeft } from '@/components/icons';
import { LanguageButton } from '@/components/input/LanguageButton';
import { NavButton } from '@/components/input/NavButton';
import { Logo } from '@/components/svg';
export function SideNav(props: any) {
const { t, labels } = useMessages();
const { pathname, renderUrl, websiteId, router } = useNavigation();
const [isCollapsed, setIsCollapsed] = useGlobalState('sidenav-collapsed', false);
const hasNav = !!(websiteId || pathname.startsWith('/admin') || pathname.includes('/settings'));
const links = [
{
id: 'boards',
label: t(labels.boards),
path: '/boards',
icon: <LayoutDashboard />,
},
{
id: 'websites',
label: t(labels.websites),
path: '/websites',
icon: <Globe />,
},
{
id: 'links',
label: t(labels.links),
path: '/links',
icon: <LinkIcon />,
},
{
id: 'pixels',
label: t(labels.pixels),
path: '/pixels',
icon: <Grid2x2 />,
},
];
const handleSelect = (id: Key) => {
router.push(id === 'user' ? '/websites' : `/teams/${id}/websites`);
};
return (
<Column
{...props}
backgroundColor="surface-base"
justifyContent="space-between"
border
borderRadius
paddingX="2"
height="100%"
margin="2"
style={{
width: isCollapsed ? '55px' : '240px',
transition: 'width 0.2s ease-in-out',
overflow: 'hidden',
}}
>
<Column style={{ minHeight: 0, overflowY: 'auto', overflowX: 'hidden' }}>
<Row
alignItems="center"
justifyContent="space-between"
height="60px"
style={{ flexShrink: 0 }}
>
<Row paddingX="3" alignItems="center" justifyContent="space-between" flexGrow={1}>
{!isCollapsed && (
<IconLabel icon={<Logo />}>
<Text weight="bold">umami</Text>
</IconLabel>
)}
<PanelButton />
</Row>
</Row>
<Row marginBottom="4" style={{ flexShrink: 0 }}>
<NavButton showText={!isCollapsed} onAction={handleSelect} />
</Row>
{websiteId ? (
<WebsiteNav websiteId={websiteId} isCollapsed={isCollapsed} />
) : (
<Column gap="2">
{links.map(({ id, path, label, icon }) => {
const isSelected = pathname.startsWith(renderUrl(path, false));
return (
<Link key={id} href={renderUrl(path, false)} role="button">
<TooltipTrigger isDisabled={!isCollapsed} delay={0}>
<Focusable>
<Row
alignItems="center"
hover={{ backgroundColor: 'surface-sunken' }}
backgroundColor={isSelected ? 'surface-sunken' : undefined}
borderRadius
minHeight="40px"
>
<IconLabel
icon={icon}
label={isCollapsed ? '' : label}
weight={isSelected ? 'bold' : undefined}
padding
/>
</Row>
</Focusable>
<Tooltip placement="right">{label}</Tooltip>
</TooltipTrigger>
</Link>
);
})}
</Column>
)}
</Column>
<Row alignItems="center" justifyContent="center" wrap="wrap" marginBottom="4" gap>
<LanguageButton />
<ThemeButton />
</Row>
</Column>
);
}
const PanelButton = (props: ButtonProps) => {
const [isCollapsed, setIsCollapsed] = useGlobalState('sidenav-collapsed', false);
return (
<Button
onPress={() => setIsCollapsed(!isCollapsed)}
variant="zero"
{...props}
style={{ padding: 0 }}
>
<Icon strokeColor="muted">
<PanelLeft />
</Icon>
</Button>
);
};