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-empty-interface": "off",
"@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": {
"React": "writable"

1
next-env.d.ts vendored
View file

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

View file

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

View file

@ -6,7 +6,7 @@ import { Panel } from '@/components/common/Panel';
import { WebsiteChart } from '@/app/(main)/websites/[websiteId]/WebsiteChart';
import { LinkMetricsBar } from '@/app/(main)/links/[linkId]/LinkMetricsBar';
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 { ReferrersTable } from '@/components/metrics/ReferrersTable';
import { BrowsersTable } from '@/components/metrics/BrowsersTable';
@ -14,9 +14,16 @@ import { OSTable } from '@/components/metrics/OSTable';
import { DevicesTable } from '@/components/metrics/DevicesTable';
import { WorldMap } from '@/components/metrics/WorldMap';
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 }) {
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 (
<LinkProvider linkId={linkId}>
@ -27,29 +34,67 @@ export function LinkPage({ linkId }: { linkId: string }) {
<Panel>
<WebsiteChart websiteId={linkId} />
</Panel>
<GridRow layout="two">
<Panel>
<ReferrersTable {...props} />
</Panel>
</GridRow>
<Grid gap="3">
<GridRow layout="three">
<Grid gap>
<GridRow layout="one" {...rowProps}>
<Panel>
<BrowsersTable {...props} />
</Panel>
<Panel>
<OSTable {...props} />
</Panel>
<Panel>
<DevicesTable {...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>
</GridRow>
<GridRow layout="two-one">
<GridRow layout="two-one" {...rowProps}>
<Panel gridColumn="span 2" noPadding>
<WorldMap websiteId={linkId} />
</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>
</GridRow>
</Grid>

View file

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

View file

@ -6,7 +6,7 @@ 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 { Grid, Tab, TabList, TabPanel, Tabs } from '@umami/react-zen';
import { GridRow } from '@/components/common/GridRow';
import { ReferrersTable } from '@/components/metrics/ReferrersTable';
import { BrowsersTable } from '@/components/metrics/BrowsersTable';
@ -14,9 +14,16 @@ import { OSTable } from '@/components/metrics/OSTable';
import { DevicesTable } from '@/components/metrics/DevicesTable';
import { WorldMap } from '@/components/metrics/WorldMap';
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 }) {
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 (
<PixelProvider pixelId={pixelId}>
@ -27,29 +34,67 @@ export function PixelPage({ pixelId }: { pixelId: string }) {
<Panel>
<WebsiteChart websiteId={pixelId} />
</Panel>
<GridRow layout="two">
<Panel>
<ReferrersTable {...props} />
</Panel>
</GridRow>
<Grid gap="3">
<GridRow layout="three">
<Grid gap>
<GridRow layout="one" {...rowProps}>
<Panel>
<BrowsersTable {...props} />
</Panel>
<Panel>
<OSTable {...props} />
</Panel>
<Panel>
<DevicesTable {...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>
</GridRow>
<GridRow layout="two-one">
<GridRow layout="two-one" {...rowProps}>
<Panel gridColumn="span 2" noPadding>
<WorldMap websiteId={pixelId} />
</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>
</GridRow>
</Grid>

View file

@ -1,10 +1,10 @@
'use client';
import { ReactNode } from 'react';
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 { WebsiteHeader } from './WebsiteHeader';
import { WebsiteNav } from '@/app/(main)/websites/[websiteId]/WebsiteNav';
import { WebsiteNav } from './WebsiteNav';
export function WebsiteLayout({ websiteId, children }: { websiteId: string; children: ReactNode }) {
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 { Panel } from '@/components/common/Panel';
import { PagesTable } from '@/components/metrics/PagesTable';
@ -8,37 +8,97 @@ import { OSTable } from '@/components/metrics/OSTable';
import { DevicesTable } from '@/components/metrics/DevicesTable';
import { WorldMap } from '@/components/metrics/WorldMap';
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 }) {
const props = { websiteId, limit: 10, allowDownload: false };
const { formatMessage, labels } = useMessages();
const tableProps = { websiteId, limit: 10, allowDownload: false };
const rowProps = { minHeight: 570 };
return (
<Grid gap="3">
<GridRow layout="two">
<GridRow layout="two" {...rowProps}>
<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>
<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>
</GridRow>
<GridRow layout="three">
<Panel>
<BrowsersTable {...props} />
</Panel>
<Panel>
<OSTable {...props} />
</Panel>
<Panel>
<DevicesTable {...props} />
</Panel>
</GridRow>
<GridRow layout="two-one">
<GridRow layout="two-one" {...rowProps}>
<Panel gridColumn="span 2" noPadding>
<WorldMap websiteId={websiteId} />
</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>
</GridRow>
</Grid>

View file

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

View file

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

View file

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

View file

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