Refactored website components. New layout.

This commit is contained in:
Mike Cao 2025-05-20 01:12:07 -07:00
parent 6e41ba2e2c
commit 06f76dda13
35 changed files with 1159 additions and 987 deletions

View file

@ -78,7 +78,7 @@
"@react-spring/web": "^9.7.3", "@react-spring/web": "^9.7.3",
"@svgr/cli": "^8.1.0", "@svgr/cli": "^8.1.0",
"@tanstack/react-query": "^5.28.6", "@tanstack/react-query": "^5.28.6",
"@umami/react-zen": "^0.108.0", "@umami/react-zen": "^0.111.0",
"@umami/redis-client": "^0.27.0", "@umami/redis-client": "^0.27.0",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"chalk": "^4.1.1", "chalk": "^4.1.1",

1698
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -1,11 +1,10 @@
'use client'; 'use client';
import { Grid, Loading, Column } from '@umami/react-zen'; import { Grid, Loading, Column, Row } from '@umami/react-zen';
import Script from 'next/script'; import Script from 'next/script';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import { UpdateNotice } from './UpdateNotice'; import { UpdateNotice } from './UpdateNotice';
import { SideNav } from '@/app/(main)/SideNav'; import { SideNav } from '@/app/(main)/SideNav';
import { MenuBar } from '@/app/(main)/MenuBar'; import { MenuBar } from '@/app/(main)/MenuBar';
import { Page } from '@/components/common/Page';
import { useLoginQuery, useConfig } from '@/components/hooks'; import { useLoginQuery, useConfig } from '@/components/hooks';
export function App({ children }) { export function App({ children }) {
@ -31,25 +30,26 @@ export function App({ children }) {
} }
return ( return (
<Grid height="100vh" width="100%" columns="auto 1fr" rows="auto 1fr"> <Grid height="100vh" width="100%" columns="auto 1fr" rows="auto 1fr" backgroundColor="2">
<SideNav gridColumn="1 / 2" gridRow="1 / 3" /> <Column gridColumn="1 / 2" gridRow="1 / 3" backgroundColor>
<MenuBar gridColumn="2 / 3" gridRow="1 / 2" /> <SideNav />
</Column>
<Row gridColumn="2 / 3" gridRow="1 / 2">
<MenuBar />
</Row>
<Column <Column
gridColumn="2 / 3" gridColumn="2 / 3"
gridRow="2 / 3" gridRow="2 / 3"
alignItems="center" alignItems="center"
overflow="auto" overflow="auto"
backgroundColor="2"
position="relative" position="relative"
> >
<Page>
{children} {children}
</Column>
<UpdateNotice user={user} config={config} />
{process.env.NODE_ENV === 'production' && !pathname.includes('/share/') && ( {process.env.NODE_ENV === 'production' && !pathname.includes('/share/') && (
<Script src={`${process.env.basePath || ''}/telemetry.js`} /> <Script src={`${process.env.basePath || ''}/telemetry.js`} />
)} )}
</Page>
</Column>
<UpdateNotice user={user} config={config} />
</Grid> </Grid>
); );
} }

View file

@ -2,13 +2,12 @@ import { ThemeButton, Row, Button, Icon } from '@umami/react-zen';
import { LanguageButton } from '@/components/input/LanguageButton'; import { LanguageButton } from '@/components/input/LanguageButton';
import { ProfileButton } from '@/components/input/ProfileButton'; import { ProfileButton } from '@/components/input/ProfileButton';
import { TeamsButton } from '@/components/input/TeamsButton'; import { TeamsButton } from '@/components/input/TeamsButton';
import type { RowProps } from '@umami/react-zen/Row';
import useGlobalState from '@/components/hooks/useGlobalState'; import useGlobalState from '@/components/hooks/useGlobalState';
import { Lucide } from '@/components/icons'; import { Lucide } from '@/components/icons';
import { WebsiteSelect } from '@/components/input/WebsiteSelect'; import { WebsiteSelect } from '@/components/input/WebsiteSelect';
import { useNavigation } from '@/components/hooks'; import { useNavigation } from '@/components/hooks';
export function MenuBar(props: RowProps) { export function MenuBar() {
const [isCollapsed, setCollapsed] = useGlobalState('sidenav-collapsed'); const [isCollapsed, setCollapsed] = useGlobalState('sidenav-collapsed');
const { websiteId } = useNavigation(); const { websiteId } = useNavigation();
@ -16,14 +15,13 @@ export function MenuBar(props: RowProps) {
return ( return (
<Row <Row
{...props}
justifyContent="space-between" justifyContent="space-between"
alignItems="center" alignItems="center"
paddingY="3" paddingY="3"
paddingX="3" paddingX="3"
paddingRight="5" paddingRight="5"
backgroundColor="2"
border="bottom" border="bottom"
width="100%"
> >
<Row alignItems="center"> <Row alignItems="center">
<Button onPress={() => setCollapsed(!isCollapsed)} variant="quiet"> <Button onPress={() => setCollapsed(!isCollapsed)} variant="quiet">

View file

@ -10,25 +10,35 @@ export function SideNav(props: any) {
const [isCollapsed] = useGlobalState('sidenav-collapsed'); const [isCollapsed] = useGlobalState('sidenav-collapsed');
const links = [ const links = [
{
label: formatMessage(labels.boards),
href: renderTeamUrl('/boards'),
icon: <Lucide.LayoutDashboard />,
},
{ {
label: formatMessage(labels.dashboard), label: formatMessage(labels.dashboard),
href: renderTeamUrl('/dashboard'), href: renderTeamUrl('/dashboard'),
icon: <Lucide.Copy />, icon: <Lucide.Copy />,
}, },
{
label: formatMessage(labels.reports),
href: renderTeamUrl('/reports'),
icon: <Lucide.ChartArea />,
},
{ {
label: formatMessage(labels.websites), label: formatMessage(labels.websites),
href: renderTeamUrl('/websites'), href: renderTeamUrl('/websites'),
icon: <Lucide.Globe />, icon: <Lucide.Globe />,
}, },
{ {
label: formatMessage(labels.reports), label: formatMessage(labels.boards),
href: renderTeamUrl('/reports'), href: renderTeamUrl('/boards'),
icon: <Lucide.ChartArea />, icon: <Lucide.LayoutDashboard />,
},
{
label: formatMessage(labels.links),
href: renderTeamUrl('/links'),
icon: <Lucide.Link />,
},
{
label: formatMessage(labels.pixels),
href: renderTeamUrl('/pixels'),
icon: <Lucide.Grid2X2 />,
}, },
{ {
label: formatMessage(labels.settings), label: formatMessage(labels.settings),

View file

@ -3,7 +3,7 @@ import { Button } from '@umami/react-zen';
import Link from 'next/link'; import Link from 'next/link';
import Script from 'next/script'; import Script from 'next/script';
import { WebsiteSelect } from '@/components/input/WebsiteSelect'; import { WebsiteSelect } from '@/components/input/WebsiteSelect';
import { Page } from '@/components/common/Page'; import { PageBody } from '@/components/common/PageBody';
import { SectionHeader } from '@/components/common/SectionHeader'; import { SectionHeader } from '@/components/common/SectionHeader';
import { EventsChart } from '@/components/metrics/EventsChart'; import { EventsChart } from '@/components/metrics/EventsChart';
import { WebsiteChart } from '../websites/[websiteId]/WebsiteChart'; import { WebsiteChart } from '../websites/[websiteId]/WebsiteChart';
@ -117,7 +117,7 @@ export function TestConsole({ websiteId }: { websiteId: string }) {
const website = data?.data.find(({ id }) => websiteId === id); const website = data?.data.find(({ id }) => websiteId === id);
return ( return (
<Page isLoading={isLoading} error={error}> <PageBody isLoading={isLoading} error={error}>
<SectionHeader title="Test console"> <SectionHeader title="Test console">
<WebsiteSelect websiteId={website?.id} onSelect={handleChange} /> <WebsiteSelect websiteId={website?.id} onSelect={handleChange} />
</SectionHeader> </SectionHeader>
@ -214,6 +214,6 @@ export function TestConsole({ websiteId }: { websiteId: string }) {
<EventsChart websiteId={website.id} /> <EventsChart websiteId={website.id} />
</div> </div>
)} )}
</Page> </PageBody>
); );
} }

View file

@ -3,15 +3,16 @@ import { Metadata } from 'next';
import { ReportsHeader } from './ReportsHeader'; import { ReportsHeader } from './ReportsHeader';
import { ReportsDataTable } from './ReportsDataTable'; import { ReportsDataTable } from './ReportsDataTable';
import { useNavigation } from '@/components/hooks'; import { useNavigation } from '@/components/hooks';
import { PageBody } from '@/components/common/PageBody';
export function ReportsPage() { export function ReportsPage() {
const { teamId } = useNavigation(); const { teamId } = useNavigation();
return ( return (
<> <PageBody>
<ReportsHeader /> <ReportsHeader />
<ReportsDataTable teamId={teamId} /> <ReportsDataTable teamId={teamId} />
</> </PageBody>
); );
} }

View file

@ -5,6 +5,7 @@ import { useLoginQuery, useMessages, useNavigation } from '@/components/hooks';
import { SideMenu } from '@/components/common/SideMenu'; import { SideMenu } from '@/components/common/SideMenu';
import { PageHeader } from '@/components/common/PageHeader'; import { PageHeader } from '@/components/common/PageHeader';
import { Panel } from '@/components/common/Panel'; import { Panel } from '@/components/common/Panel';
import { PageBody } from '@/components/common/PageBody';
export function SettingsLayout({ children }: { children: ReactNode }) { export function SettingsLayout({ children }: { children: ReactNode }) {
const { user } = useLoginQuery(); const { user } = useLoginQuery();
@ -33,11 +34,11 @@ export function SettingsLayout({ children }: { children: ReactNode }) {
const value = items.find(({ url }) => pathname.includes(url))?.id; const value = items.find(({ url }) => pathname.includes(url))?.id;
return ( return (
<PageBody>
<Column gap="6"> <Column gap="6">
<PageHeader title={formatMessage(labels.settings)} /> <PageHeader title={formatMessage(labels.settings)} />
<Grid columns="160px 1fr" gap>
<Grid columns="160px 1fr" gap="6"> <Column>
<Column marginTop="6">
<SideMenu items={items} selectedKey={value} /> <SideMenu items={items} selectedKey={value} />
</Column> </Column>
<Column> <Column>
@ -45,5 +46,6 @@ export function SettingsLayout({ children }: { children: ReactNode }) {
</Column> </Column>
</Grid> </Grid>
</Column> </Column>
</PageBody>
); );
} }

View file

@ -31,7 +31,7 @@ export function WebsitesTable({
return ( return (
<DataTable data={data}> <DataTable data={data}>
<DataColumn id="name" label={formatMessage(labels.name)}> <DataColumn id="name" label={formatMessage(labels.name)}>
{(row: any) => <Link href={`/websites/${row.id}`}>{row.name}</Link>} {(row: any) => <Link href={renderTeamUrl(`/websites/${row.id}`)}>{row.name}</Link>}
</DataColumn> </DataColumn>
<DataColumn id="domain" label={formatMessage(labels.domain)} /> <DataColumn id="domain" label={formatMessage(labels.domain)} />
{showActions && ( {showActions && (

View file

@ -5,6 +5,7 @@ import { Grid, Column } from '@umami/react-zen';
import { SideMenu } from '@/components/common/SideMenu'; import { SideMenu } from '@/components/common/SideMenu';
import { Panel } from '@/components/common/Panel'; import { Panel } from '@/components/common/Panel';
import { PageHeader } from '@/components/common/PageHeader'; import { PageHeader } from '@/components/common/PageHeader';
import { PageBody } from '@/components/common/PageBody';
export function TeamSettingsLayout({ children }: { children: ReactNode }) { export function TeamSettingsLayout({ children }: { children: ReactNode }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
@ -31,11 +32,11 @@ export function TeamSettingsLayout({ children }: { children: ReactNode }) {
const value = items.find(({ url }) => pathname.includes(url))?.id; const value = items.find(({ url }) => pathname.includes(url))?.id;
return ( return (
<PageBody>
<Column gap="6"> <Column gap="6">
<PageHeader title={formatMessage(labels.teamSettings)} /> <PageHeader title={formatMessage(labels.teamSettings)} />
<Column gap="6"> <Grid columns="200px 1fr" gap>
<Grid columns="200px 1fr"> <Column>
<Column marginTop="6">
<SideMenu items={items} selectedKey={value} /> <SideMenu items={items} selectedKey={value} />
</Column> </Column>
<Column> <Column>
@ -43,6 +44,6 @@ export function TeamSettingsLayout({ children }: { children: ReactNode }) {
</Column> </Column>
</Grid> </Grid>
</Column> </Column>
</Column> </PageBody>
); );
} }

View file

@ -5,12 +5,14 @@ import { Column } from '@umami/react-zen';
import { PageHeader } from '@/components/common/PageHeader'; import { PageHeader } from '@/components/common/PageHeader';
import { WebsiteAddButton } from '@/app/(main)/settings/websites/WebsiteAddButton'; import { WebsiteAddButton } from '@/app/(main)/settings/websites/WebsiteAddButton';
import { Panel } from '@/components/common/Panel'; import { Panel } from '@/components/common/Panel';
import { PageBody } from '@/components/common/PageBody';
export function WebsitesPage() { export function WebsitesPage() {
const { teamId } = useNavigation(); const { teamId } = useNavigation();
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
return ( return (
<PageBody>
<Column gap="6"> <Column gap="6">
<PageHeader title={formatMessage(labels.websites)}> <PageHeader title={formatMessage(labels.websites)}>
<WebsiteAddButton teamId={teamId} /> <WebsiteAddButton teamId={teamId} />
@ -19,5 +21,6 @@ export function WebsitesPage() {
<WebsitesDataTable teamId={teamId} allowEdit={false} /> <WebsitesDataTable teamId={teamId} allowEdit={false} />
</Panel> </Panel>
</Column> </Column>
</PageBody>
); );
} }

View file

@ -3,7 +3,7 @@ import { useMemo } from 'react';
import { firstBy } from 'thenby'; import { firstBy } from 'thenby';
import { WebsiteChart } from './WebsiteChart'; import { WebsiteChart } from './WebsiteChart';
import { useDashboard } from '@/store/dashboard'; import { useDashboard } from '@/store/dashboard';
import { WebsiteHeader } from './WebsiteHeader'; import { WebsiteControls } from './WebsiteControls';
import { WebsiteMetricsBar } from './WebsiteMetricsBar'; import { WebsiteMetricsBar } from './WebsiteMetricsBar';
import { useMessages, useNavigation } from '@/components/hooks'; import { useMessages, useNavigation } from '@/components/hooks';
import { LinkButton } from '@/components/common/LinkButton'; import { LinkButton } from '@/components/common/LinkButton';
@ -33,7 +33,7 @@ export function WebsiteChartList({
{ordered.map(({ id }, index) => { {ordered.map(({ id }, index) => {
return index < limit ? ( return index < limit ? (
<div key={id}> <div key={id}>
<WebsiteHeader websiteId={id} showLinks={false}> <WebsiteControls websiteId={id} showLinks={false}>
<LinkButton href={renderTeamUrl(`/websites/${id}`)} variant="primary"> <LinkButton href={renderTeamUrl(`/websites/${id}`)} variant="primary">
<Text>{formatMessage(labels.viewDetails)}</Text> <Text>{formatMessage(labels.viewDetails)}</Text>
<Icon> <Icon>
@ -42,7 +42,7 @@ export function WebsiteChartList({
</Icon> </Icon>
</Icon> </Icon>
</LinkButton> </LinkButton>
</WebsiteHeader> </WebsiteControls>
<WebsiteMetricsBar websiteId={id} showChange={true} /> <WebsiteMetricsBar websiteId={id} showChange={true} />
{showCharts && <WebsiteChart websiteId={id} />} {showCharts && <WebsiteChart websiteId={id} />}
</div> </div>

View file

@ -0,0 +1,24 @@
import { Column, Row } from '@umami/react-zen';
import { WebsiteFilterButton } from '@/app/(main)/websites/[websiteId]/WebsiteFilterButton';
import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter';
import { FilterBar } from '@/components/input/FilterBar';
export function WebsiteControls({
websiteId,
showFilter = true,
}: {
websiteId: string;
showFilter?: boolean;
}) {
return (
<Column marginBottom="6" gap="3">
<Row alignItems="center" justifyContent="space-between" gap="3" paddingY="3">
{showFilter && <WebsiteFilterButton websiteId={websiteId} />}
<Row alignItems="center" gap="3">
<WebsiteDateFilter websiteId={websiteId} />
</Row>
</Row>
<FilterBar />
</Column>
);
}

View file

@ -1,4 +1,5 @@
import { Button, Icon, Icons, DialogTrigger, Dialog, Modal, Text } from '@umami/react-zen'; import { Button, Icon, DialogTrigger, Dialog, Modal, Text } from '@umami/react-zen';
import { Lucide } from '@/components/icons';
import { FilterEditForm } from '@/components/common/FilterEditForm'; import { FilterEditForm } from '@/components/common/FilterEditForm';
import { useMessages, useNavigation, useFilters } from '@/components/hooks'; import { useMessages, useNavigation, useFilters } from '@/components/hooks';
@ -33,9 +34,9 @@ export function WebsiteFilterButton({
<DialogTrigger> <DialogTrigger>
<Button variant="quiet"> <Button variant="quiet">
<Icon> <Icon>
<Icons.Plus /> <Lucide.ListFilter />
</Icon> </Icon>
{showText && <Text>{formatMessage(labels.filter)}</Text>} {showText && <Text weight="bold">{formatMessage(labels.filter)}</Text>}
</Button> </Button>
<Modal> <Modal>
<Dialog> <Dialog>

View file

@ -1,39 +1,28 @@
import { Column, Row, Heading } from '@umami/react-zen'; import { Button, Icon, Text, Row } from '@umami/react-zen';
import { Favicon } from '@/components/common/Favicon'; import { PageHeader } from '@/components/common/PageHeader';
import { ActiveUsers } from '@/components/metrics/ActiveUsers';
import { useWebsite } from '@/components/hooks/useWebsite'; import { useWebsite } from '@/components/hooks/useWebsite';
import { WebsiteFilterButton } from '@/app/(main)/websites/[websiteId]/WebsiteFilterButton'; import { Lucide } from '@/components/icons';
import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter'; import { Favicon } from '@/components/common/Favicon';
import { FilterBar } from '@/components/metrics/FilterBar';
import { WebsiteMenu } from '@/app/(main)/websites/[websiteId]/WebsiteMenu';
export function WebsiteHeader({ export function WebsiteHeader() {
websiteId,
showFilter = true,
allowEdit = true,
}: {
websiteId: string;
showFilter?: boolean;
allowEdit?: boolean;
}) {
const website = useWebsite(); const website = useWebsite();
const { name, domain } = website || {};
return ( return (
<Column marginY="6" gap="6"> <PageHeader title={website.name} icon={<Favicon domain={website.domain} />}>
<Row alignItems="center" justifyContent="space-between" gap="3"> <Row alignItems="center" gap>
<Row alignItems="center" gap="3"> <Button>
<Favicon domain={domain} /> <Icon>
<Heading>{name}</Heading> <Lucide.Share />
</Icon>
<Text>Share</Text>
</Button>
<Button>
<Icon>
<Lucide.Edit />
</Icon>
<Text>Edit</Text>
</Button>
</Row> </Row>
<ActiveUsers websiteId={websiteId} /> </PageHeader>
<Row alignItems="center" gap="3">
{showFilter && <WebsiteFilterButton websiteId={websiteId} />}
<WebsiteDateFilter websiteId={websiteId} />
{allowEdit && <WebsiteMenu websiteId={websiteId} />}
</Row>
</Row>
<FilterBar websiteId={websiteId} />
</Column>
); );
} }

View file

@ -1,20 +1,27 @@
'use client'; 'use client';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { Grid, Column, Box } from '@umami/react-zen'; import { Grid, Column } from '@umami/react-zen';
import { WebsiteProvider } from './WebsiteProvider'; import { WebsiteProvider } from './WebsiteProvider';
import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
import { WebsiteNav } from '@/app/(main)/websites/[websiteId]/WebsiteNav';
import { PageBody } from '@/components/common/PageBody';
import { WebsiteHeader } from '@/app/(main)/websites/[websiteId]/WebsiteHeader'; import { WebsiteHeader } from '@/app/(main)/websites/[websiteId]/WebsiteHeader';
import { WebsiteTabs } from '@/app/(main)/websites/[websiteId]/WebsiteTabs';
export function WebsiteLayout({ websiteId, children }: { websiteId: string; children: ReactNode }) { export function WebsiteLayout({ websiteId, children }: { websiteId: string; children: ReactNode }) {
return ( return (
<WebsiteProvider websiteId={websiteId}> <WebsiteProvider websiteId={websiteId}>
<WebsiteHeader websiteId={websiteId} /> <PageBody>
<Grid columns="170px 1140px" justifyContent="center" gap> <WebsiteHeader />
<Box position="sticky" top="20px" alignSelf="flex-start"> <Grid columns="auto 1fr" justifyContent="center" gap width="100%">
<WebsiteTabs websiteId={websiteId} /> <Column position="sticky" top="0px" alignSelf="flex-start" width="200px" paddingTop="3">
</Box> <WebsiteNav websiteId={websiteId} />
<Column>{children}</Column> </Column>
<Column>
<WebsiteControls websiteId={websiteId} />
{children}
</Column>
</Grid> </Grid>
</PageBody>
</WebsiteProvider> </WebsiteProvider>
); );
} }

View file

@ -3,7 +3,7 @@ import { Icons } from '@/components/icons';
import { useMessages, useNavigation } from '@/components/hooks'; import { useMessages, useNavigation } from '@/components/hooks';
import Link from 'next/link'; import Link from 'next/link';
export function WebsiteTabs({ websiteId }: { websiteId: string }) { export function WebsiteNav({ websiteId }: { websiteId: string }) {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { pathname, renderTeamUrl } = useNavigation(); const { pathname, renderTeamUrl } = useNavigation();
@ -60,7 +60,7 @@ export function WebsiteTabs({ websiteId }: { websiteId: string }) {
id: 'retention', id: 'retention',
label: formatMessage(labels.retention), label: formatMessage(labels.retention),
icon: <Icons.Magnet />, icon: <Icons.Magnet />,
path: '/funnels', path: '/retention',
}, },
{ {
id: 'utm', id: 'utm',
@ -97,7 +97,7 @@ export function WebsiteTabs({ websiteId }: { websiteId: string }) {
return ( return (
<Link key={id} href={renderTeamUrl(`/websites/${websiteId}${path}`)}> <Link key={id} href={renderTeamUrl(`/websites/${websiteId}${path}`)}>
<NavMenuItem highlightColor="5" isSelected={isSelected}> <NavMenuItem isSelected={isSelected}>
<Row alignItems="center" gap> <Row alignItems="center" gap>
<Icon style={{ fill: 'currentcolor' }}>{icon}</Icon> <Icon style={{ fill: 'currentcolor' }}>{icon}</Icon>
<Text>{label}</Text> <Text>{label}</Text>

View file

@ -8,5 +8,5 @@ export default async function ({ params }: { params: Promise<{ websiteId: string
} }
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Event Data', title: 'Events',
}; };

View file

@ -0,0 +1,6 @@
'use client';
import { Column } from '@umami/react-zen';
export function GoalsPage({ websiteId }: { websiteId: string }) {
return <Column>Goals {websiteId}</Column>;
}

View file

@ -0,0 +1,12 @@
import { Metadata } from 'next';
import { GoalsPage } from './GoalsPage';
export default async function ({ params }: { params: Promise<{ websiteId: string }> }) {
const { websiteId } = await params;
return <GoalsPage websiteId={websiteId} />;
}
export const metadata: Metadata = {
title: 'Goals',
};

View file

@ -1,6 +1,6 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { Page } from '@/components/common/Page'; import { PageBody } from '@/components/common/PageBody';
import { SectionHeader } from '@/components/common/SectionHeader'; import { SectionHeader } from '@/components/common/SectionHeader';
import { useApi, useMessages } from '@/components/hooks'; import { useApi, useMessages } from '@/components/hooks';
import { EmptyPlaceholder } from '@/components/common/EmptyPlaceholder'; import { EmptyPlaceholder } from '@/components/common/EmptyPlaceholder';
@ -21,11 +21,11 @@ export function RealtimeHome() {
}, [data, router]); }, [data, router]);
return ( return (
<Page isLoading={isLoading || data?.length > 0} error={error}> <PageBody isLoading={isLoading || data?.length > 0} error={error}>
<SectionHeader title={formatMessage(labels.realtime)} /> <SectionHeader title={formatMessage(labels.realtime)} />
{data?.length === 0 && ( {data?.length === 0 && (
<EmptyPlaceholder message={formatMessage(messages.noWebsitesConfigured)} /> <EmptyPlaceholder message={formatMessage(messages.noWebsitesConfigured)} />
)} )}
</Page> </PageBody>
); );
} }

View file

@ -2,7 +2,7 @@
import { firstBy } from 'thenby'; import { firstBy } from 'thenby';
import { Grid } from '@umami/react-zen'; import { Grid } from '@umami/react-zen';
import { GridRow } from '@/components/common/GridRow'; import { GridRow } from '@/components/common/GridRow';
import { Page } from '@/components/common/Page'; import { PageBody } from '@/components/common/PageBody';
import { Panel } from '@/components/common/Panel'; import { Panel } from '@/components/common/Panel';
import { RealtimeChart } from '@/components/metrics/RealtimeChart'; import { RealtimeChart } from '@/components/metrics/RealtimeChart';
import { WorldMap } from '@/components/metrics/WorldMap'; import { WorldMap } from '@/components/metrics/WorldMap';
@ -17,7 +17,7 @@ export function WebsiteRealtimePage({ websiteId }: { websiteId: string }) {
const { data, isLoading, error } = useRealtimeQuery(websiteId); const { data, isLoading, error } = useRealtimeQuery(websiteId);
if (isLoading || error) { if (isLoading || error) {
return <Page isLoading={isLoading} error={error} />; return <PageBody isLoading={isLoading} error={error} />;
} }
const countries = percentFilter( const countries = percentFilter(

View file

@ -0,0 +1,6 @@
'use client';
import { RetentionTable } from './RetentionTable';
export function RetentionPage({ websiteId }: { websiteId: string }) {
return <RetentionTable websiteId={websiteId} />;
}

View file

@ -0,0 +1,66 @@
import { EmptyPlaceholder } from '@/components/common/EmptyPlaceholder';
import { useMessages, useLocale, useReport } from '@/components/hooks';
import { formatDate } from '@/lib/date';
const DAYS = [1, 2, 3, 4, 5, 6, 7, 14, 21, 28];
export function RetentionTable({ days = DAYS }) {
const { formatMessage, labels } = useMessages();
const { locale } = useLocale();
const { report } = useReport();
const { data } = report || {};
if (!data) {
return <EmptyPlaceholder />;
}
const rows = data.reduce((arr: any[], row: { date: any; visitors: any; day: any }) => {
const { date, visitors, day } = row;
if (day === 0) {
return arr.concat({
date,
visitors,
records: days
.reduce((arr, day) => {
arr[day] = data.find(x => x.date === date && x.day === day);
return arr;
}, [])
.filter(n => n),
});
}
return arr;
}, []);
const totalDays = rows.length;
return (
<>
<div>
<div>
<div>{formatMessage(labels.date)}</div>
<div>{formatMessage(labels.visitors)}</div>
{days.map(n => (
<div key={n}>
{formatMessage(labels.day)} {n}
</div>
))}
</div>
{rows.map(({ date, visitors, records }, rowIndex) => {
return (
<div key={rowIndex}>
<div>{formatDate(date, 'PP', locale)}</div>
<div>{visitors}</div>
{days.map(day => {
if (totalDays - rowIndex < day) {
return null;
}
const percentage = records.filter(a => a.day === day)[0]?.percentage;
return <div key={day}>{percentage ? `${Number(percentage).toFixed(2)}%` : ''}</div>;
})}
</div>
);
})}
</div>
</>
);
}

View file

@ -0,0 +1,12 @@
import { Metadata } from 'next';
import { RetentionPage } from './RetentionPage';
export default async function ({ params }: { params: Promise<{ websiteId: string }> }) {
const { websiteId } = await params;
return <RetentionPage websiteId={websiteId} />;
}
export const metadata: Metadata = {
title: 'Retention',
};

View file

@ -15,7 +15,7 @@ export function SessionsTable({ data = [] }: { data: any[]; showDomain?: boolean
<DataColumn id="id" label={formatMessage(labels.session)} width="100px"> <DataColumn id="id" label={formatMessage(labels.session)} width="100px">
{(row: any) => ( {(row: any) => (
<Link href={`sessions/${row.id}`} className={styles.link}> <Link href={`sessions/${row.id}`} className={styles.link}>
<Avatar key={row.id} seed={row.id} size={64} /> <Avatar seed={row.id} size={64} />
</Link> </Link>
)} )}
</DataColumn> </DataColumn>

View file

@ -0,0 +1,6 @@
'use client';
import { Column } from '@umami/react-zen';
export function UTMPage({ websiteId }: { websiteId: string }) {
return <Column>Goals {websiteId}</Column>;
}

View file

@ -0,0 +1,12 @@
import { Metadata } from 'next';
import { UTMPage } from './UTMPage';
export default async function ({ params }: { params: Promise<{ websiteId: string }> }) {
const { websiteId } = await params;
return <UTMPage websiteId={websiteId} />;
}
export const metadata: Metadata = {
title: 'UTM Parameters',
};

View file

@ -2,7 +2,7 @@
import { WebsiteProvider } from '@/app/(main)/websites/[websiteId]/WebsiteProvider'; import { WebsiteProvider } from '@/app/(main)/websites/[websiteId]/WebsiteProvider';
import { WebsiteDetailsPage } from '@/app/(main)/websites/[websiteId]/WebsiteDetailsPage'; import { WebsiteDetailsPage } from '@/app/(main)/websites/[websiteId]/WebsiteDetailsPage';
import { useShareTokenQuery } from '@/components/hooks'; import { useShareTokenQuery } from '@/components/hooks';
import { Page } from '@/components/common/Page'; import { PageBody } from '@/components/common/PageBody';
import { Header } from './Header'; import { Header } from './Header';
import { Footer } from './Footer'; import { Footer } from './Footer';
@ -14,12 +14,12 @@ export function SharePage({ shareId }) {
} }
return ( return (
<Page> <PageBody>
<Header /> <Header />
<WebsiteProvider websiteId={shareToken.websiteId}> <WebsiteProvider websiteId={shareToken.websiteId}>
<WebsiteDetailsPage websiteId={shareToken.websiteId} /> <WebsiteDetailsPage websiteId={shareToken.websiteId} />
</WebsiteProvider> </WebsiteProvider>
<Footer /> <Footer />
</Page> </PageBody>
); );
} }

View file

@ -5,13 +5,13 @@ const LAYOUTS = {
two: { two: {
columns: { columns: {
xs: '1fr', xs: '1fr',
md: 'repeat(auto-fill, minmax(600px, 1fr))', md: 'repeat(auto-fill, minmax(560px, 1fr))',
}, },
}, },
three: { three: {
columns: { columns: {
xs: '1fr', xs: '1fr',
md: 'repeat(auto-fill, minmax(400px, 1fr))', md: 'repeat(auto-fill, minmax(360px, 1fr))',
}, },
}, },
'one-two': { columns: { xs: '1fr', lg: 'repeat(3, 1fr)' } }, 'one-two': { columns: { xs: '1fr', lg: 'repeat(3, 1fr)' } },

View file

@ -3,13 +3,14 @@ import { ReactNode } from 'react';
import { AlertBanner, Loading, Column } from '@umami/react-zen'; import { AlertBanner, Loading, Column } from '@umami/react-zen';
import { useMessages } from '@/components/hooks'; import { useMessages } from '@/components/hooks';
export function Page({ export function PageBody({
maxWidth = '1600px',
error, error,
isLoading, isLoading,
children, children,
...props ...props
}: { }: {
className?: string; maxWidth?: string;
error?: unknown; error?: unknown;
isLoading?: boolean; isLoading?: boolean;
children?: ReactNode; children?: ReactNode;
@ -25,7 +26,7 @@ export function Page({
} }
return ( return (
<Column {...props} width="100%" maxWidth="1320px" margin="auto" paddingBottom="9"> <Column {...props} width="100%" paddingBottom="9" style={{ margin: '0 auto', maxWidth }}>
{children} {children}
</Column> </Column>
); );

View file

@ -15,8 +15,14 @@ export function PageHeader({
children?: ReactNode; children?: ReactNode;
}) { }) {
return ( return (
<Row justifyContent="space-between" alignItems="center" paddingY="6" border="bottom"> <Row
<Row gap="3"> justifyContent="space-between"
alignItems="center"
paddingY="6"
border="bottom"
width="100%"
>
<Row alignItems="center" gap="3">
{icon && <Icon>{icon}</Icon>} {icon && <Icon>{icon}</Icon>}
{title && <Heading size="4">{title}</Heading>} {title && <Heading size="4">{title}</Heading>}
{description && <Text color="muted">{description}</Text>} {description && <Text color="muted">{description}</Text>}

View file

@ -1,5 +1,6 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { Text, List, ListItem, Icon, Row } from '@umami/react-zen'; import { Text, NavMenu, NavMenuItem, Icon, Row } from '@umami/react-zen';
import Link from 'next/link';
export interface SideMenuProps { export interface SideMenuProps {
items: { id: string; label: string; url: string; icon?: ReactNode }[]; items: { id: string; label: string; url: string; icon?: ReactNode }[];
@ -8,17 +9,19 @@ export interface SideMenuProps {
export function SideMenu({ items, selectedKey }: SideMenuProps) { export function SideMenu({ items, selectedKey }: SideMenuProps) {
return ( return (
<List> <NavMenu highlightColor="3">
{items.map(({ id, label, url, icon }) => { {items.map(({ id, label, url, icon }) => {
return ( return (
<ListItem key={id} id={id} href={url}> <Link key={id} href={url}>
<NavMenuItem isSelected={id === selectedKey}>
<Row alignItems="center" gap> <Row alignItems="center" gap>
{icon && <Icon>{icon}</Icon>} {icon && <Icon>{icon}</Icon>}
<Text weight={id === selectedKey ? 'bold' : 'regular'}>{label}</Text> <Text>{label}</Text>
</Row> </Row>
</ListItem> </NavMenuItem>
</Link>
); );
})} })}
</List> </NavMenu>
); );
} }

View file

@ -2,9 +2,8 @@ import { MouseEvent } from 'react';
import { Button, Icon, Icons, Text, Row, TooltipTrigger, Tooltip } from '@umami/react-zen'; import { Button, Icon, Icons, Text, Row, TooltipTrigger, Tooltip } from '@umami/react-zen';
import { useNavigation, useMessages, useFormat, useFilters } from '@/components/hooks'; import { useNavigation, useMessages, useFormat, useFilters } from '@/components/hooks';
import { isSearchOperator } from '@/lib/params'; import { isSearchOperator } from '@/lib/params';
import { WebsiteFilterButton } from '@/app/(main)/websites/[websiteId]/WebsiteFilterButton';
export function FilterBar({ websiteId }: { websiteId: string }) { export function FilterBar() {
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { formatValue } = useFormat(); const { formatValue } = useFormat();
const { router, renderUrl } = useNavigation(); const { router, renderUrl } = useNavigation();
@ -25,8 +24,9 @@ export function FilterBar({ websiteId }: { websiteId: string }) {
return ( return (
<Row <Row
theme="dark"
backgroundColor="1"
gap gap
backgroundColor="3"
alignItems="center" alignItems="center"
justifyContent="space-between" justifyContent="space-between"
paddingY="2" paddingY="2"
@ -57,18 +57,21 @@ export function FilterBar({ websiteId }: { websiteId: string }) {
> >
<Row alignItems="center" gap="4"> <Row alignItems="center" gap="4">
<Row alignItems="center" gap="2"> <Row alignItems="center" gap="2">
<Text weight="bold">{label}</Text> <Text color="12" weight="bold">
{label}
</Text>
<Text color="11">{operatorLabels[operator]}</Text> <Text color="11">{operatorLabels[operator]}</Text>
<Text weight="bold">{paramValue}</Text> <Text color="12" weight="bold">
{paramValue}
</Text>
</Row> </Row>
<Icon onClick={e => handleCloseFilter(name, e)} size="xs"> <Icon onClick={e => handleCloseFilter(name, e)} size="xs" color>
<Icons.Close /> <Icons.Close />
</Icon> </Icon>
</Row> </Row>
</Row> </Row>
); );
})} })}
<WebsiteFilterButton websiteId={websiteId} alignment="center" showText={false} />
</Row> </Row>
<TooltipTrigger delay={0}> <TooltipTrigger delay={0}>
<Button variant="quiet" onPress={handleResetFilter}> <Button variant="quiet" onPress={handleResetFilter}>

View file

@ -157,6 +157,7 @@ export const labels = defineMessages({
eventData: { id: 'label.event-data', defaultMessage: 'Event data' }, eventData: { id: 'label.event-data', defaultMessage: 'Event data' },
sessionData: { id: 'label.session-data', defaultMessage: 'Session data' }, sessionData: { id: 'label.session-data', defaultMessage: 'Session data' },
funnel: { id: 'label.funnel', defaultMessage: 'Funnel' }, funnel: { id: 'label.funnel', defaultMessage: 'Funnel' },
funnels: { id: 'label.funnels', defaultMessage: 'Funnels' },
funnelDescription: { funnelDescription: {
id: 'label.funnel-description', id: 'label.funnel-description',
defaultMessage: 'Understand the conversion and drop-off rate of users.', defaultMessage: 'Understand the conversion and drop-off rate of users.',
@ -315,6 +316,8 @@ export const labels = defineMessages({
other: { id: 'label.other', defaultMessage: 'Other' }, other: { id: 'label.other', defaultMessage: 'Other' },
boards: { id: 'label.boards', defaultMessage: 'Boards' }, boards: { id: 'label.boards', defaultMessage: 'Boards' },
apply: { id: 'label.apply', defaultMessage: 'Apply' }, apply: { id: 'label.apply', defaultMessage: 'Apply' },
links: { id: 'label.links', defaultMessage: 'Links' },
pixels: { id: 'label.pixels', defaultMessage: 'Pixels' },
}); });
export const messages = defineMessages({ export const messages = defineMessages({