Responsive fixes.

This commit is contained in:
Mike Cao 2025-11-12 16:39:58 -08:00
parent 3d8402d2f1
commit 8a66603d32
16 changed files with 302 additions and 231 deletions

View file

@ -95,7 +95,7 @@ export function Attribution({
})}
</MetricsBar>
<SectionHeader title={formatMessage(labels.sources)} />
<Grid columns="1fr 1fr" gap>
<Grid columns={{ xs: '1fr', md: '1fr 1fr' }} gap>
<Panel>
<AttributionTable data={data?.['referrer']} title={formatMessage(labels.referrer)} />
</Panel>
@ -104,7 +104,7 @@ export function Attribution({
</Panel>
</Grid>
<SectionHeader title="UTM" />
<Grid columns="1fr 1fr" gap>
<Grid columns={{ xs: '1fr', md: '1fr 1fr' }} gap>
<Panel>
<AttributionTable data={data?.['utm_source']} title={formatMessage(labels.sources)} />
</Panel>

View file

@ -17,7 +17,7 @@ export function AttributionPage({ websiteId }: { websiteId: string }) {
return (
<Column gap="6">
<WebsiteControls websiteId={websiteId} />
<Grid columns="1fr 1fr 1fr" gap>
<Grid columns={{ xs: '1fr', md: '1fr 1fr 1fr' }} gap>
<Column>
<Select
label={formatMessage(labels.model)}

View file

@ -1,4 +1,4 @@
import { Text, DataTable, DataColumn } from '@umami/react-zen';
import { Text, DataTable, DataColumn, Column } from '@umami/react-zen';
import { useMessages, useResultQuery, useFormat, useFields } from '@/components/hooks';
import { LoadingPanel } from '@/components/common/LoadingPanel';
import { formatShortTime } from '@/lib/format';
@ -27,43 +27,65 @@ export function Breakdown({ websiteId, selectedFields = [], startDate, endDate }
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'];
return `${+n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`;
}}
</DataColumn>
</DataTable>
<Column overflow="auto" minHeight="0" height="100%">
<DataTable data={data} style={{ tableLayout: 'fixed' }}>
{selectedFields.map(field => {
return (
<DataColumn
key={field}
id={field}
label={fields.find(f => f.name === field)?.label}
width="minmax(120px, 1fr)"
>
{row => {
const value = formatValue(row[field], field);
return (
<Text truncate title={value}>
{value}
</Text>
);
}}
</DataColumn>
);
})}
<DataColumn
id="visitors"
label={formatMessage(labels.visitors)}
align="end"
width="120px"
>
{row => row?.['visitors']?.toLocaleString()}
</DataColumn>
<DataColumn id="visits" label={formatMessage(labels.visits)} align="end" width="120px">
{row => row?.['visits']?.toLocaleString()}
</DataColumn>
<DataColumn id="views" label={formatMessage(labels.views)} align="end" width="120px">
{row => row?.['views']?.toLocaleString()}
</DataColumn>
<DataColumn
id="bounceRate"
label={formatMessage(labels.bounceRate)}
align="end"
width="120px"
>
{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"
width="120px"
>
{row => {
const n = row?.['totaltime'] / row?.['visits'];
return `${+n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`;
}}
</DataColumn>
</DataTable>
</Column>
</LoadingPanel>
);
}

View file

@ -2,7 +2,7 @@
import { FieldSelectForm } from '@/app/(main)/websites/[websiteId]/(reports)/breakdown/FieldSelectForm';
import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
import { Panel } from '@/components/common/Panel';
import { useDateRange, useMessages, useMobile } from '@/components/hooks';
import { useDateRange, useMessages } from '@/components/hooks';
import { ListCheck } from '@/components/icons';
import { DialogButton } from '@/components/input/DialogButton';
import { Column, Row } from '@umami/react-zen';
@ -14,12 +14,10 @@ export function BreakdownPage({ websiteId }: { websiteId: string }) {
dateRange: { startDate, endDate },
} = useDateRange();
const [fields, setFields] = useState(['path']);
const { isMobile } = useMobile();
return (
<Column gap>
<WebsiteControls websiteId={websiteId} />
<Row alignItems="center" justifyContent={isMobile ? 'flex-end' : 'flex-start'}>
<Row alignItems="center" justifyContent="flex-start">
<FieldsButton value={fields} onChange={setFields} />
</Row>
<Panel height="900px" overflow="auto" allowFullscreen>
@ -41,8 +39,9 @@ const FieldsButton = ({ value, onChange }) => {
<DialogButton
icon={<ListCheck />}
label={formatMessage(labels.fields)}
width="800px"
width="400px"
minHeight="300px"
variant="outline"
>
{({ close }) => {
return <FieldSelectForm selectedFields={value} onChange={onChange} onClose={close} />;

View file

@ -51,51 +51,72 @@ export function Retention({ websiteId, days = DAYS, startDate, endDate }: Retent
<LoadingPanel data={data} isLoading={isLoading} error={error}>
{data && (
<Panel allowFullscreen height="900px">
<Column gap="1" width="100%" overflow="auto">
<Grid
columns="120px repeat(10, 100px)"
alignItems="center"
gap="1"
height="50px"
autoFlow="column"
>
<Column>
<Text weight="bold" align="center">
{formatMessage(labels.cohort)}
</Text>
</Column>
{days.map(n => (
<Column key={n}>
<Text weight="bold" align="center" wrap="nowrap">
{formatMessage(labels.day)} {n}
<Column
paddingY="6"
paddingX={{ xs: '3', md: '6' }}
position="absolute"
top="40px"
left="0"
right="0"
bottom="0"
>
<Column gap="1" overflow="auto">
<Grid
columns="120px repeat(10, 100px)"
alignItems="center"
gap="1"
height="50px"
width="max-content"
minWidth="100%"
autoFlow="column"
>
<Column>
<Text weight="bold" align="center">
{formatMessage(labels.cohort)}
</Text>
</Column>
))}
</Grid>
{rows.map(({ date, visitors, records }: any, rowIndex: number) => {
return (
<Grid key={rowIndex} columns="120px repeat(10, 100px)" gap="1" autoFlow="column">
<Column justifyContent="center" gap="1">
<Text weight="bold">{formatDate(date, 'PP', locale)}</Text>
<Row alignItems="center" gap>
<Icon>
<Users />
</Icon>
<Text>{formatLongNumber(visitors)}</Text>
</Row>
{days.map(n => (
<Column key={n}>
<Text weight="bold" align="center" wrap="nowrap">
{formatMessage(labels.day)} {n}
</Text>
</Column>
{days.map(day => {
if (totalDays - rowIndex < day) {
return null;
}
const percentage = records.filter(a => a.day === day)[0]?.percentage;
return (
<Cell key={day}>{percentage ? `${Number(percentage).toFixed(2)}%` : ''}</Cell>
);
})}
</Grid>
);
})}
))}
</Grid>
{rows.map(({ date, visitors, records }: any, rowIndex: number) => {
return (
<Grid
key={rowIndex}
columns="120px repeat(10, 100px)"
gap="1"
autoFlow="column"
width="max-content"
minWidth="100%"
>
<Column justifyContent="center" gap="1">
<Text weight="bold">{formatDate(date, 'PP', locale)}</Text>
<Row alignItems="center" gap>
<Icon>
<Users />
</Icon>
<Text>{formatLongNumber(visitors)}</Text>
</Row>
</Column>
{days.map(day => {
if (totalDays - rowIndex < day) {
return null;
}
const percentage = records.filter(a => a.day === day)[0]?.percentage;
return (
<Cell key={day}>
{percentage ? `${Number(percentage).toFixed(2)}%` : ''}
</Cell>
);
})}
</Grid>
);
})}
</Column>
</Column>
</Panel>
)}

View file

@ -43,7 +43,7 @@ export function UTM({ websiteId, startDate, endDate }: UTMProps) {
return (
<Panel key={param}>
<Grid columns="1fr 1fr">
<Grid columns={{ xs: '1fr', md: '1fr 1fr' }} gap="6">
<Column>
<Heading>
<Text transform="capitalize">{param.replace(/^utm_/, '')}</Text>

View file

@ -34,6 +34,7 @@ export function ExpandedViewModal({
maxWidth: 1320,
width: '100vw',
height: isMobile ? '100dvh' : 'calc(100dvh - 40px)',
overflow: 'hidden',
}}
>
{({ close }) => {

View file

@ -23,10 +23,10 @@ export function WebsiteControls({
return (
<Column gap>
<Grid columns={{ xs: '1fr', md: 'auto 1fr' }} gap>
<Row alignItems="center" justifyContent="flex-end">
<Row alignItems="center" justifyContent="flex-start">
{allowFilter ? <WebsiteFilterButton websiteId={websiteId} /> : <div />}
</Row>
<Row alignItems="center" justifyContent="flex-end">
<Row alignItems="center" justifyContent={{ xs: 'flex-start', md: 'flex-end' }}>
{allowDateFilter && (
<WebsiteDateFilter websiteId={websiteId} allowCompare={allowCompare} />
)}

View file

@ -19,17 +19,23 @@ export function WebsiteExpandedView({
} = useNavigation();
return (
<Column gap>
<Row display={{ xs: 'flex', md: 'none' }}>
<Column height="100%" overflow="hidden" gap>
<Row id="expanded-mobile-menu-button" display={{ xs: 'flex', md: 'none' }}>
<MobileMenuButton>
{({ close }) => {
return <WebsiteExpandedMenu excludedIds={excludedIds} onItemClick={close} />;
return (
<Column padding="3">
<WebsiteExpandedMenu excludedIds={excludedIds} onItemClick={close} />
</Column>
);
}}
</MobileMenuButton>
</Row>
<Grid columns={{ xs: '1fr', md: 'auto 1fr' }} gap="6" height="100%" overflow="hidden">
<Grid columns={{ xs: '1fr', md: 'auto 1fr' }} gap="6" overflow="hidden">
<Column
id="metrics-expanded-menu"
display={{ xs: 'none', md: 'flex' }}
width="240px"
gap="6"
border="right"
paddingRight="3"
@ -37,7 +43,7 @@ export function WebsiteExpandedView({
>
<WebsiteExpandedMenu excludedIds={excludedIds} />
</Column>
<Column overflow="hidden">
<Column id="metrics-expanded-table" overflow="hidden">
<MetricsExpandedTable
title={formatMessage(labels[view])}
type={view}

View file

@ -19,15 +19,11 @@ export function WebsiteHeader({ showActions }: { showActions?: boolean }) {
return (
<PageHeader title={website.name} icon={<Favicon domain={website.domain} />} marginBottom="3">
<Row alignItems="center" gap="6">
<Row alignItems="center" gap="6" wrap="wrap">
<ActiveUsers websiteId={website.id} />
{showActions && (
<Row
display={{ xs: 'none', sm: 'none', md: 'none', lg: 'flex', xl: 'flex' }}
alignItems="center"
gap
>
<Row alignItems="center" gap>
<ShareButton websiteId={website.id} shareId={website.shareId} />
<LinkButton href={renderUrl(`/websites/${website.id}/settings`, false)}>
<Icon>

View file

@ -1,5 +1,5 @@
import { ReactNode } from 'react';
import { Heading, Icon, Row, RowProps, Text, Column } from '@umami/react-zen';
import { Heading, Icon, Row, Text, Column, Grid } from '@umami/react-zen';
export function PageHeader({
title,
@ -8,7 +8,6 @@ export function PageHeader({
icon,
showBorder = true,
children,
...props
}: {
title: string;
description?: string;
@ -18,16 +17,13 @@ export function PageHeader({
allowEdit?: boolean;
className?: string;
children?: ReactNode;
} & RowProps) {
}) {
return (
<Row
justifyContent="space-between"
alignItems="center"
<Grid
columns={{ xs: '1fr', md: '1fr 1fr' }}
paddingY="6"
marginBottom="6"
border={showBorder ? 'bottom' : undefined}
width="100%"
{...props}
>
<Column gap="2">
{label}
@ -46,6 +42,6 @@ export function PageHeader({
)}
</Column>
<Row justifyContent="flex-end">{children}</Row>
</Row>
</Grid>
);
}

View file

@ -28,6 +28,7 @@ export function WebsiteDateFilter({
query: { compare = 'prev', offset = 0 },
} = useNavigation();
const disableForward = isAllTime || isAfter(dateRange.endDate, new Date());
const showCompare = allowCompare && !isAllTime;
const websiteDateRange = useDateRangeQuery(websiteId);
@ -62,7 +63,7 @@ export function WebsiteDateFilter({
}, [dateRange]);
return (
<Row gap>
<Row wrap="wrap" gap>
{showButtons && !isAllTime && !isCustomRange && (
<Row gap="1">
<Button onPress={() => handleIncrement(-1)} variant="outline">
@ -85,7 +86,7 @@ export function WebsiteDateFilter({
renderDate={+offset !== 0}
/>
</Row>
{allowCompare && !isAllTime && (
{showCompare && (
<Row alignItems="center" gap>
<Text weight="bold">VS</Text>
<Row width="200px">

View file

@ -7,7 +7,7 @@ export interface MetricsBarProps extends GridProps {
export function MetricsBar({ children, ...props }: MetricsBarProps) {
return (
<Grid columns="repeat(auto-fit, minmax(140px, 1fr))" gap {...props}>
<Grid columns="repeat(auto-fit, minmax(160px, 1fr))" gap {...props}>
{children}
</Grid>
);

View file

@ -6,7 +6,7 @@ export const IP_ADDRESS_HEADERS = [
'fastly-client-ip', // Fastly
'x-nf-client-connection-ip', // Netlify
'do-connecting-ip', // Digital Ocean
'x-appengine-user-ip', // Google App Ending
'x-appengine-user-ip', // Google App Engine
'x-client-ip',
'x-cluster-client-ip',
'x-forwarded',