Redesigned overview page.

This commit is contained in:
Mike Cao 2025-08-21 03:01:37 -07:00
parent 5d1f2a6f2d
commit f7ca583410
12 changed files with 257 additions and 96 deletions

View file

@ -42,7 +42,8 @@
"@typescript-eslint/no-var-requires": "off", "@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/no-empty-interface": "off", "@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/no-unused-vars": ["error", { "ignoreRestSiblings": true }], "@typescript-eslint/no-unused-vars": ["error", { "ignoreRestSiblings": true }],
"@typescript-eslint/no-namespace": ["error", { "allowDeclarations": true }] "@typescript-eslint/no-namespace": ["error", { "allowDeclarations": true }],
"@typescript-eslint/triple-slash-reference": "off"
}, },
"globals": { "globals": {
"React": "writable" "React": "writable"

1
next-env.d.ts vendored
View file

@ -1,5 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View file

@ -11,7 +11,7 @@ export function LinkHeader() {
return ( return (
<PageHeader title={link.name} description={link.url}> <PageHeader title={link.name} description={link.url}>
<LinkButton href={getSlugUrl(link.slug)}> <LinkButton href={getSlugUrl(link.slug)} target="_blank">
<Icon> <Icon>
<ExternalLink /> <ExternalLink />
</Icon> </Icon>

View file

@ -6,7 +6,7 @@ import { Panel } from '@/components/common/Panel';
import { WebsiteChart } from '@/app/(main)/websites/[websiteId]/WebsiteChart'; import { WebsiteChart } from '@/app/(main)/websites/[websiteId]/WebsiteChart';
import { LinkMetricsBar } from '@/app/(main)/links/[linkId]/LinkMetricsBar'; import { LinkMetricsBar } from '@/app/(main)/links/[linkId]/LinkMetricsBar';
import { LinkControls } from '@/app/(main)/links/[linkId]/LinkControls'; import { LinkControls } from '@/app/(main)/links/[linkId]/LinkControls';
import { Grid } from '@umami/react-zen'; import { Grid, Tab, TabList, TabPanel, Tabs } from '@umami/react-zen';
import { GridRow } from '@/components/common/GridRow'; import { GridRow } from '@/components/common/GridRow';
import { ReferrersTable } from '@/components/metrics/ReferrersTable'; import { ReferrersTable } from '@/components/metrics/ReferrersTable';
import { BrowsersTable } from '@/components/metrics/BrowsersTable'; import { BrowsersTable } from '@/components/metrics/BrowsersTable';
@ -14,9 +14,16 @@ import { OSTable } from '@/components/metrics/OSTable';
import { DevicesTable } from '@/components/metrics/DevicesTable'; import { DevicesTable } from '@/components/metrics/DevicesTable';
import { WorldMap } from '@/components/metrics/WorldMap'; import { WorldMap } from '@/components/metrics/WorldMap';
import { CountriesTable } from '@/components/metrics/CountriesTable'; import { CountriesTable } from '@/components/metrics/CountriesTable';
import { ChannelsTable } from '@/components/metrics/ChannelsTable';
import { RegionsTable } from '@/components/metrics/RegionsTable';
import { CitiesTable } from '@/components/metrics/CitiesTable';
import { SessionsWeekly } from '@/app/(main)/websites/[websiteId]/sessions/SessionsWeekly';
import { useMessages } from '@/components/hooks';
export function LinkPage({ linkId }: { linkId: string }) { export function LinkPage({ linkId }: { linkId: string }) {
const props = { websiteId: linkId, limit: 10, allowDownload: false }; const { formatMessage, labels } = useMessages();
const tableProps = { websiteId: linkId, limit: 10, allowDownload: false };
const rowProps = { minHeight: 570 };
return ( return (
<LinkProvider linkId={linkId}> <LinkProvider linkId={linkId}>
@ -27,29 +34,67 @@ export function LinkPage({ linkId }: { linkId: string }) {
<Panel> <Panel>
<WebsiteChart websiteId={linkId} /> <WebsiteChart websiteId={linkId} />
</Panel> </Panel>
<GridRow layout="two"> <Grid gap>
<GridRow layout="one" {...rowProps}>
<Panel> <Panel>
<ReferrersTable {...props} /> <Tabs>
<TabList>
<Tab id="referrer">{formatMessage(labels.referrers)}</Tab>
<Tab id="channel">{formatMessage(labels.channels)}</Tab>
</TabList>
<TabPanel id="referrer">
<ReferrersTable {...tableProps} />
</TabPanel>
<TabPanel id="channel">
<ChannelsTable {...tableProps} />
</TabPanel>
</Tabs>
</Panel> </Panel>
</GridRow> </GridRow>
<Grid gap="3"> <GridRow layout="two-one" {...rowProps}>
<GridRow layout="three">
<Panel>
<BrowsersTable {...props} />
</Panel>
<Panel>
<OSTable {...props} />
</Panel>
<Panel>
<DevicesTable {...props} />
</Panel>
</GridRow>
<GridRow layout="two-one">
<Panel gridColumn="span 2" noPadding> <Panel gridColumn="span 2" noPadding>
<WorldMap websiteId={linkId} /> <WorldMap websiteId={linkId} />
</Panel> </Panel>
<Panel> <Panel>
<CountriesTable {...props} /> <Tabs>
<TabList>
<Tab id="country">{formatMessage(labels.countries)}</Tab>
<Tab id="region">{formatMessage(labels.regions)}</Tab>
<Tab id="city">{formatMessage(labels.cities)}</Tab>
</TabList>
<TabPanel id="country">
<CountriesTable {...tableProps} />
</TabPanel>
<TabPanel id="region">
<RegionsTable {...tableProps} />
</TabPanel>
<TabPanel id="city">
<CitiesTable {...tableProps} />
</TabPanel>
</Tabs>
</Panel>
</GridRow>
<GridRow layout="two" {...rowProps}>
<Panel>
<Tabs>
<TabList>
<Tab id="browser">{formatMessage(labels.browsers)}</Tab>
<Tab id="os">{formatMessage(labels.os)}</Tab>
<Tab id="device">{formatMessage(labels.devices)}</Tab>
</TabList>
<TabPanel id="browser">
<BrowsersTable {...tableProps} />
</TabPanel>
<TabPanel id="os">
<OSTable {...tableProps} />
</TabPanel>
<TabPanel id="device">
<DevicesTable {...tableProps} />
</TabPanel>
</Tabs>
</Panel>
<Panel>
<SessionsWeekly websiteId={linkId} />
</Panel> </Panel>
</GridRow> </GridRow>
</Grid> </Grid>

View file

@ -11,7 +11,7 @@ export function PixelHeader() {
return ( return (
<PageHeader title={pixel.name} description={pixel.url}> <PageHeader title={pixel.name} description={pixel.url}>
<LinkButton href={getSlugUrl(pixel.slug)}> <LinkButton href={getSlugUrl(pixel.slug)} target="_blank">
<Icon> <Icon>
<ExternalLink /> <ExternalLink />
</Icon> </Icon>

View file

@ -6,7 +6,7 @@ import { Panel } from '@/components/common/Panel';
import { WebsiteChart } from '@/app/(main)/websites/[websiteId]/WebsiteChart'; import { WebsiteChart } from '@/app/(main)/websites/[websiteId]/WebsiteChart';
import { PixelMetricsBar } from '@/app/(main)/pixels/[pixelId]/PixelMetricsBar'; import { PixelMetricsBar } from '@/app/(main)/pixels/[pixelId]/PixelMetricsBar';
import { PixelControls } from '@/app/(main)/pixels/[pixelId]/PixelControls'; import { PixelControls } from '@/app/(main)/pixels/[pixelId]/PixelControls';
import { Grid } from '@umami/react-zen'; import { Grid, Tab, TabList, TabPanel, Tabs } from '@umami/react-zen';
import { GridRow } from '@/components/common/GridRow'; import { GridRow } from '@/components/common/GridRow';
import { ReferrersTable } from '@/components/metrics/ReferrersTable'; import { ReferrersTable } from '@/components/metrics/ReferrersTable';
import { BrowsersTable } from '@/components/metrics/BrowsersTable'; import { BrowsersTable } from '@/components/metrics/BrowsersTable';
@ -14,9 +14,16 @@ import { OSTable } from '@/components/metrics/OSTable';
import { DevicesTable } from '@/components/metrics/DevicesTable'; import { DevicesTable } from '@/components/metrics/DevicesTable';
import { WorldMap } from '@/components/metrics/WorldMap'; import { WorldMap } from '@/components/metrics/WorldMap';
import { CountriesTable } from '@/components/metrics/CountriesTable'; import { CountriesTable } from '@/components/metrics/CountriesTable';
import { useMessages } from '@/components/hooks';
import { ChannelsTable } from '@/components/metrics/ChannelsTable';
import { RegionsTable } from '@/components/metrics/RegionsTable';
import { CitiesTable } from '@/components/metrics/CitiesTable';
import { SessionsWeekly } from '@/app/(main)/websites/[websiteId]/sessions/SessionsWeekly';
export function PixelPage({ pixelId }: { pixelId: string }) { export function PixelPage({ pixelId }: { pixelId: string }) {
const props = { websiteId: pixelId, limit: 10, allowDownload: false }; const { formatMessage, labels } = useMessages();
const tableProps = { websiteId: pixelId, limit: 10, allowDownload: false };
const rowProps = { minHeight: 570 };
return ( return (
<PixelProvider pixelId={pixelId}> <PixelProvider pixelId={pixelId}>
@ -27,29 +34,67 @@ export function PixelPage({ pixelId }: { pixelId: string }) {
<Panel> <Panel>
<WebsiteChart websiteId={pixelId} /> <WebsiteChart websiteId={pixelId} />
</Panel> </Panel>
<GridRow layout="two"> <Grid gap>
<GridRow layout="one" {...rowProps}>
<Panel> <Panel>
<ReferrersTable {...props} /> <Tabs>
<TabList>
<Tab id="referrer">{formatMessage(labels.referrers)}</Tab>
<Tab id="channel">{formatMessage(labels.channels)}</Tab>
</TabList>
<TabPanel id="referrer">
<ReferrersTable {...tableProps} />
</TabPanel>
<TabPanel id="channel">
<ChannelsTable {...tableProps} />
</TabPanel>
</Tabs>
</Panel> </Panel>
</GridRow> </GridRow>
<Grid gap="3"> <GridRow layout="two-one" {...rowProps}>
<GridRow layout="three">
<Panel>
<BrowsersTable {...props} />
</Panel>
<Panel>
<OSTable {...props} />
</Panel>
<Panel>
<DevicesTable {...props} />
</Panel>
</GridRow>
<GridRow layout="two-one">
<Panel gridColumn="span 2" noPadding> <Panel gridColumn="span 2" noPadding>
<WorldMap websiteId={pixelId} /> <WorldMap websiteId={pixelId} />
</Panel> </Panel>
<Panel> <Panel>
<CountriesTable {...props} /> <Tabs>
<TabList>
<Tab id="country">{formatMessage(labels.countries)}</Tab>
<Tab id="region">{formatMessage(labels.regions)}</Tab>
<Tab id="city">{formatMessage(labels.cities)}</Tab>
</TabList>
<TabPanel id="country">
<CountriesTable {...tableProps} />
</TabPanel>
<TabPanel id="region">
<RegionsTable {...tableProps} />
</TabPanel>
<TabPanel id="city">
<CitiesTable {...tableProps} />
</TabPanel>
</Tabs>
</Panel>
</GridRow>
<GridRow layout="two" {...rowProps}>
<Panel>
<Tabs>
<TabList>
<Tab id="browser">{formatMessage(labels.browsers)}</Tab>
<Tab id="os">{formatMessage(labels.os)}</Tab>
<Tab id="device">{formatMessage(labels.devices)}</Tab>
</TabList>
<TabPanel id="browser">
<BrowsersTable {...tableProps} />
</TabPanel>
<TabPanel id="os">
<OSTable {...tableProps} />
</TabPanel>
<TabPanel id="device">
<DevicesTable {...tableProps} />
</TabPanel>
</Tabs>
</Panel>
<Panel>
<SessionsWeekly websiteId={pixelId} />
</Panel> </Panel>
</GridRow> </GridRow>
</Grid> </Grid>

View file

@ -1,10 +1,10 @@
'use client'; 'use client';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { Column, Grid } from '@umami/react-zen'; import { Column, Grid } from '@umami/react-zen';
import { WebsiteProvider } from './WebsiteProvider'; import { WebsiteProvider } from '@/app/(main)/websites/WebsiteProvider';
import { PageBody } from '@/components/common/PageBody'; import { PageBody } from '@/components/common/PageBody';
import { WebsiteHeader } from './WebsiteHeader'; import { WebsiteHeader } from './WebsiteHeader';
import { WebsiteNav } from '@/app/(main)/websites/[websiteId]/WebsiteNav'; import { WebsiteNav } from './WebsiteNav';
export function WebsiteLayout({ websiteId, children }: { websiteId: string; children: ReactNode }) { export function WebsiteLayout({ websiteId, children }: { websiteId: string; children: ReactNode }) {
return ( return (

View file

@ -1,4 +1,4 @@
import { Grid } from '@umami/react-zen'; import { Grid, Tabs, Tab, TabList, TabPanel } from '@umami/react-zen';
import { GridRow } from '@/components/common/GridRow'; import { GridRow } from '@/components/common/GridRow';
import { Panel } from '@/components/common/Panel'; import { Panel } from '@/components/common/Panel';
import { PagesTable } from '@/components/metrics/PagesTable'; import { PagesTable } from '@/components/metrics/PagesTable';
@ -8,37 +8,97 @@ import { OSTable } from '@/components/metrics/OSTable';
import { DevicesTable } from '@/components/metrics/DevicesTable'; import { DevicesTable } from '@/components/metrics/DevicesTable';
import { WorldMap } from '@/components/metrics/WorldMap'; import { WorldMap } from '@/components/metrics/WorldMap';
import { CountriesTable } from '@/components/metrics/CountriesTable'; import { CountriesTable } from '@/components/metrics/CountriesTable';
import { useMessages } from '@/components/hooks';
import { ChannelsTable } from '@/components/metrics/ChannelsTable';
import { RegionsTable } from '@/components/metrics/RegionsTable';
import { CitiesTable } from '@/components/metrics/CitiesTable';
import { SessionsWeekly } from '@/app/(main)/websites/[websiteId]/sessions/SessionsWeekly';
export function WebsiteTableView({ websiteId }: { websiteId: string }) { export function WebsiteTableView({ websiteId }: { websiteId: string }) {
const props = { websiteId, limit: 10, allowDownload: false }; const { formatMessage, labels } = useMessages();
const tableProps = { websiteId, limit: 10, allowDownload: false };
const rowProps = { minHeight: 570 };
return ( return (
<Grid gap="3"> <Grid gap="3">
<GridRow layout="two"> <GridRow layout="two" {...rowProps}>
<Panel> <Panel>
<PagesTable {...props} /> <Tabs>
<TabList>
<Tab id="page">{formatMessage(labels.pages)}</Tab>
<Tab id="entry">{formatMessage(labels.entry)}</Tab>
<Tab id="exit">{formatMessage(labels.exit)}</Tab>
</TabList>
<TabPanel id="page">
<PagesTable type="path" {...tableProps} />
</TabPanel>
<TabPanel id="entry">
<PagesTable type="entry" {...tableProps} />
</TabPanel>
<TabPanel id="exit">
<PagesTable type="exit" {...tableProps} />
</TabPanel>
</Tabs>
</Panel> </Panel>
<Panel> <Panel>
<ReferrersTable {...props} /> <Tabs>
<TabList>
<Tab id="referrer">{formatMessage(labels.referrers)}</Tab>
<Tab id="channel">{formatMessage(labels.channels)}</Tab>
</TabList>
<TabPanel id="referrer">
<ReferrersTable {...tableProps} />
</TabPanel>
<TabPanel id="channel">
<ChannelsTable {...tableProps} />
</TabPanel>
</Tabs>
</Panel> </Panel>
</GridRow> </GridRow>
<GridRow layout="three"> <GridRow layout="two-one" {...rowProps}>
<Panel>
<BrowsersTable {...props} />
</Panel>
<Panel>
<OSTable {...props} />
</Panel>
<Panel>
<DevicesTable {...props} />
</Panel>
</GridRow>
<GridRow layout="two-one">
<Panel gridColumn="span 2" noPadding> <Panel gridColumn="span 2" noPadding>
<WorldMap websiteId={websiteId} /> <WorldMap websiteId={websiteId} />
</Panel> </Panel>
<Panel> <Panel>
<CountriesTable {...props} /> <Tabs>
<TabList>
<Tab id="country">{formatMessage(labels.countries)}</Tab>
<Tab id="region">{formatMessage(labels.regions)}</Tab>
<Tab id="city">{formatMessage(labels.cities)}</Tab>
</TabList>
<TabPanel id="country">
<CountriesTable {...tableProps} />
</TabPanel>
<TabPanel id="region">
<RegionsTable {...tableProps} />
</TabPanel>
<TabPanel id="city">
<CitiesTable {...tableProps} />
</TabPanel>
</Tabs>
</Panel>
</GridRow>
<GridRow layout="two" {...rowProps}>
<Panel>
<Tabs>
<TabList>
<Tab id="browser">{formatMessage(labels.browsers)}</Tab>
<Tab id="os">{formatMessage(labels.os)}</Tab>
<Tab id="device">{formatMessage(labels.devices)}</Tab>
</TabList>
<TabPanel id="browser">
<BrowsersTable {...tableProps} />
</TabPanel>
<TabPanel id="os">
<OSTable {...tableProps} />
</TabPanel>
<TabPanel id="device">
<DevicesTable {...tableProps} />
</TabPanel>
</Tabs>
</Panel>
<Panel>
<SessionsWeekly websiteId={websiteId} />
</Panel> </Panel>
</GridRow> </GridRow>
</Grid> </Grid>

View file

@ -52,9 +52,7 @@ export function SessionsWeekly({ websiteId }: { websiteId: string }) {
.toLowerCase(); .toLowerCase();
return ( return (
<Row key={i} justifyContent="flex-end"> <Row key={i} justifyContent="flex-end">
<Text color="muted" weight="bold"> <Text color="muted">{label}</Text>
{label}
</Text>
</Row> </Row>
); );
})} })}

View file

@ -1,5 +1,5 @@
import { ReactNode, useMemo, useState } from 'react'; import { ReactNode, useMemo, useState } from 'react';
import { Button, Column, Icon, Row, SearchField, Text, Grid } from '@umami/react-zen'; import { Button, Column, Icon, Row, SearchField, Text } from '@umami/react-zen';
import { LinkButton } from '@/components/common/LinkButton'; import { LinkButton } from '@/components/common/LinkButton';
import { LoadingPanel } from '@/components/common/LoadingPanel'; import { LoadingPanel } from '@/components/common/LoadingPanel';
import { import {
@ -114,11 +114,18 @@ export function MetricsTable({
}, [data, dataFilter, search, limit, formatValue, type]); }, [data, dataFilter, search, limit, formatValue, type]);
const downloadData = isExpanded ? data : filteredData; const downloadData = isExpanded ? data : filteredData;
const hasActions = data && (allowSearch || allowDownload || onClose || children);
return ( return (
<LoadingPanel data={data} isFetching={isFetching} isLoading={isLoading} error={error}> <LoadingPanel
<Grid rows="40px 1fr" height="100%" overflow="hidden" gap> data={data}
<Row alignItems="center"> isFetching={isFetching}
isLoading={isLoading}
error={error}
height="100%"
>
{hasActions && (
<Row alignItems="center" paddingBottom="3">
{allowSearch && <SearchField value={search} onSearch={setSearch} delay={300} />} {allowSearch && <SearchField value={search} onSearch={setSearch} delay={300} />}
<Row justifyContent="flex-end" flexGrow={1} gap> <Row justifyContent="flex-end" flexGrow={1} gap>
{children} {children}
@ -132,13 +139,22 @@ export function MetricsTable({
)} )}
</Row> </Row>
</Row> </Row>
<Column overflowY="auto" minHeight="0" height="100%" paddingRight="3" overflow="hidden"> )}
<Column
overflowY="auto"
minHeight="0"
height="100%"
paddingRight="3"
overflow="hidden"
flexGrow={1}
>
{data && {data &&
(isExpanded ? ( (isExpanded ? (
<ListExpandedTable {...(props as ListExpandedTableProps)} data={data} /> <ListExpandedTable {...(props as ListExpandedTableProps)} data={data} />
) : ( ) : (
<ListTable {...(props as ListTableProps)} data={filteredData} /> <ListTable {...(props as ListTableProps)} data={filteredData} />
))} ))}
</Column>
{showMore && limit && ( {showMore && limit && (
<Row justifyContent="center"> <Row justifyContent="center">
<LinkButton href={updateParams({ view: type })} variant="quiet"> <LinkButton href={updateParams({ view: type })} variant="quiet">
@ -149,8 +165,6 @@ export function MetricsTable({
</LinkButton> </LinkButton>
</Row> </Row>
)} )}
</Column>
</Grid>
</LoadingPanel> </LoadingPanel>
); );
} }

View file

@ -7,15 +7,12 @@ import { useContext } from 'react';
import { MetricsTable, MetricsTableProps } from './MetricsTable'; import { MetricsTable, MetricsTableProps } from './MetricsTable';
export interface PagesTableProps extends MetricsTableProps { export interface PagesTableProps extends MetricsTableProps {
type: string;
allowFilter?: boolean; allowFilter?: boolean;
} }
export function PagesTable({ allowFilter, ...props }: PagesTableProps) { export function PagesTable({ type, allowFilter, ...props }: PagesTableProps) {
const { const { router, updateParams } = useNavigation();
router,
updateParams,
query: { view = 'path' },
} = useNavigation();
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();
const { domain } = useContext(WebsiteContext); const { domain } = useContext(WebsiteContext);
@ -45,11 +42,11 @@ export function PagesTable({ allowFilter, ...props }: PagesTableProps) {
const renderLink = ({ x }) => { const renderLink = ({ x }) => {
return ( return (
<FilterLink <FilterLink
id={view === 'entry' || view === 'exit' ? 'path' : view} id={type === 'entry' || type === 'exit' ? 'path' : type}
value={x} value={x}
label={!x && formatMessage(labels.none)} label={!x && formatMessage(labels.none)}
externalUrl={ externalUrl={
view !== 'title' type !== 'title'
? `${domain.startsWith('http') ? domain : `https://${domain}`}${x}` ? `${domain.startsWith('http') ? domain : `https://${domain}`}${x}`
: null : null
} }
@ -61,12 +58,12 @@ export function PagesTable({ allowFilter, ...props }: PagesTableProps) {
<MetricsTable <MetricsTable
{...props} {...props}
title={formatMessage(labels.pages)} title={formatMessage(labels.pages)}
type={view} type={type}
metric={formatMessage(labels.visitors)} metric={formatMessage(labels.visitors)}
dataFilter={emptyFilter} dataFilter={emptyFilter}
renderLabel={renderLink} renderLabel={renderLink}
> >
{allowFilter && <FilterButtons items={buttons} value={view} onChange={handleChange} />} {allowFilter && <FilterButtons items={buttons} value={type} onChange={handleChange} />}
</MetricsTable> </MetricsTable>
); );
} }

View file

@ -16,7 +16,7 @@ const filters = {
export function QueryParametersTable({ export function QueryParametersTable({
allowFilter, allowFilter,
...props ...props
}: { allowFilter: boolean } & MetricsTableProps) { }: { allowFilter?: boolean } & MetricsTableProps) {
const [filter, setFilter] = useState(FILTER_COMBINED); const [filter, setFilter] = useState(FILTER_COMBINED);
const { formatMessage, labels } = useMessages(); const { formatMessage, labels } = useMessages();