New website nav.
This commit is contained in:
1 parent
5e6799a715
commit
a534c51b5e
38 files changed
+190
-159
No files matched your search
+176
-20
@@ -1,5 +1,14 @@
|
||||
import { ReactNode } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Sidebar, SidebarHeader, SidebarSection, SidebarItem } from '@umami/react-zen';
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarHeader,
|
||||
SidebarSection,
|
||||
SidebarItem,
|
||||
Button,
|
||||
Icon,
|
||||
Row,
|
||||
} from '@umami/react-zen';
|
||||
import {
|
||||
Globe,
|
||||
LayoutDashboard,
|
||||
@@ -8,62 +17,209 @@ import {
|
||||
Grid2X2,
|
||||
Settings,
|
||||
LockKeyhole,
|
||||
PanelLeft,
|
||||
Eye,
|
||||
Lightning,
|
||||
User,
|
||||
Clock,
|
||||
Target,
|
||||
Funnel,
|
||||
Path,
|
||||
Magnet,
|
||||
Tag,
|
||||
Money,
|
||||
Network,
|
||||
Arrow,
|
||||
Sheet,
|
||||
} from '@/components/icons';
|
||||
import { useMessages, useNavigation, useGlobalState } from '@/components/hooks';
|
||||
import { LinkButton } from '@/components/common/LinkButton';
|
||||
|
||||
type NavLink = {
|
||||
id: string;
|
||||
label: string;
|
||||
path: string;
|
||||
icon: ReactNode;
|
||||
};
|
||||
|
||||
export function SideNav(props: any) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { renderUrl, pathname } = useNavigation();
|
||||
const [isCollapsed] = useGlobalState('sidenav-collapsed');
|
||||
const { websiteId, pathname, renderUrl } = useNavigation();
|
||||
const [isCollapsed, setCollapsed] = useGlobalState('sidenav-collapsed');
|
||||
const isWebsite = websiteId && !pathname.includes('/settings');
|
||||
|
||||
const links = [
|
||||
{
|
||||
id: 'websites',
|
||||
label: formatMessage(labels.websites),
|
||||
href: renderUrl('/websites', false),
|
||||
path: '/websites',
|
||||
icon: <Globe />,
|
||||
},
|
||||
{
|
||||
id: 'boards',
|
||||
label: formatMessage(labels.boards),
|
||||
href: renderUrl('/boards', false),
|
||||
path: '/boards',
|
||||
icon: <LayoutDashboard />,
|
||||
},
|
||||
{
|
||||
id: 'links',
|
||||
label: formatMessage(labels.links),
|
||||
href: renderUrl('/links', false),
|
||||
path: '/links',
|
||||
icon: <LinkIcon />,
|
||||
},
|
||||
{
|
||||
id: 'pixels',
|
||||
label: formatMessage(labels.pixels),
|
||||
href: renderUrl('/pixels', false),
|
||||
path: '/pixels',
|
||||
icon: <Grid2X2 />,
|
||||
},
|
||||
];
|
||||
|
||||
const websiteLinks = [
|
||||
{
|
||||
id: 'overview',
|
||||
label: formatMessage(labels.overview),
|
||||
icon: <Eye />,
|
||||
path: '/',
|
||||
},
|
||||
{
|
||||
id: 'events',
|
||||
label: formatMessage(labels.events),
|
||||
icon: <Lightning />,
|
||||
path: '/events',
|
||||
},
|
||||
{
|
||||
id: 'sessions',
|
||||
label: formatMessage(labels.sessions),
|
||||
icon: <User />,
|
||||
path: '/sessions',
|
||||
},
|
||||
{
|
||||
id: 'realtime',
|
||||
label: formatMessage(labels.realtime),
|
||||
icon: <Clock />,
|
||||
path: '/realtime',
|
||||
},
|
||||
{
|
||||
id: 'breakdown',
|
||||
label: formatMessage(labels.breakdown),
|
||||
icon: <Sheet />,
|
||||
path: '/breakdown',
|
||||
},
|
||||
{
|
||||
id: 'goals',
|
||||
label: formatMessage(labels.goals),
|
||||
icon: <Target />,
|
||||
path: '/goals',
|
||||
},
|
||||
{
|
||||
id: 'funnel',
|
||||
label: formatMessage(labels.funnels),
|
||||
icon: <Funnel />,
|
||||
path: '/funnels',
|
||||
},
|
||||
{
|
||||
id: 'journeys',
|
||||
label: formatMessage(labels.journeys),
|
||||
icon: <Path />,
|
||||
path: '/journeys',
|
||||
},
|
||||
{
|
||||
id: 'retention',
|
||||
label: formatMessage(labels.retention),
|
||||
icon: <Magnet />,
|
||||
path: '/retention',
|
||||
},
|
||||
{
|
||||
id: 'utm',
|
||||
label: formatMessage(labels.utm),
|
||||
icon: <Tag />,
|
||||
path: '/utm',
|
||||
},
|
||||
{
|
||||
id: 'revenue',
|
||||
label: formatMessage(labels.revenue),
|
||||
icon: <Money />,
|
||||
path: '/revenue',
|
||||
},
|
||||
{
|
||||
id: 'attribution',
|
||||
label: formatMessage(labels.attribution),
|
||||
icon: <Network />,
|
||||
path: '/attribution',
|
||||
},
|
||||
];
|
||||
|
||||
const bottomLinks = [
|
||||
{
|
||||
id: 'settings',
|
||||
label: formatMessage(labels.settings),
|
||||
href: '/settings',
|
||||
path: '/settings',
|
||||
icon: <Settings />,
|
||||
},
|
||||
{
|
||||
id: 'admin',
|
||||
label: formatMessage(labels.admin),
|
||||
href: '/admin',
|
||||
path: '/admin',
|
||||
icon: <LockKeyhole />,
|
||||
},
|
||||
].filter(n => n);
|
||||
];
|
||||
|
||||
return (
|
||||
<Sidebar {...props} isCollapsed={isCollapsed} variant="0" showBorder={true}>
|
||||
<Sidebar {...props} isCollapsed={isCollapsed} variant="quiet" showBorder={true}>
|
||||
<SidebarSection>
|
||||
<SidebarHeader label="umami" icon={<Logo />} />
|
||||
</SidebarSection>
|
||||
<SidebarSection>
|
||||
{links.map(({ href, label, icon }) => {
|
||||
return (
|
||||
<Link key={href} href={href} role="button">
|
||||
<SidebarItem label={label} icon={icon} isSelected={pathname.startsWith(href)} />
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
<SidebarSection flexGrow={1}>
|
||||
{!isWebsite && <NavItems items={links} params={false} />}
|
||||
{isWebsite && (
|
||||
<>
|
||||
<Row>
|
||||
<LinkButton href={renderUrl('/websites', false)} variant="outline">
|
||||
<Icon rotate={180}>
|
||||
<Arrow />
|
||||
</Icon>
|
||||
</LinkButton>
|
||||
</Row>
|
||||
<NavItems items={websiteLinks} prefix={`/websites/${websiteId}`} />
|
||||
</>
|
||||
)}
|
||||
</SidebarSection>
|
||||
<SidebarSection>
|
||||
{!isWebsite && <NavItems items={bottomLinks} params={false} />}
|
||||
</SidebarSection>
|
||||
<SidebarSection>
|
||||
<Row>
|
||||
<Button onPress={() => setCollapsed(!isCollapsed)} variant="quiet">
|
||||
<Icon>
|
||||
<PanelLeft />
|
||||
</Icon>
|
||||
</Button>
|
||||
</Row>
|
||||
</SidebarSection>
|
||||
<SidebarSection alignSelf="end">{``}</SidebarSection>
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
|
||||
const NavItems = ({
|
||||
items,
|
||||
prefix = '',
|
||||
params,
|
||||
}: {
|
||||
items: NavLink[];
|
||||
prefix?: string;
|
||||
params?: Record<string, string | number> | false;
|
||||
}) => {
|
||||
const { renderUrl, pathname, websiteId } = useNavigation();
|
||||
|
||||
return items.map(({ id, path, label, icon }) => {
|
||||
const isSelected = websiteId
|
||||
? (path === '/' && pathname.endsWith(websiteId)) || pathname.endsWith(path)
|
||||
: pathname.startsWith(path);
|
||||
return (
|
||||
<Link key={id} href={renderUrl(`${prefix}${path}`, params)} role="button">
|
||||
<SidebarItem label={label} icon={icon} isSelected={isSelected} />
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
};
|
||||
@@ -1,13 +1,12 @@
|
||||
import { ThemeButton, Row, Button, Icon } from '@umami/react-zen';
|
||||
import { ThemeButton, Row, Icon } from '@umami/react-zen';
|
||||
import { LanguageButton } from '@/components/input/LanguageButton';
|
||||
import { ProfileButton } from '@/components/input/ProfileButton';
|
||||
import { TeamsButton } from '@/components/input/TeamsButton';
|
||||
import { WebsiteSelect } from '@/components/input/WebsiteSelect';
|
||||
import { PanelLeft, Slash } from '@/components/icons';
|
||||
import { useNavigation, useGlobalState } from '@/components/hooks';
|
||||
import { Slash } from '@/components/icons';
|
||||
import { useNavigation } from '@/components/hooks';
|
||||
|
||||
export function TopNav() {
|
||||
const [isCollapsed, setCollapsed] = useGlobalState('sidenav-collapsed');
|
||||
const { teamId, websiteId, pathname } = useNavigation();
|
||||
const isSettings = pathname.includes('/settings');
|
||||
|
||||
@@ -22,11 +21,6 @@ export function TopNav() {
|
||||
width="100%"
|
||||
>
|
||||
<Row alignItems="center">
|
||||
<Button onPress={() => setCollapsed(!isCollapsed)} variant="quiet">
|
||||
<Icon>
|
||||
<PanelLeft />
|
||||
</Icon>
|
||||
</Button>
|
||||
<Row alignItems="center" gap="1">
|
||||
<TeamsButton />
|
||||
{websiteId && !isSettings && (
|
||||
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1
-1
@@ -6,7 +6,7 @@ import { ListCheck } from '@/components/icons';
|
||||
import { Panel } from '@/components/common/Panel';
|
||||
import { Breakdown } from './Breakdown';
|
||||
import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
|
||||
import { FieldSelectForm } from '@/app/(main)/websites/[websiteId]/reports/breakdown/FieldSelectForm';
|
||||
import { FieldSelectForm } from '@/app/(main)/websites/[websiteId]/(reports)/breakdown/FieldSelectForm';
|
||||
|
||||
export function BreakdownPage({ websiteId }: { websiteId: string }) {
|
||||
const {
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,4 +1,4 @@
|
||||
import { DataTable, DataColumn, Icon, Row } from '@umami/react-zen';
|
||||
import { DataTable, DataColumn, Icon, Row, Text } from '@umami/react-zen';
|
||||
import { useFormat, useMessages, useNavigation } from '@/components/hooks';
|
||||
import { Empty } from '@/components/common/Empty';
|
||||
import { Avatar } from '@/components/common/Avatar';
|
||||
@@ -21,13 +21,17 @@ export function EventsTable({ data = [] }) {
|
||||
<DataColumn id="event" label={formatMessage(labels.event)} width="2fr">
|
||||
{(row: any) => {
|
||||
return (
|
||||
<Row alignItems="center" gap>
|
||||
<Row alignItems="center" gap="2">
|
||||
<Link href={renderUrl(`/websites/${row.websiteId}/sessions/${row.sessionId}`)}>
|
||||
<Avatar seed={row.sessionId} size={32} />
|
||||
</Link>
|
||||
<Icon>{row.eventName ? <Bolt /> : <Eye />}</Icon>
|
||||
{formatMessage(row.eventName ? labels.triggeredEvent : labels.viewedPage)}
|
||||
<strong>{row.eventName || row.urlPath}</strong>
|
||||
<Text>
|
||||
{formatMessage(row.eventName ? labels.triggeredEvent : labels.viewedPage)}
|
||||
</Text>
|
||||
<Text weight="bold" truncate>
|
||||
{row.eventName || row.urlPath}
|
||||
</Text>
|
||||
</Row>
|
||||
);
|
||||
}}
|
||||
@@ -39,14 +43,14 @@ export function EventsTable({ data = [] }) {
|
||||
</TypeIcon>
|
||||
)}
|
||||
</DataColumn>
|
||||
<DataColumn id="browser" label={formatMessage(labels.browser)}>
|
||||
<DataColumn id="browser" label={formatMessage(labels.browser)} width="120px">
|
||||
{(row: any) => (
|
||||
<TypeIcon type="browser" value={row.browser}>
|
||||
{formatValue(row.browser, 'browser')}
|
||||
</TypeIcon>
|
||||
)}
|
||||
</DataColumn>
|
||||
<DataColumn id="device" label={formatMessage(labels.device)}>
|
||||
<DataColumn id="device" label={formatMessage(labels.device)} width="120px">
|
||||
{(row: any) => (
|
||||
<TypeIcon type="device" value={row.device}>
|
||||
{formatValue(row.device, 'device')}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
'use client';
|
||||
import { ReactNode } from 'react';
|
||||
import { Grid, Column } from '@umami/react-zen';
|
||||
import { ReportsNav } from './ReportsNav';
|
||||
|
||||
export function ReportsLayout({ websiteId, children }: { websiteId: string; children: ReactNode }) {
|
||||
return (
|
||||
<Grid columns="180px 1fr" gap="6">
|
||||
<Column>
|
||||
<ReportsNav websiteId={websiteId} />
|
||||
</Column>
|
||||
<Column minWidth="0">{children}</Column>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
import { Row, NavMenu, NavMenuItem, Icon, Text } from '@umami/react-zen';
|
||||
import { useMessages, useNavigation } from '@/components/hooks';
|
||||
import { Funnel, Sheet, Magnet, Money, Network, Path, Tag, Target } from '@/components/icons';
|
||||
import Link from 'next/link';
|
||||
|
||||
export function ReportsNav({ websiteId }: { websiteId: string }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { pathname, renderUrl } = useNavigation();
|
||||
|
||||
const links = [
|
||||
{
|
||||
id: 'goals',
|
||||
label: formatMessage(labels.goals),
|
||||
icon: <Target />,
|
||||
path: '/goals',
|
||||
},
|
||||
{
|
||||
id: 'funnel',
|
||||
label: formatMessage(labels.funnels),
|
||||
icon: <Funnel />,
|
||||
path: '/funnels',
|
||||
},
|
||||
{
|
||||
id: 'journeys',
|
||||
label: formatMessage(labels.journeys),
|
||||
icon: <Path />,
|
||||
path: '/journeys',
|
||||
},
|
||||
{
|
||||
id: 'retention',
|
||||
label: formatMessage(labels.retention),
|
||||
icon: <Magnet />,
|
||||
path: '/retention',
|
||||
},
|
||||
{
|
||||
id: 'breakdown',
|
||||
label: formatMessage(labels.breakdown),
|
||||
icon: <Sheet />,
|
||||
path: '/breakdown',
|
||||
},
|
||||
{
|
||||
id: 'utm',
|
||||
label: formatMessage(labels.utm),
|
||||
icon: <Tag />,
|
||||
path: '/utm',
|
||||
},
|
||||
{
|
||||
id: 'revenue',
|
||||
label: formatMessage(labels.revenue),
|
||||
icon: <Money />,
|
||||
path: '/revenue',
|
||||
},
|
||||
{
|
||||
id: 'attribution',
|
||||
label: formatMessage(labels.attribution),
|
||||
icon: <Network />,
|
||||
path: '/attribution',
|
||||
},
|
||||
];
|
||||
|
||||
const selected = links.find(({ path }) => path && pathname.endsWith(path))?.id || 'goals';
|
||||
|
||||
return (
|
||||
<NavMenu highlightColor="3">
|
||||
{links.map(({ id, label, icon, path }) => {
|
||||
const isSelected = selected === id;
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={id}
|
||||
href={renderUrl(
|
||||
`/websites/${websiteId}/reports${path}`,
|
||||
path === '/retention' ? { date: undefined } : null,
|
||||
)}
|
||||
>
|
||||
<NavMenuItem isSelected={isSelected}>
|
||||
<Row alignItems="center" gap>
|
||||
<Icon>{icon}</Icon>
|
||||
<Text>{label}</Text>
|
||||
</Row>
|
||||
</NavMenuItem>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</NavMenu>
|
||||
);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Metadata } from 'next';
|
||||
import { ReportsLayout } from './ReportsLayout';
|
||||
|
||||
export default async function ({
|
||||
children,
|
||||
params,
|
||||
}: {
|
||||
children: any;
|
||||
params: Promise<{ websiteId: string }>;
|
||||
}) {
|
||||
const { websiteId } = await params;
|
||||
|
||||
return <ReportsLayout websiteId={websiteId}>{children}</ReportsLayout>;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
template: '%s | Umami',
|
||||
default: 'Websites | Umami',
|
||||
},
|
||||
};
|
||||
Reference in new issue
Block a user