Added journey page. Removed dashboard.

This commit is contained in:
Mike Cao 2025-06-06 19:44:09 -07:00
parent 3847e32f39
commit cee05d762c
24 changed files with 328 additions and 422 deletions

View file

@ -1,54 +0,0 @@
import { Text, Icon } from '@umami/react-zen';
import { useMemo } from 'react';
import { firstBy } from 'thenby';
import { WebsiteChart } from './WebsiteChart';
import { useDashboard } from '@/store/dashboard';
import { WebsiteControls } from './WebsiteControls';
import { WebsiteMetricsBar } from './WebsiteMetricsBar';
import { useMessages, useNavigation } from '@/components/hooks';
import { LinkButton } from '@/components/common/LinkButton';
import { Arrow } from '@/components/icons';
export function WebsiteChartList({
websites,
showCharts,
limit,
}: {
websites: any[];
showCharts?: boolean;
limit?: number;
}) {
const { formatMessage, labels } = useMessages();
const { websiteOrder, websiteActive } = useDashboard();
const { renderTeamUrl } = useNavigation();
const ordered = useMemo(() => {
return websites
.filter(website => (websiteActive.length ? websiteActive.includes(website.id) : true))
.map(website => ({ ...website, order: websiteOrder.indexOf(website.id) || 0 }))
.sort(firstBy('order'));
}, [websites, websiteOrder, websiteActive]);
return (
<div>
{ordered.map(({ id }: { id: string }, index) => {
return index < limit ? (
<div key={id}>
<WebsiteControls websiteId={id} showLinks={false}>
<LinkButton href={renderTeamUrl(`/websites/${id}`)} variant="primary">
<Text>{formatMessage(labels.viewDetails)}</Text>
<Icon>
<Icon>
<Arrow />
</Icon>
</Icon>
</LinkButton>
</WebsiteControls>
<WebsiteMetricsBar websiteId={id} showChange={true} />
{showCharts && <WebsiteChart websiteId={id} />}
</div>
) : null;
})}
</div>
);
}

View file

@ -65,7 +65,7 @@ export function WebsiteNav({ websiteId }: { websiteId: string }) {
},
{
id: 'journeys',
label: formatMessage(labels.journey),
label: formatMessage(labels.journeys),
icon: <Path />,
path: '/journeys',
},

View file

@ -1,4 +1,4 @@
import { Grid, Column, Row, Text, Icon, ProgressBar, Dialog } from '@umami/react-zen';
import { Grid, Column, Row, Text, Icon, ProgressBar, Dialog, Box } from '@umami/react-zen';
import { useMessages, useResultQuery } from '@/components/hooks';
import { LoadingPanel } from '@/components/common/LoadingPanel';
import { File, Lightning, User } from '@/components/icons';
@ -13,7 +13,7 @@ type FunnelResult = {
visitors: number;
previous: number;
dropped: number;
droppoff: number;
dropoff: number;
remaining: number;
};
@ -53,25 +53,35 @@ export function Funnel({ id, name, type, parameters, websiteId, startDate, endDa
</Grid>
{data?.map(
(
{ type, value, visitors, previous, dropped, remaining }: FunnelResult,
{ type, value, visitors, previous, dropped, dropoff, remaining }: FunnelResult,
index: number,
) => {
const isPage = type === 'page';
return (
<Grid key={index} columns="auto 1fr" gap="6">
<Column>
<Column alignItems="center" position="relative">
<Row
borderRadius="full"
backgroundColor="2"
backgroundColor="3"
width="40px"
height="40px"
justifyContent="center"
alignItems="center"
style={{ zIndex: 1 }}
>
<Text weight="bold" size="4">
<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>
@ -87,12 +97,16 @@ export function Funnel({ id, name, type, parameters, websiteId, startDate, endDa
</Row>
<Row alignItems="center" gap>
{index > 0 && (
<ChangeLabel value={-dropped}>{formatLongNumber(dropped)}</ChangeLabel>
<ChangeLabel value={-dropped} title={`${-Math.round(dropoff * 100)}%`}>
{formatLongNumber(dropped)}
</ChangeLabel>
)}
<Icon>
<User />
</Icon>
<Text title={visitors.toString()}>{formatLongNumber(visitors)}</Text>
<Text title={visitors.toString()} transform="lowercase">
{`${formatLongNumber(visitors)} ${formatMessage(labels.visitors)}`}
</Text>
</Row>
</Row>
<Row alignItems="center" gap="6">
@ -102,9 +116,11 @@ export function Funnel({ id, name, type, parameters, websiteId, startDate, endDa
maxValue={previous || 1}
style={{ width: '100%' }}
/>
<Text weight="bold" size="7">
{Math.round(remaining * 100)}%
</Text>
<Row minWidth="90px" justifyContent="end">
<Text weight="bold" size="7">
{Math.round(remaining * 100)}%
</Text>
</Row>
</Row>
</Column>
</Grid>

View file

@ -0,0 +1,99 @@
import { Grid, Row, Column, Text, Icon, Button, Dialog } from '@umami/react-zen';
import { ReportEditButton } from '@/components/input/ReportEditButton';
import { useMessages, useResultQuery } from '@/components/hooks';
import { Arrow, Eye } from '@/components/icons';
import { LoadingPanel } from '@/components/common/LoadingPanel';
import { JourneyEditForm } from './JourneyEditForm';
export interface JourneyProps {
id: string;
name: string;
type: string;
parameters: {
steps: string;
startStep: string;
endStep: string;
};
websiteId: string;
startDate: Date;
endDate: Date;
}
export type GoalData = { num: number; total: number };
export function Journey({
id,
name,
type,
parameters,
websiteId,
startDate,
endDate,
}: JourneyProps) {
const { formatMessage, labels } = useMessages();
const { data, error, isLoading } = useResultQuery<GoalData>(type, {
websiteId,
dateRange: {
startDate,
endDate,
},
parameters,
});
return (
<LoadingPanel isEmpty={!data} isLoading={isLoading} error={error}>
<Grid gap>
<Grid columns="1fr auto" gap>
<Column gap>
<Row>
<Text size="4" weight="bold">
{name}
</Text>
</Row>
</Column>
<Column>
<ReportEditButton id={id} name={name} type={type}>
{({ close }) => {
return (
<Dialog
title={formatMessage(labels.goal)}
variant="modal"
style={{ minHeight: 375, minWidth: 400 }}
>
<JourneyEditForm id={id} websiteId={websiteId} onClose={close} />
</Dialog>
);
}}
</ReportEditButton>
</Column>
</Grid>
<Row alignItems="center" gap>
<Text>
{formatMessage(labels.steps)}: {parameters?.steps}
</Text>
</Row>
<Row alignItems="center" justifyContent="space-between">
<Row alignItems="center" gap="6">
<Text>
{formatMessage(labels.startStep)}: {parameters?.startStep}
</Text>
<Icon>
<Arrow />
</Icon>
<Text>
{formatMessage(labels.endStep)}: {parameters?.endStep || formatMessage(labels.none)}
</Text>
</Row>
<Button>
<Row alignItems="center" gap>
<Icon>
<Eye />
</Icon>
<Text>View</Text>
</Row>
</Button>
</Row>
</Grid>
</LoadingPanel>
);
}

View file

@ -0,0 +1,28 @@
import { Button, DialogTrigger, Dialog, Icon, Text, Modal } from '@umami/react-zen';
import { useMessages } from '@/components/hooks';
import { JourneyEditForm } from './JourneyEditForm';
import { Plus } from '@/components/icons';
export function JourneyAddButton({ websiteId }: { websiteId: string }) {
const { formatMessage, labels } = useMessages();
return (
<DialogTrigger>
<Button variant="primary">
<Icon>
<Plus />
</Icon>
<Text>{formatMessage(labels.journey)}</Text>
</Button>
<Modal>
<Dialog
variant="modal"
title={formatMessage(labels.journey)}
style={{ minHeight: 375, minWidth: 400 }}
>
{({ close }) => <JourneyEditForm websiteId={websiteId} onClose={close} />}
</Dialog>
</Modal>
</DialogTrigger>
);
}

View file

@ -0,0 +1,96 @@
import {
Form,
FormField,
TextField,
FormButtons,
FormSubmitButton,
Button,
Select,
ListItem,
Loading,
} from '@umami/react-zen';
import { useApi, useMessages, useModified, useReportQuery } from '@/components/hooks';
const JOURNEY_STEPS = ['3', '4', '5', '6', '7'];
export function JourneyEditForm({
id,
websiteId,
onSave,
onClose,
}: {
id?: string;
websiteId: string;
onSave?: () => void;
onClose?: () => void;
}) {
const { formatMessage, labels } = useMessages();
const { touch } = useModified();
const { post, useMutation } = useApi();
const { data } = useReportQuery(id);
const { mutate, error, isPending } = useMutation({
mutationFn: (params: any) => post(`/reports${id ? `/${id}` : ''}`, params),
});
const handleSubmit = async ({ name, ...parameters }) => {
mutate(
{ ...data, id, name, type: 'journey', websiteId, parameters },
{
onSuccess: async () => {
if (id) touch(`report:${id}`);
touch('reports:journey');
onSave?.();
onClose?.();
},
},
);
};
if (id && !data) {
return <Loading position="page" />;
}
const defaultValues = {
name: data?.name || '',
steps: data?.steps || '5',
startStep: data?.parameters?.startStep || '',
endStep: data?.parameters?.endStep || '',
};
return (
<Form onSubmit={handleSubmit} error={error?.message} defaultValues={defaultValues}>
<FormField
name="name"
label={formatMessage(labels.name)}
rules={{ required: formatMessage(labels.required) }}
>
<TextField />
</FormField>
<FormField
name="steps"
label={formatMessage(labels.steps)}
rules={{ required: formatMessage(labels.required) }}
>
<Select>
{JOURNEY_STEPS.map(step => (
<ListItem key={step} id={step}>
{step}
</ListItem>
))}
</Select>
</FormField>
<FormField name="startStep" label={formatMessage(labels.startStep)}>
<TextField />
</FormField>
<FormField name="endStep" label={formatMessage(labels.endStep)}>
<TextField />
</FormField>
<FormButtons>
<Button onPress={onClose} isDisabled={isPending}>
{formatMessage(labels.cancel)}
</Button>
<FormSubmitButton>{formatMessage(labels.save)}</FormSubmitButton>
</FormButtons>
</Form>
);
}

View file

@ -0,0 +1,38 @@
'use client';
import { Grid, Loading } from '@umami/react-zen';
import { SectionHeader } from '@/components/common/SectionHeader';
import { Journey } from './Journey';
import { JourneyAddButton } from './JourneyAddButton';
import { WebsiteControls } from '../WebsiteControls';
import { useDateRange, useReportsQuery } from '@/components/hooks';
import { LoadingPanel } from '@/components/common/LoadingPanel';
import { Panel } from '@/components/common/Panel';
export function JourneysPage({ websiteId }: { websiteId: string }) {
const { result } = useReportsQuery({ websiteId, type: 'journey' });
const {
dateRange: { startDate, endDate },
} = useDateRange(websiteId);
if (!result) {
return <Loading position="page" />;
}
return (
<>
<WebsiteControls websiteId={websiteId} />
<LoadingPanel isEmpty={!result?.data} isLoading={!result}>
<SectionHeader>
<JourneyAddButton websiteId={websiteId} />
</SectionHeader>
<Grid columns="1fr 1fr" gap>
{result?.data?.map((report: any) => (
<Panel key={report.id}>
<Journey {...report} reportId={report.id} startDate={startDate} endDate={endDate} />
</Panel>
))}
</Grid>
</LoadingPanel>
</>
);
}

View file

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