Metrics components refactoring. New event data page.

This commit is contained in:
Mike Cao 2023-07-10 04:35:19 -07:00
parent 4e6d24e932
commit c865f43b11
47 changed files with 756 additions and 672 deletions

View file

@ -1,114 +1,13 @@
import { useState } from 'react';
import { Loading } from 'react-basics';
import ErrorMessage from 'components/common/ErrorMessage';
import useApi from 'hooks/useApi';
import useDateRange from 'hooks/useDateRange';
import usePageQuery from 'hooks/usePageQuery';
import { formatShortTime, formatNumber, formatLongNumber } from 'lib/format';
import MetricCard from './MetricCard';
import useMessages from 'hooks/useMessages';
import styles from './MetricsBar.module.css';
export function MetricsBar({ websiteId }) {
const { formatMessage, labels } = useMessages();
const { get, useQuery } = useApi();
const [dateRange] = useDateRange(websiteId);
const { startDate, endDate, modified } = dateRange;
const [format, setFormat] = useState(true);
const {
query: { url, referrer, title, os, browser, device, country, region, city },
} = usePageQuery();
const { data, error, isLoading, isFetched } = useQuery(
[
'websites:stats',
{ websiteId, modified, url, referrer, title, os, browser, device, country, region, city },
],
() =>
get(`/websites/${websiteId}/stats`, {
startAt: +startDate,
endAt: +endDate,
url,
referrer,
title,
os,
browser,
device,
country,
region,
city,
}),
);
const formatFunc = format
? n => (n >= 0 ? formatLongNumber(n) : `-${formatLongNumber(Math.abs(n))}`)
: formatNumber;
function handleSetFormat() {
setFormat(state => !state);
}
const { pageviews, uniques, bounces, totaltime } = data || {};
const num = Math.min(data && uniques.value, data && bounces.value);
const diffs = data && {
pageviews: pageviews.value - pageviews.change,
uniques: uniques.value - uniques.change,
bounces: bounces.value - bounces.change,
totaltime: totaltime.value - totaltime.change,
};
export function MetricsBar({ children, onClick, isLoading, isFetched, error }) {
return (
<div className={styles.bar} onClick={handleSetFormat}>
<div className={styles.bar} onClick={onClick}>
{isLoading && !isFetched && <Loading icon="dots" />}
{error && <ErrorMessage />}
{data && !error && isFetched && (
<>
<MetricCard
className={styles.card}
label={formatMessage(labels.views)}
value={pageviews.value}
change={pageviews.change}
format={formatFunc}
/>
<MetricCard
className={styles.card}
label={formatMessage(labels.visitors)}
value={uniques.value}
change={uniques.change}
format={formatFunc}
/>
<MetricCard
className={styles.card}
label={formatMessage(labels.bounceRate)}
value={uniques.value ? (num / uniques.value) * 100 : 0}
change={
uniques.value && uniques.change
? (num / uniques.value) * 100 -
(Math.min(diffs.uniques, diffs.bounces) / diffs.uniques) * 100 || 0
: 0
}
format={n => Number(n).toFixed(0) + '%'}
reverseColors
/>
<MetricCard
className={styles.card}
label={formatMessage(labels.averageVisitTime)}
value={
totaltime.value && pageviews.value
? totaltime.value / (pageviews.value - bounces.value)
: 0
}
change={
totaltime.value && pageviews.value
? (diffs.totaltime / (diffs.pageviews - diffs.bounces) -
totaltime.value / (pageviews.value - bounces.value)) *
-1 || 0
: 0
}
format={n => `${n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`}
/>
</>
)}
{children}
</div>
);
}

View file

@ -1,5 +1,6 @@
.bar {
display: flex;
flex-direction: row;
cursor: pointer;
min-height: 110px;
gap: 20px;

View file

@ -1,132 +0,0 @@
import { useMemo } from 'react';
import { Button, Icon, Text, Row, Column } from 'react-basics';
import Link from 'next/link';
import classNames from 'classnames';
import PageviewsChart from './PageviewsChart';
import MetricsBar from './MetricsBar';
import WebsiteHeader from './WebsiteHeader';
import WebsiteDateFilter from 'components/input/WebsiteDateFilter';
import ErrorMessage from 'components/common/ErrorMessage';
import FilterTags from 'components/metrics/FilterTags';
import RefreshButton from 'components/input/RefreshButton';
import useApi from 'hooks/useApi';
import useDateRange from 'hooks/useDateRange';
import useTimezone from 'hooks/useTimezone';
import usePageQuery from 'hooks/usePageQuery';
import { getDateArray, getDateLength } from 'lib/date';
import Icons from 'components/icons';
import useSticky from 'hooks/useSticky';
import useMessages from 'hooks/useMessages';
import styles from './WebsiteChart.module.css';
import useLocale from 'hooks/useLocale';
export function WebsiteChart({
websiteId,
name,
domain,
stickyHeader = false,
showChart = true,
showDetailsButton = false,
onDataLoad = () => {},
}) {
const { formatMessage, labels } = useMessages();
const [dateRange] = useDateRange(websiteId);
const { startDate, endDate, unit, value, modified } = dateRange;
const [timezone] = useTimezone();
const {
query: { url, referrer, os, browser, device, country, region, city, title },
} = usePageQuery();
const { get, useQuery } = useApi();
const { ref, isSticky } = useSticky({ enabled: stickyHeader });
const { data, isLoading, error } = useQuery(
[
'websites:pageviews',
{ websiteId, modified, url, referrer, os, browser, device, country, region, city, title },
],
() =>
get(`/websites/${websiteId}/pageviews`, {
startAt: +startDate,
endAt: +endDate,
unit,
timezone,
url,
referrer,
os,
browser,
device,
country,
region,
city,
title,
}),
{ onSuccess: onDataLoad },
);
const chartData = useMemo(() => {
if (data) {
return {
pageviews: getDateArray(data.pageviews, startDate, endDate, unit),
sessions: getDateArray(data.sessions, startDate, endDate, unit),
};
}
return { pageviews: [], sessions: [] };
}, [data, modified]);
const { dir } = useLocale();
return (
<>
<WebsiteHeader websiteId={websiteId} name={name} domain={domain}>
{showDetailsButton && (
<Link href={`/websites/${websiteId}`}>
<Button variant="primary">
<Text>{formatMessage(labels.viewDetails)}</Text>
<Icon>
<Icon rotate={dir === 'rtl' ? 180 : 0}>
<Icons.ArrowRight />
</Icon>
</Icon>
</Button>
</Link>
)}
</WebsiteHeader>
<FilterTags
websiteId={websiteId}
params={{ url, referrer, os, browser, device, country, region, city, title }}
/>
<Row
ref={ref}
className={classNames(styles.header, {
[styles.sticky]: stickyHeader,
[styles.isSticky]: isSticky,
})}
>
<Column defaultSize={12} xl={8}>
<MetricsBar websiteId={websiteId} />
</Column>
<Column defaultSize={12} xl={4}>
<div className={styles.actions}>
<RefreshButton websiteId={websiteId} isLoading={isLoading} />
<WebsiteDateFilter websiteId={websiteId} value={value} className={styles.dropdown} />
</div>
</Column>
</Row>
<Row>
<Column className={styles.chart}>
{error && <ErrorMessage />}
{showChart && (
<PageviewsChart
websiteId={websiteId}
data={chartData}
unit={unit}
records={getDateLength(startDate, endDate, unit)}
loading={isLoading}
/>
)}
</Column>
</Row>
</>
);
}
export default WebsiteChart;

View file

@ -1,57 +0,0 @@
.container {
position: relative;
display: flex;
flex-direction: column;
align-self: stretch;
}
.chart {
position: relative;
overflow: hidden;
}
.title {
font-size: var(--font-size-lg);
line-height: 60px;
font-weight: 600;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
min-height: 90px;
margin-bottom: 20px;
background: var(--base50);
z-index: var(--z-index-above);
}
.actions {
display: flex;
align-items: center;
flex-direction: row;
justify-content: flex-end;
gap: 10px;
}
.dropdown {
min-width: 200px;
}
@media only screen and (min-width: 992px) {
.sticky {
position: sticky;
top: -1px;
}
.isSticky {
border-bottom: 1px solid var(--base300);
}
}
@media only screen and (max-width: 1200px) {
.actions {
margin-top: 40px;
}
}

View file

@ -1,42 +0,0 @@
import { Flexbox, Row, Column, Text, Button, Icon } from 'react-basics';
import Favicon from 'components/common/Favicon';
import ActiveUsers from './ActiveUsers';
import styles from './WebsiteHeader.module.css';
import { useMessages } from 'hooks';
import Icons from 'components/icons';
export function WebsiteHeader({ websiteId, name, domain, children }) {
const { formatMessage, labels } = useMessages();
const links = [
{ label: formatMessage(labels.overview), icon: <Icons.Overview /> },
{ label: formatMessage(labels.realtime), icon: <Icons.Clock /> },
{ label: formatMessage(labels.reports), icon: <Icons.Reports /> },
{ label: formatMessage(labels.eventData), icon: <Icons.Nodes /> },
];
return (
<Row className={styles.header} justifyContent="center">
<Column className={styles.title} variant="two">
<Favicon domain={domain} />
<Text>{name}</Text>
</Column>
<Column className={styles.actions} variant="two">
<ActiveUsers websiteId={websiteId} />
<Flexbox alignItems="center">
{links.map(({ label, icon }) => {
return (
<Button key={label} variant="quiet">
<Icon>{icon}</Icon>
<Text>{label}</Text>
</Button>
);
})}
</Flexbox>
{children}
</Column>
</Row>
);
}
export default WebsiteHeader;

View file

@ -1,25 +0,0 @@
.header {
display: flex;
flex-direction: row;
align-items: center;
}
.title {
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
font-size: 24px;
font-weight: 700;
overflow: hidden;
height: 100px;
}
.actions {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
gap: 30px;
min-height: 0;
}