New website nav.

This commit is contained in:
Mike Cao 2025-07-15 03:35:18 -07:00
parent 5e6799a715
commit a534c51b5e
38 changed files with 190 additions and 159 deletions

View file

@ -0,0 +1,69 @@
import { Text, DataTable, DataColumn } from '@umami/react-zen';
import { useMessages, useResultQuery, useFormat, useFields } from '@/components/hooks';
import { LoadingPanel } from '@/components/common/LoadingPanel';
import { formatShortTime } from '@/lib/format';
export interface BreakdownProps {
websiteId: string;
startDate: Date;
endDate: Date;
selectedFields: string[];
}
export function Breakdown({ websiteId, selectedFields = [], startDate, endDate }: BreakdownProps) {
const { formatMessage, labels } = useMessages();
const { formatValue } = useFormat();
const { fields } = useFields();
const { data, error, isLoading } = useResultQuery<any>(
'breakdown',
{
websiteId,
startDate,
endDate,
fields: selectedFields,
},
{ enabled: !!selectedFields.length },
);
return (
<LoadingPanel data={data} isLoading={isLoading} error={error}>
<DataTable data={data}>
{selectedFields.map(field => {
return (
<DataColumn key={field} id={field} label={fields.find(f => f.name === field)?.label}>
{row => {
const value = formatValue(row[field], field);
return (
<Text truncate title={value}>
{value}
</Text>
);
}}
</DataColumn>
);
})}
<DataColumn id="visitors" label={formatMessage(labels.visitors)} align="end">
{row => row?.['visitors']?.toLocaleString()}
</DataColumn>
<DataColumn id="visits" label={formatMessage(labels.visits)} align="end">
{row => row?.['visits']?.toLocaleString()}
</DataColumn>
<DataColumn id="views" label={formatMessage(labels.views)} align="end">
{row => row?.['views']?.toLocaleString()}
</DataColumn>
<DataColumn id="bounceRate" label={formatMessage(labels.bounceRate)} align="end">
{row => {
const n = (Math.min(row?.['visits'], row?.['bounces']) / row?.['visits']) * 100;
return Math.round(+n) + '%';
}}
</DataColumn>
<DataColumn id="visitDuration" label={formatMessage(labels.visitDuration)} align="end">
{row => {
const n = (row?.['totaltime'] / row?.['visits']) * 100;
return `${+n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`;
}}
</DataColumn>
</DataTable>
</LoadingPanel>
);
}

View file

@ -0,0 +1,55 @@
'use client';
import { useState } from 'react';
import { Button, Column, Box, Text, Icon, DialogTrigger, Modal, Dialog } from '@umami/react-zen';
import { useDateRange, useMessages } from '@/components/hooks';
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';
export function BreakdownPage({ websiteId }: { websiteId: string }) {
const {
dateRange: { startDate, endDate },
} = useDateRange(websiteId);
const [fields, setFields] = useState(['path']);
return (
<Column gap>
<WebsiteControls websiteId={websiteId} />
<FieldsButton value={fields} onChange={setFields} />
<Panel height="900px" overflow="auto" allowFullscreen>
<Breakdown
websiteId={websiteId}
startDate={startDate}
endDate={endDate}
selectedFields={fields}
/>
</Panel>
</Column>
);
}
const FieldsButton = ({ value, onChange }) => {
const { formatMessage, labels } = useMessages();
return (
<Box>
<DialogTrigger>
<Button>
<Icon>
<ListCheck />
</Icon>
<Text>Fields</Text>
</Button>
<Modal>
<Dialog title={formatMessage(labels.fields)} style={{ width: 400 }}>
{({ close }) => (
<FieldSelectForm selectedFields={value} onChange={onChange} onClose={close} />
)}
</Dialog>
</Modal>
</DialogTrigger>
</Box>
);
};

View file

@ -0,0 +1,46 @@
import { Column, List, ListItem, Grid, Button } from '@umami/react-zen';
import { useFields, useMessages } from '@/components/hooks';
import { useState } from 'react';
export function FieldSelectForm({
selectedFields = [],
onChange,
onClose,
}: {
selectedFields?: string[];
onChange: (values: string[]) => void;
onClose?: () => void;
}) {
const [selected, setSelected] = useState(selectedFields);
const { formatMessage, labels } = useMessages();
const { fields } = useFields();
const handleChange = (value: string[]) => {
setSelected(value);
};
const handleApply = () => {
onChange?.(selected);
onClose();
};
return (
<Column width="300px" gap="6">
<List value={selected} onChange={handleChange} selectionMode="multiple">
{fields.map(({ name, label }) => {
return (
<ListItem key={name} id={name}>
{label}
</ListItem>
);
})}
</List>
<Grid columns="1fr 1fr" gap>
<Button onPress={onClose}>{formatMessage(labels.cancel)}</Button>
<Button onPress={handleApply} variant="primary">
{formatMessage(labels.apply)}
</Button>
</Grid>
</Column>
);
}

View file

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