UI for new funnel report.

This commit is contained in:
Mike Cao 2024-04-02 23:30:12 -07:00
parent 30a1cdd53c
commit cfe7089916
13 changed files with 179 additions and 125 deletions

View file

@ -13,7 +13,7 @@ export function Report({
className,
}: {
reportId: string;
defaultParameters: { [key: string]: any };
defaultParameters: { type: string; parameters: { [key: string]: any } };
children: ReactNode;
className?: string;
}) {

View file

@ -51,7 +51,7 @@
align-items: center;
justify-content: flex-end;
background: var(--base900);
height: 50px;
height: 30px;
border-radius: 5px;
overflow: hidden;
position: relative;
@ -61,19 +61,12 @@
color: var(--base700);
}
.value {
color: var(--base50);
margin-inline-end: 20px;
}
.track {
background-color: var(--base100);
border-radius: 5px;
}
.info {
display: flex;
justify-content: space-between;
text-transform: lowercase;
}
@ -82,3 +75,24 @@
border-radius: 4px;
border: 1px solid var(--base300);
}
.metric {
color: var(--base700);
display: flex;
justify-content: space-between;
gap: 10px;
font-size: 24px;
margin: 10px 0;
text-transform: lowercase;
}
.visitors {
color: var(--base900);
font-weight: 900;
margin-right: 10px;
}
.percent {
font-weight: 700;
align-self: flex-end;
}

View file

@ -2,8 +2,8 @@ import { useContext } from 'react';
import classNames from 'classnames';
import { useMessages } from 'components/hooks';
import { ReportContext } from '../[reportId]/Report';
import styles from './FunnelChart.module.css';
import { formatLongNumber } from 'lib/format';
import styles from './FunnelChart.module.css';
export interface FunnelChartProps {
className?: string;
@ -18,35 +18,33 @@ export function FunnelChart({ className }: FunnelChartProps) {
return (
<div className={classNames(styles.chart, className)}>
{data?.map(({ url, visitors, dropped, dropoff, remaining }, index: number) => {
{data?.map(({ type, value, visitors, dropped, dropoff, remaining }, index: number) => {
return (
<div key={url} className={styles.step}>
<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(labels.viewedPage)}:</span>
<span className={styles.item}>{url}</span>
<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(visitors)}</span>
{formatMessage(labels.visitors)}
</div>
<div className={styles.percent}>{(remaining * 100).toFixed(2)}%</div>
</div>
<div className={styles.track}>
<div className={styles.bar} style={{ width: `${remaining * 100}%` }}>
<span className={styles.value}>
{remaining > 0.1 && `${(remaining * 100).toFixed(2)}%`}
</span>
</div>
<div className={styles.bar} style={{ width: `${remaining * 100}%` }}></div>
</div>
<div className={styles.info}>
<div>
<b>{formatLongNumber(visitors)}</b>
<span> {formatMessage(labels.visitors)}</span>
<span> ({(remaining * 100).toFixed(2)}%)</span>
{dropoff > 0 && (
<div className={styles.info}>
<b>{formatLongNumber(dropped)}</b> {formatMessage(labels.visitorsDroppedOff)} (
{(dropoff * 100).toFixed(2)}%)
</div>
{dropoff > 0 && (
<div>
<b>{formatLongNumber(dropped)}</b> {formatMessage(labels.visitorsDroppedOff)} (
{(dropoff * 100).toFixed(2)}%)
</div>
)}
</div>
)}
</div>
</div>
);

View file

@ -0,0 +1,9 @@
.item {
display: flex;
align-items: center;
gap: 10px;
}
.type {
color: var(--base700);
}

View file

@ -10,48 +10,54 @@ import {
Popup,
SubmitButton,
TextField,
Button,
} from 'react-basics';
import Icons from 'components/icons';
import UrlAddForm from './UrlAddForm';
import FunnelStepAddForm from './FunnelStepAddForm';
import { ReportContext } from '../[reportId]/Report';
import BaseParameters from '../[reportId]/BaseParameters';
import ParameterList from '../[reportId]/ParameterList';
import PopupForm from '../[reportId]/PopupForm';
import styles from './FunnelParameters.module.css';
export function FunnelParameters() {
const { report, runReport, updateReport, isRunning } = useContext(ReportContext);
const { formatMessage, labels } = useMessages();
const { id, parameters } = report || {};
const { websiteId, dateRange, urls } = parameters || {};
const queryDisabled = !websiteId || !dateRange || urls?.length < 2;
const { websiteId, dateRange, steps } = parameters || {};
const queryDisabled = !websiteId || !dateRange || steps?.length < 2;
const handleSubmit = (data: any, e: any) => {
e.stopPropagation();
e.preventDefault();
if (!queryDisabled) {
runReport(data);
}
};
const handleAddUrl = (url: string) => {
updateReport({ parameters: { urls: parameters.urls.concat(url) } });
const handleAddStep = (step: { type: string; value: string }) => {
updateReport({ parameters: { steps: parameters.steps.concat(step) } });
};
const handleRemoveUrl = (url: string) => {
const urls = [...parameters.urls];
updateReport({ parameters: { urls: urls.filter(n => n !== url) } });
const handleRemoveStep = (index: number) => {
const steps = [...parameters.steps];
delete steps[index];
updateReport({ parameters: { steps: steps.filter(n => n) } });
};
const AddUrlButton = () => {
const AddStepButton = () => {
return (
<PopupTrigger>
<Icon>
<Icons.Plus />
</Icon>
<Popup position="right" alignment="start">
<Button>
<Icon>
<Icons.Plus />
</Icon>
</Button>
<Popup alignment="start">
<PopupForm>
<UrlAddForm onAdd={handleAddUrl} />
<FunnelStepAddForm onAdd={handleAddStep} />
</PopupForm>
</Popup>
</PopupTrigger>
@ -69,12 +75,17 @@ export function FunnelParameters() {
<TextField autoComplete="off" />
</FormInput>
</FormRow>
<FormRow label={formatMessage(labels.urls)} action={<AddUrlButton />}>
<FormRow label={formatMessage(labels.steps)} action={<AddStepButton />}>
<ParameterList>
{urls.map(url => {
{steps.map((step: { type: string; value: string }, index: number) => {
return (
<ParameterList.Item key={url} onRemove={() => handleRemoveUrl(url)}>
{url}
<ParameterList.Item key={index} onRemove={() => handleRemoveStep(index)}>
<div className={styles.item}>
<div className={styles.type}>
<Icon>{step.type === 'url' ? <Icons.Eye /> : <Icons.Bolt />}</Icon>
</div>
<div>{step.value}</div>
</div>
</ParameterList.Item>
);
})}

View file

@ -9,7 +9,7 @@ import { REPORT_TYPES } from 'lib/constants';
const defaultParameters = {
type: REPORT_TYPES.funnel,
parameters: { window: 60, urls: [] },
parameters: { window: 60, steps: [] },
};
export default function FunnelReport({ reportId }: { reportId?: string }) {

View file

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

View file

@ -0,0 +1,71 @@
import { useState } from 'react';
import { useMessages } from 'components/hooks';
import { Button, FormRow, TextField, Flexbox, Dropdown, Item } from 'react-basics';
import styles from './FunnelStepAddForm.module.css';
export interface UrlAddFormProps {
defaultValue?: string;
onAdd?: (step: { type: string; value: string }) => void;
}
export function FunnelStepAddForm({ defaultValue = '', onAdd }: UrlAddFormProps) {
const [type, setType] = useState('url');
const [value, setValue] = useState(defaultValue);
const { formatMessage, labels } = useMessages();
const items = [
{ label: formatMessage(labels.url), value: 'url' },
{ label: formatMessage(labels.event), value: 'event' },
];
const isDisabled = !type || !value;
const handleSave = () => {
onAdd({ type, value });
setValue('');
};
const handleChange = e => {
setValue(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 (
<FormRow label={formatMessage(labels.addStep)}>
<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={handleChange}
autoFocus={true}
autoComplete="off"
onKeyDown={handleKeyDown}
/>
<Button variant="primary" onClick={handleSave} disabled={isDisabled}>
{formatMessage(labels.add)}
</Button>
</Flexbox>
</FormRow>
);
}
export default FunnelStepAddForm;

View file

@ -1,14 +0,0 @@
.form {
position: absolute;
background: var(--base50);
width: 300px;
padding: 30px;
margin-top: 10px;
border: 1px solid var(--base400);
border-radius: var(--border-radius);
box-shadow: 0 0 0 5px rgba(0, 0, 0, 0.1);
}
.input {
width: 100%;
}

View file

@ -1,52 +0,0 @@
import { useState } from 'react';
import { useMessages } from 'components/hooks';
import { Button, Form, FormRow, TextField, Flexbox } from 'react-basics';
import styles from './UrlAddForm.module.css';
export interface UrlAddFormProps {
defaultValue?: string;
onAdd?: (url: string) => void;
}
export function UrlAddForm({ defaultValue = '', onAdd }: UrlAddFormProps) {
const [url, setUrl] = useState(defaultValue);
const { formatMessage, labels } = useMessages();
const handleSave = () => {
onAdd(url);
setUrl('');
};
const handleChange = e => {
setUrl(e.target.value);
};
const handleKeyDown = e => {
if (e.key === 'Enter') {
e.stopPropagation();
handleSave();
}
};
return (
<Form>
<FormRow label={formatMessage(labels.url)}>
<Flexbox gap={10}>
<TextField
className={styles.input}
value={url}
onChange={handleChange}
autoFocus={true}
autoComplete="off"
onKeyDown={handleKeyDown}
/>
<Button variant="primary" onClick={handleSave}>
{formatMessage(labels.add)}
</Button>
</Flexbox>
</FormRow>
</Form>
);
}
export default UrlAddForm;