Pixel/link metrics pages.

This commit is contained in:
Mike Cao 2025-08-21 01:33:20 -07:00
parent 789b8b36d8
commit 8e766e2db7
42 changed files with 530 additions and 49 deletions

View file

@ -0,0 +1,34 @@
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';
import { WebsiteMonthSelect } from '@/components/input/WebsiteMonthSelect';
import { ExportButton } from '@/components/input/ExportButton';
export function PixelControls({
pixelId: websiteId,
allowFilter = true,
allowDateFilter = true,
allowMonthFilter,
allowCompare,
allowDownload = false,
}: {
pixelId: string;
allowFilter?: boolean;
allowCompare?: boolean;
allowDateFilter?: boolean;
allowMonthFilter?: boolean;
allowDownload?: boolean;
}) {
return (
<Column gap>
<Row alignItems="center" justifyContent="space-between" gap="3">
{allowFilter ? <WebsiteFilterButton websiteId={websiteId} /> : <div />}
{allowDateFilter && <WebsiteDateFilter websiteId={websiteId} allowCompare={allowCompare} />}
{allowDownload && <ExportButton websiteId={websiteId} />}
{allowMonthFilter && <WebsiteMonthSelect websiteId={websiteId} />}
</Row>
{allowFilter && <FilterBar websiteId={websiteId} />}
</Column>
);
}

View file

@ -0,0 +1,22 @@
import { usePixel, useMessages, useSlug } from '@/components/hooks';
import { PageHeader } from '@/components/common/PageHeader';
import { Icon, Text } from '@umami/react-zen';
import { ExternalLink } from '@/components/icons';
import { LinkButton } from '@/components/common/LinkButton';
export function PixelHeader() {
const { formatMessage, labels } = useMessages();
const { getSlugUrl } = useSlug('pixel');
const pixel = usePixel();
return (
<PageHeader title={pixel.name} description={pixel.url}>
<LinkButton href={getSlugUrl(pixel.slug)}>
<Icon>
<ExternalLink />
</Icon>
<Text>{formatMessage(labels.view)}</Text>
</LinkButton>
</PageHeader>
);
}

View file

@ -0,0 +1,71 @@
import { useDateRange, useMessages } from '@/components/hooks';
import { MetricCard } from '@/components/metrics/MetricCard';
import { MetricsBar } from '@/components/metrics/MetricsBar';
import { formatLongNumber } from '@/lib/format';
import { useWebsiteStatsQuery } from '@/components/hooks/queries/useWebsiteStatsQuery';
import { LoadingPanel } from '@/components/common/LoadingPanel';
export function PixelMetricsBar({
pixelId,
}: {
pixelId: string;
showChange?: boolean;
compareMode?: boolean;
}) {
const { dateRange } = useDateRange(pixelId);
const { formatMessage, labels } = useMessages();
const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(pixelId);
const isAllTime = dateRange.value === 'all';
const { pageviews, visitors, visits, comparison } = data || {};
const metrics = data
? [
{
value: visitors,
label: formatMessage(labels.visitors),
change: visitors - comparison.visitors,
formatValue: formatLongNumber,
},
{
value: visits,
label: formatMessage(labels.visits),
change: visits - comparison.visits,
formatValue: formatLongNumber,
},
{
value: pageviews,
label: formatMessage(labels.views),
change: pageviews - comparison.pageviews,
formatValue: formatLongNumber,
},
]
: null;
return (
<LoadingPanel
data={metrics}
isLoading={isLoading}
isFetching={isFetching}
error={error}
minHeight="136px"
>
<MetricsBar>
{metrics?.map(({ label, value, prev, change, formatValue, reverseColors }: any) => {
return (
<MetricCard
key={label}
value={value}
previousValue={prev}
label={label}
change={change}
formatValue={formatValue}
reverseColors={reverseColors}
showChange={!isAllTime}
/>
);
})}
</MetricsBar>
</LoadingPanel>
);
}

View file

@ -0,0 +1,59 @@
'use client';
import { PageBody } from '@/components/common/PageBody';
import { PixelProvider } from '@/app/(main)/pixels/PixelProvider';
import { PixelHeader } from '@/app/(main)/pixels/[pixelId]/PixelHeader';
import { Panel } from '@/components/common/Panel';
import { WebsiteChart } from '@/app/(main)/websites/[websiteId]/WebsiteChart';
import { PixelMetricsBar } from '@/app/(main)/pixels/[pixelId]/PixelMetricsBar';
import { PixelControls } from '@/app/(main)/pixels/[pixelId]/PixelControls';
import { Grid } from '@umami/react-zen';
import { GridRow } from '@/components/common/GridRow';
import { ReferrersTable } from '@/components/metrics/ReferrersTable';
import { BrowsersTable } from '@/components/metrics/BrowsersTable';
import { OSTable } from '@/components/metrics/OSTable';
import { DevicesTable } from '@/components/metrics/DevicesTable';
import { WorldMap } from '@/components/metrics/WorldMap';
import { CountriesTable } from '@/components/metrics/CountriesTable';
export function PixelPage({ pixelId }: { pixelId: string }) {
const props = { websiteId: pixelId, limit: 10, allowDownload: false };
return (
<PixelProvider pixelId={pixelId}>
<PageBody gap>
<PixelHeader />
<PixelControls pixelId={pixelId} />
<PixelMetricsBar pixelId={pixelId} showChange={true} />
<Panel>
<WebsiteChart websiteId={pixelId} />
</Panel>
<GridRow layout="two">
<Panel>
<ReferrersTable {...props} />
</Panel>
</GridRow>
<Grid gap="3">
<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>
<WorldMap websiteId={pixelId} />
</Panel>
<Panel>
<CountriesTable {...props} />
</Panel>
</GridRow>
</Grid>
</PageBody>
</PixelProvider>
);
}

View file

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