umami/src/app/(main)/websites/[websiteId]/(reports)/funnels/Funnel.tsx
Mike Cao 801a3ec6bb Hide add/edit buttons on share pages.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 14:51:46 -08:00

137 lines
4.9 KiB
TypeScript

import { Box, Column, Dialog, Grid, Icon, ProgressBar, Row, Text } from '@umami/react-zen';
import { LoadingPanel } from '@/components/common/LoadingPanel';
import { useMessages, useNavigation, useResultQuery } from '@/components/hooks';
import { File, User } from '@/components/icons';
import { ReportEditButton } from '@/components/input/ReportEditButton';
import { ChangeLabel } from '@/components/metrics/ChangeLabel';
import { Lightning } from '@/components/svg';
import { formatLongNumber } from '@/lib/format';
import { FunnelEditForm } from './FunnelEditForm';
type FunnelResult = {
type: string;
value: string;
visitors: number;
previous: number;
dropped: number;
dropoff: number;
remaining: number;
};
export function Funnel({ id, name, type, parameters, websiteId }) {
const { formatMessage, labels } = useMessages();
const { pathname } = useNavigation();
const isSharePage = pathname.includes('/share/');
const { data, error, isLoading } = useResultQuery(type, {
websiteId,
...parameters,
});
return (
<LoadingPanel data={data} isLoading={isLoading} error={error}>
<Grid gap>
<Grid columns="1fr auto" gap>
<Column gap>
<Row>
<Text size="4" weight="bold">
{name}
</Text>
</Row>
</Column>
{!isSharePage && (
<Column>
<ReportEditButton id={id} name={name} type={type}>
{({ close }) => {
return (
<Dialog
title={formatMessage(labels.funnel)}
style={{ minHeight: 300, minWidth: 400 }}
>
<FunnelEditForm id={id} websiteId={websiteId} onClose={close} />
</Dialog>
);
}}
</ReportEditButton>
</Column>
)}
</Grid>
{data?.map(
(
{ type, value, visitors, previous, dropped, dropoff, remaining }: FunnelResult,
index: number,
) => {
const isPage = type === 'path';
return (
<Grid key={index} columns="auto 1fr" gap="6">
<Column alignItems="center" position="relative">
<Row
borderRadius="full"
backgroundColor="3"
width="40px"
height="40px"
justifyContent="center"
alignItems="center"
style={{ zIndex: 1 }}
>
<Text weight="bold" size="3">
{index + 1}
</Text>
</Row>
{index > 0 && (
<Box
position="absolute"
backgroundColor="3"
width="2px"
height="120px"
top="-100%"
/>
)}
</Column>
<Column gap>
<Row alignItems="center" justifyContent="space-between" gap>
<Text color="muted">
{formatMessage(isPage ? labels.viewedPage : labels.triggeredEvent)}
</Text>
<Text color="muted">{formatMessage(labels.conversionRate)}</Text>
</Row>
<Row alignItems="center" justifyContent="space-between" gap>
<Row alignItems="center" gap>
<Icon>{type === 'path' ? <File /> : <Lightning />}</Icon>
<Text>{value}</Text>
</Row>
<Row alignItems="center" gap>
{index > 0 && (
<ChangeLabel value={-dropped} title={`${-Math.round(dropoff * 100)}%`}>
{formatLongNumber(dropped)}
</ChangeLabel>
)}
<Icon>
<User />
</Icon>
<Text title={visitors.toString()} transform="lowercase">
{`${formatLongNumber(visitors)} ${formatMessage(labels.visitors)}`}
</Text>
</Row>
</Row>
<Row alignItems="center" gap="6">
<ProgressBar
value={visitors || 0}
minValue={0}
maxValue={previous || 1}
style={{ width: '100%' }}
/>
<Row minWidth="90px" justifyContent="end">
<Text weight="bold" size="7">
{Math.round(remaining * 100)}%
</Text>
</Row>
</Row>
</Column>
</Grid>
);
},
)}
</Grid>
</LoadingPanel>
);
}