Goals report CH

This commit is contained in:
Brian Cao 2024-05-05 22:15:47 -07:00
parent 1dda711401
commit 60e7257656
20 changed files with 758 additions and 4 deletions

View file

@ -4,6 +4,7 @@ import EventDataReport from '../event-data/EventDataReport';
import InsightsReport from '../insights/InsightsReport';
import RetentionReport from '../retention/RetentionReport';
import UTMReport from '../utm/UTMReport';
import GoalReport from '../goals/GoalsReport';
import { useReport } from 'components/hooks';
const reports = {
@ -12,6 +13,7 @@ const reports = {
insights: InsightsReport,
retention: RetentionReport,
utm: UTMReport,
goals: GoalReport,
};
export default function ReportPage({ reportId }: { reportId: string }) {

View file

@ -37,6 +37,12 @@ export function ReportTemplates({ showHeader = true }: { showHeader?: boolean })
url: renderTeamUrl('/reports/utm'),
icon: <Tag />,
},
{
title: formatMessage(labels.goals),
description: formatMessage(labels.goalsDescription),
url: renderTeamUrl('/reports/goals'),
icon: <Tag />,
},
];
return (

View file

@ -0,0 +1,7 @@
.dropdown {
width: 140px;
}
.input {
width: 200px;
}

View file

@ -0,0 +1,95 @@
import { useMessages } from 'components/hooks';
import { useState } from 'react';
import { Button, Dropdown, Flexbox, FormRow, Item, TextField } from 'react-basics';
import styles from './GoalsAddForm.module.css';
export function GoalsAddForm({
type: defaultType = 'url',
value: defaultValue = '',
goal: defaultGoal = 10,
onChange,
}: {
type?: string;
value?: string;
goal?: number;
onChange?: (step: { type: string; value: string; goal: number }) => void;
}) {
const [type, setType] = useState(defaultType);
const [value, setValue] = useState(defaultValue);
const [goal, setGoal] = useState(defaultGoal);
const { formatMessage, labels } = useMessages();
const items = [
{ label: formatMessage(labels.url), value: 'url' },
{ label: formatMessage(labels.event), value: 'event' },
];
const isDisabled = !type || !value;
const handleSave = () => {
onChange({ type, value, goal });
setValue('');
setGoal(10);
};
const handleChange = (e, set) => {
set(e.target.value);
};
const handleKeyDown = e => {
if (e.key === 'Enter') {
e.stopPropagation();
handleSave();
}
};
const renderTypeValue = (value: any) => {
return items.find(item => item.value === value)?.label;
};
return (
<Flexbox direction="column" gap={10}>
<FormRow label={formatMessage(defaultValue ? labels.update : labels.add)}>
<Flexbox gap={10}>
<Dropdown
className={styles.dropdown}
items={items}
value={type}
renderValue={renderTypeValue}
onChange={(value: any) => setType(value)}
>
{({ value, label }) => {
return <Item key={value}>{label}</Item>;
}}
</Dropdown>
<TextField
className={styles.input}
value={value}
onChange={e => handleChange(e, setValue)}
autoFocus={true}
autoComplete="off"
onKeyDown={handleKeyDown}
/>
</Flexbox>
</FormRow>
<FormRow label={formatMessage(labels.goal)}>
<Flexbox gap={10}>
<TextField
className={styles.input}
value={goal?.toString()}
onChange={e => handleChange(e, setGoal)}
autoFocus={true}
autoComplete="off"
onKeyDown={handleKeyDown}
/>
.
</Flexbox>
</FormRow>
<FormRow>
<Button variant="primary" onClick={handleSave} disabled={isDisabled}>
{formatMessage(defaultValue ? labels.update : labels.add)}
</Button>
</FormRow>
</Flexbox>
);
}
export default GoalsAddForm;

View file

@ -0,0 +1,87 @@
.chart {
display: grid;
}
.num {
display: flex;
align-items: center;
justify-content: center;
border-radius: 100%;
width: 50px;
height: 50px;
font-size: 16px;
font-weight: 700;
color: var(--base800);
background: var(--base100);
z-index: 1;
}
.step {
display: grid;
grid-template-columns: max-content 1fr;
column-gap: 30px;
position: relative;
padding-bottom: 60px;
}
.card {
display: grid;
gap: 20px;
margin-top: 14px;
}
.header {
display: flex;
flex-direction: column;
gap: 20px;
}
.bar {
display: flex;
align-items: center;
justify-content: flex-end;
background: var(--base900);
height: 30px;
border-radius: 5px;
overflow: hidden;
position: relative;
}
.label {
color: var(--base600);
font-weight: 700;
text-transform: uppercase;
}
.track {
background-color: var(--base100);
border-radius: 5px;
}
.item {
font-size: 20px;
color: var(--base900);
font-weight: 700;
}
.metric {
color: var(--base700);
display: flex;
justify-content: space-between;
gap: 10px;
margin: 10px 0;
text-transform: lowercase;
}
.visitors {
color: var(--base900);
font-size: 24px;
font-weight: 900;
margin-right: 10px;
}
.percent {
font-size: 20px;
font-weight: 700;
align-self: flex-end;
}

View file

@ -0,0 +1,49 @@
import { useContext } from 'react';
import classNames from 'classnames';
import { useMessages } from 'components/hooks';
import { ReportContext } from '../[reportId]/Report';
import { formatLongNumber } from 'lib/format';
import styles from './GoalsChart.module.css';
export function GoalsChart({ className }: { className?: string; isLoading?: boolean }) {
const { report } = useContext(ReportContext);
const { formatMessage, labels } = useMessages();
const { data } = report || {};
return (
<div className={classNames(styles.chart, className)}>
{data?.map(({ type, value, goal, result }, index: number) => {
return (
<div key={index} className={styles.step}>
<div className={styles.num}>{index + 1}</div>
<div className={styles.card}>
<div className={styles.header}>
<span className={styles.label}>
{formatMessage(type === 'url' ? labels.viewedPage : labels.triggeredEvent)}
</span>
<span className={styles.item}>{value}</span>
</div>
<div className={styles.metric}>
<div>
<span className={styles.visitors}>{formatLongNumber(result)}</span>
{formatMessage(labels.visitors)}
</div>
<div>
<span className={styles.visitors}>{formatLongNumber(goal)}</span>
{formatMessage(labels.goal)}
</div>
<div className={styles.percent}>{(result / goal).toFixed(2)}%</div>
</div>
<div className={styles.track}>
<div className={styles.bar} style={{ width: `${result / goal}%` }}></div>
</div>
</div>
</div>
);
})}
</div>
);
}
export default GoalsChart;

View file

@ -0,0 +1,26 @@
.item {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
}
.type {
color: var(--base700);
}
.value {
display: flex;
align-self: center;
gap: 20px;
}
.goal {
color: var(--blue900);
background-color: var(--blue100);
font-size: 12px;
font-weight: 900;
padding: 2px 8px;
border-radius: 5px;
white-space: nowrap;
}

View file

@ -0,0 +1,123 @@
import { useMessages } from 'components/hooks';
import Icons from 'components/icons';
import { formatNumber } from 'lib/format';
import { useContext } from 'react';
import {
Button,
Form,
FormButtons,
FormRow,
Icon,
Popup,
PopupTrigger,
SubmitButton,
} from 'react-basics';
import BaseParameters from '../[reportId]/BaseParameters';
import ParameterList from '../[reportId]/ParameterList';
import PopupForm from '../[reportId]/PopupForm';
import { ReportContext } from '../[reportId]/Report';
import GoalsAddForm from './GoalsAddForm';
import styles from './GoalsParameters.module.css';
export function GoalsParameters() {
const { report, runReport, updateReport, isRunning } = useContext(ReportContext);
const { formatMessage, labels } = useMessages();
const { id, parameters } = report || {};
const { websiteId, dateRange, goals } = parameters || {};
const queryDisabled = !websiteId || !dateRange || goals?.length < 1;
const handleSubmit = (data: any, e: any) => {
e.stopPropagation();
e.preventDefault();
if (!queryDisabled) {
runReport(data);
}
};
const handleAddGoals = (goal: { type: string; value: string }) => {
updateReport({ parameters: { goals: parameters.goals.concat(goal) } });
};
const handleUpdateGoals = (
close: () => void,
index: number,
goal: { type: string; value: string },
) => {
const goals = [...parameters.goals];
goals[index] = goal;
updateReport({ parameters: { goals } });
close();
};
const handleRemoveGoals = (index: number) => {
const goals = [...parameters.goals];
delete goals[index];
updateReport({ parameters: { goals: goals.filter(n => n) } });
};
const AddGoalsButton = () => {
return (
<PopupTrigger>
<Button>
<Icon>
<Icons.Plus />
</Icon>
</Button>
<Popup alignment="start">
<PopupForm>
<GoalsAddForm onChange={handleAddGoals} />
</PopupForm>
</Popup>
</PopupTrigger>
);
};
return (
<Form values={parameters} onSubmit={handleSubmit} preventSubmit={true}>
<BaseParameters allowWebsiteSelect={!id} />
<FormRow label={formatMessage(labels.goals)} action={<AddGoalsButton />}>
<ParameterList>
{goals.map((goal: { type: string; value: string; goal: number }, index: number) => {
return (
<PopupTrigger key={index}>
<ParameterList.Item
className={styles.item}
onRemove={() => handleRemoveGoals(index)}
>
<div className={styles.value}>
<div className={styles.type}>
<Icon>{goal.type === 'url' ? <Icons.Eye /> : <Icons.Bolt />}</Icon>
</div>
<div>{goal.value}</div>
<div className={styles.goal}>{formatNumber(goal.goal)}</div>
</div>
</ParameterList.Item>
<Popup alignment="start">
{(close: () => void) => (
<PopupForm>
<GoalsAddForm
type={goal.type}
value={goal.value}
goal={goal.goal}
onChange={handleUpdateGoals.bind(null, close, index)}
/>
</PopupForm>
)}
</Popup>
</PopupTrigger>
);
})}
</ParameterList>
</FormRow>
<FormButtons>
<SubmitButton variant="primary" disabled={queryDisabled} isLoading={isRunning}>
{formatMessage(labels.runQuery)}
</SubmitButton>
</FormButtons>
</Form>
);
}
export default GoalsParameters;

View file

@ -0,0 +1,10 @@
.filters {
display: flex;
flex-direction: column;
justify-content: space-between;
border: 1px solid var(--base400);
border-radius: var(--border-radius);
line-height: 32px;
padding: 10px;
overflow: hidden;
}

View file

@ -0,0 +1,27 @@
import GoalsChart from './GoalsChart';
import GoalsParameters from './GoalsParameters';
import Report from '../[reportId]/Report';
import ReportHeader from '../[reportId]/ReportHeader';
import ReportMenu from '../[reportId]/ReportMenu';
import ReportBody from '../[reportId]/ReportBody';
import Goals from 'assets/funnel.svg';
import { REPORT_TYPES } from 'lib/constants';
const defaultParameters = {
type: REPORT_TYPES.goals,
parameters: { goals: [] },
};
export default function GoalsReport({ reportId }: { reportId?: string }) {
return (
<Report reportId={reportId} defaultParameters={defaultParameters}>
<ReportHeader icon={<Goals />} />
<ReportMenu>
<GoalsParameters />
</ReportMenu>
<ReportBody>
<GoalsChart />
</ReportBody>
</Report>
);
}

View file

@ -0,0 +1,6 @@
'use client';
import GoalReport from './GoalsReport';
export default function GoalReportPage() {
return <GoalReport />;
}

View file

@ -0,0 +1,10 @@
import GoalsReportPage from './GoalsReportPage';
import { Metadata } from 'next';
export default function () {
return <GoalsReportPage />;
}
export const metadata: Metadata = {
title: 'Goals Report',
};

View file

@ -6,5 +6,5 @@ export default function () {
}
export const metadata: Metadata = {
title: 'UTM Report',
title: 'Goals Report',
};