Retention report UI updates.

This commit is contained in:
Mike Cao 2023-08-17 03:21:20 -07:00
parent 9b8fa08d82
commit 2c8996b68f
12 changed files with 110 additions and 93 deletions

View File

@ -1,5 +1,13 @@
import { useRef, useState } from 'react'; import { useRef } from 'react';
import { Text, Icon, CalendarMonthSelect, CalendarYearSelect, Button } from 'react-basics'; import {
Text,
Icon,
CalendarMonthSelect,
CalendarYearSelect,
Button,
PopupTrigger,
Popup,
} from 'react-basics';
import { startOfMonth, endOfMonth } from 'date-fns'; import { startOfMonth, endOfMonth } from 'date-fns';
import Icons from 'components/icons'; import Icons from 'components/icons';
import { useLocale } from 'hooks'; import { useLocale } from 'hooks';
@ -7,43 +15,50 @@ import { formatDate } from 'lib/date';
import { getDateLocale } from 'lib/lang'; import { getDateLocale } from 'lib/lang';
import styles from './MonthSelect.module.css'; import styles from './MonthSelect.module.css';
const MONTH = 'month';
const YEAR = 'year';
export function MonthSelect({ date = new Date(), onChange }) { export function MonthSelect({ date = new Date(), onChange }) {
const { locale } = useLocale(); const { locale } = useLocale();
const [select, setSelect] = useState(null);
const month = formatDate(date, 'MMMM', locale); const month = formatDate(date, 'MMMM', locale);
const year = date.getFullYear(); const year = date.getFullYear();
const ref = useRef(); const ref = useRef();
const handleSelect = value => {
setSelect(state => (state !== value ? value : null));
};
const handleChange = date => { const handleChange = date => {
onChange(`range:${startOfMonth(date).getTime()}:${endOfMonth(date).getTime()}`); onChange(`range:${startOfMonth(date).getTime()}:${endOfMonth(date).getTime()}`);
setSelect(null);
}; };
return ( return (
<> <>
<div ref={ref} className={styles.container}> <div ref={ref} className={styles.container}>
<Button className={styles.input} variant="quiet" onClick={() => handleSelect(MONTH)}> <PopupTrigger>
<Text>{month}</Text> <Button className={styles.input} variant="quiet">
<Icon size="sm">{select === MONTH ? <Icons.Close /> : <Icons.ChevronDown />}</Icon> <Text>{month}</Text>
</Button> <Icon size="sm">
<Button className={styles.input} variant="quiet" onClick={() => handleSelect(YEAR)}> <Icons.ChevronDown />
<Text>{year}</Text> </Icon>
<Icon size="sm">{select === YEAR ? <Icons.Close /> : <Icons.ChevronDown />}</Icon> </Button>
</Button> <Popup className={styles.popup} alignment="start">
<CalendarMonthSelect
date={date}
locale={getDateLocale(locale)}
onSelect={handleChange}
/>
</Popup>
</PopupTrigger>
<PopupTrigger>
<Button className={styles.input} variant="quiet">
<Text>{year}</Text>
<Icon size="sm">
<Icons.ChevronDown />
</Icon>
</Button>
<Popup className={styles.popup} alignment="start">
<CalendarYearSelect
date={date}
locale={getDateLocale(locale)}
onSelect={handleChange}
/>
</Popup>
</PopupTrigger>
</div> </div>
{select === MONTH && (
<CalendarMonthSelect date={date} locale={getDateLocale(locale)} onSelect={handleChange} />
)}
{select === YEAR && (
<CalendarYearSelect date={date} locale={getDateLocale(locale)} onSelect={handleChange} />
)}
</> </>
); );
} }

View File

@ -2,6 +2,8 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border: 1px solid var(--base400);
border-radius: var(--border-radius);
} }
.input { .input {
@ -10,3 +12,11 @@
gap: 10px; gap: 10px;
cursor: pointer; cursor: pointer;
} }
.popup {
border: 1px solid var(--base400);
background: var(--base50);
border-radius: var(--border-radius);
padding: 20px;
margin-top: 5px;
}

View File

@ -1,29 +1,11 @@
import { createPortal } from 'react-dom';
import { useDocumentClick, useKeyDown } from 'react-basics';
import classNames from 'classnames'; import classNames from 'classnames';
import styles from './PopupForm.module.css'; import styles from './PopupForm.module.css';
export function PopupForm({ element, className, children, onClose }) { export function PopupForm({ className, style, children }) {
const { right, top } = element.getBoundingClientRect(); return (
const style = { position: 'absolute', left: right, top }; <div className={classNames(styles.form, className)} style={style}>
useKeyDown('Escape', onClose);
useDocumentClick(e => {
if (e.target !== element && !element?.parentElement?.contains(e.target)) {
onClose();
}
});
const handleClick = e => {
e.stopPropagation();
};
return createPortal(
<div className={classNames(styles.form, className)} style={style} onClick={handleClick}>
{children} {children}
</div>, </div>
document.body,
); );
} }

View File

@ -3,8 +3,8 @@
background: var(--base50); background: var(--base50);
min-width: 300px; min-width: 300px;
padding: 20px; padding: 20px;
margin-left: 30px;
border: 1px solid var(--base400); border: 1px solid var(--base400);
border-radius: var(--border-radius); border-radius: var(--border-radius);
box-shadow: 0 0 0 5px rgba(0, 0, 0, 0.1); box-shadow: 0 0 0 5px rgba(0, 0, 0, 0.1);
z-index: 1000;
} }

View File

@ -80,10 +80,10 @@ export function InsightsParameters() {
<Icons.Plus /> <Icons.Plus />
</Icon> </Icon>
</TooltipPopup> </TooltipPopup>
<Popup position="bottom" alignment="start"> <Popup position="bottom" alignment="start" className={styles.popup}>
{(close, element) => { {close => {
return ( return (
<PopupForm element={element} onClose={close}> <PopupForm onClose={close}>
{id === 'fields' && ( {id === 'fields' && (
<FieldSelectForm <FieldSelectForm
items={fieldOptions} items={fieldOptions}
@ -114,7 +114,7 @@ export function InsightsParameters() {
return ( return (
<FormRow key={label} label={label} action={<AddButton id={id} onAdd={handleAdd} />}> <FormRow key={label} label={label} action={<AddButton id={id} onAdd={handleAdd} />}>
<ParameterList items={parameterData[id]} onRemove={index => handleRemove(id, index)}> <ParameterList items={parameterData[id]} onRemove={index => handleRemove(id, index)}>
{({ name, filter, value, label }) => { {({ name, filter, value }) => {
return ( return (
<div className={styles.parameter}> <div className={styles.parameter}>
{id === 'fields' && ( {id === 'fields' && (

View File

@ -10,3 +10,8 @@
.op { .op {
font-weight: bold; font-weight: bold;
} }
.popup {
margin-top: -10px;
margin-left: 30px;
}

View File

@ -31,7 +31,7 @@ export function RetentionParameters() {
return ( return (
<Form ref={ref} values={parameters} onSubmit={handleSubmit} preventSubmit={true}> <Form ref={ref} values={parameters} onSubmit={handleSubmit} preventSubmit={true}>
<BaseParameters showDateSelect={false} /> <BaseParameters showDateSelect={false} />
<FormRow label={formatMessage(labels.dateRange)}> <FormRow label={formatMessage(labels.date)}>
<MonthSelect date={startDate} onChange={handleDateChange} /> <MonthSelect date={startDate} onChange={handleDateChange} />
</FormRow> </FormRow>
<FormButtons> <FormButtons>

View File

@ -6,10 +6,16 @@ import ReportMenu from '../ReportMenu';
import ReportBody from '../ReportBody'; import ReportBody from '../ReportBody';
import Magnet from 'assets/magnet.svg'; import Magnet from 'assets/magnet.svg';
import { REPORT_TYPES } from 'lib/constants'; import { REPORT_TYPES } from 'lib/constants';
import { parseDateRange } from 'lib/date';
import { endOfMonth, startOfMonth } from 'date-fns';
const defaultParameters = { const defaultParameters = {
type: REPORT_TYPES.retention, type: REPORT_TYPES.retention,
parameters: {}, parameters: {
dateRange: parseDateRange(
`range:${startOfMonth(new Date()).getTime()}:${endOfMonth(new Date()).getTime()}`,
),
},
}; };
export default function RetentionReport({ reportId }) { export default function RetentionReport({ reportId }) {

View File

@ -1,5 +1,4 @@
import { useContext } from 'react'; import { useContext } from 'react';
import { GridTable, GridColumn } from 'react-basics';
import classNames from 'classnames'; import classNames from 'classnames';
import { ReportContext } from '../Report'; import { ReportContext } from '../Report';
import EmptyPlaceholder from 'components/common/EmptyPlaceholder'; import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
@ -16,14 +15,26 @@ export function RetentionTable() {
return <EmptyPlaceholder />; return <EmptyPlaceholder />;
} }
const rows = data.reduce((arr, { date, visitors }) => { const days = [1, 2, 3, 4, 5, 6, 7, 14, 21, 28];
if (!arr.find(a => a.date === date)) {
return arr.concat({ date, visitors }); const rows = data.reduce((arr, row) => {
const { date, visitors, day } = row;
if (day === 0) {
return arr.concat({
date,
visitors,
records: days
.reduce((arr, day) => {
arr[day] = data.find(x => x.date === date && x.day === day);
return arr;
}, [])
.filter(n => n),
});
} }
return arr; return arr;
}, []); }, []);
const days = [1, 2, 3, 4, 5, 6, 7, 14, 21, 30]; const totalDays = rows.length;
return ( return (
<> <>
@ -37,15 +48,22 @@ export function RetentionTable() {
</div> </div>
))} ))}
</div> </div>
{rows.map(({ date, visitors }, i) => { {rows.map(({ date, visitors, records }, rowIndex) => {
return ( return (
<div key={i} className={styles.row}> <div key={rowIndex} className={styles.row}>
<div className={styles.date}>{formatDate(`${date} 00:00:00`, 'PP')}</div> <div className={styles.date}>{formatDate(`${date} 00:00:00`, 'PP')}</div>
<div className={styles.visitors}>{visitors}</div> <div className={styles.visitors}>{visitors}</div>
{days.map((n, day) => { {days.map(day => {
if (totalDays - rowIndex < day) {
return null;
}
const percentage = records[day]?.percentage;
return ( return (
<div key={day} className={styles.cell}> <div
{data.find(row => row.date === date && row.day === day)?.percentage.toFixed(2)} key={day}
className={classNames(styles.cell, { [styles.empty]: !percentage })}
>
{percentage ? `${percentage.toFixed(2)}%` : ''}
</div> </div>
); );
})} })}
@ -53,31 +71,8 @@ export function RetentionTable() {
); );
})} })}
</div> </div>
<DataTable data={data} />
</> </>
); );
} }
function DataTable({ data }) {
return (
<GridTable data={data || []}>
<GridColumn name="date" label={'Date'}>
{row => row.date}
</GridColumn>
<GridColumn name="day" label={'Day'}>
{row => row.day}
</GridColumn>
<GridColumn name="visitors" label={'Visitors'}>
{row => row.visitors}
</GridColumn>
<GridColumn name="returnVisitors" label={'Return Visitors'}>
{row => row.returnVisitors}
</GridColumn>
<GridColumn name="percentage" label={'Percentage'}>
{row => row.percentage}
</GridColumn>
</GridTable>
);
}
export default RetentionTable; export default RetentionTable;

View File

@ -20,7 +20,7 @@
justify-content: center; justify-content: center;
width: 60px; width: 60px;
height: 60px; height: 60px;
background: var(--blue100); background: var(--blue200);
border-radius: var(--border-radius); border-radius: var(--border-radius);
} }
@ -46,3 +46,7 @@
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
font-weight: 400; font-weight: 400;
} }
.empty {
background: var(--blue100);
}

View File

@ -94,7 +94,7 @@
"node-fetch": "^3.2.8", "node-fetch": "^3.2.8",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"react": "^18.2.0", "react": "^18.2.0",
"react-basics": "^0.94.0", "react-basics": "^0.96.0",
"react-beautiful-dnd": "^13.1.0", "react-beautiful-dnd": "^13.1.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-error-boundary": "^4.0.4", "react-error-boundary": "^4.0.4",

View File

@ -7557,10 +7557,10 @@ rc@^1.2.7:
minimist "^1.2.0" minimist "^1.2.0"
strip-json-comments "~2.0.1" strip-json-comments "~2.0.1"
react-basics@^0.94.0: react-basics@^0.96.0:
version "0.94.0" version "0.96.0"
resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.94.0.tgz#c15698148b959f40c6b451088f36f5735eb82815" resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.96.0.tgz#e5e72201abdccdda94b952ef605163ca11772d8f"
integrity sha512-OlUHWrGRctRGEm+yL9iWSC9HRnxZhlm3enP2iCKytVmt7LvaPtsK4RtZ27qp4irNvuzg79aqF+h5IFnG+Vi7WA== integrity sha512-WNAxP+0xBtUNgEXrL8aW6UQMmD6WoX9My0VW6uq+Q262DOPTU3zPtWl+9vvES4pF3tPJCFvmFAlK/Alw9+XKVQ==
dependencies: dependencies:
classnames "^2.3.1" classnames "^2.3.1"
date-fns "^2.29.3" date-fns "^2.29.3"