feat: add previous period in website overview
This commit is contained in:
parent
835d7ff43d
commit
8ff5db80e2
@ -1,9 +1,11 @@
|
||||
import { Tag } from 'antd';
|
||||
import React from 'react';
|
||||
import { formatNumber } from '../../utils/common';
|
||||
import { useGlobalStateStore } from '../../store/global';
|
||||
|
||||
interface MetricCardProps {
|
||||
value?: number;
|
||||
prev?: number;
|
||||
change?: number;
|
||||
label: string;
|
||||
reverseColors?: boolean;
|
||||
@ -13,12 +15,16 @@ interface MetricCardProps {
|
||||
export const MetricCard: React.FC<MetricCardProps> = React.memo((props) => {
|
||||
const {
|
||||
value = 0,
|
||||
prev = 0,
|
||||
change = 0,
|
||||
label,
|
||||
reverseColors = false,
|
||||
format = formatNumber,
|
||||
hideComparison = false,
|
||||
} = props;
|
||||
const showPreviousPeriod = useGlobalStateStore(
|
||||
(state) => state.showPreviousPeriod
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-center min-w-[140px] min-h-[90px]">
|
||||
@ -26,14 +32,25 @@ export const MetricCard: React.FC<MetricCardProps> = React.memo((props) => {
|
||||
{format(value)}
|
||||
</div>
|
||||
<div className="flex items-center whitespace-nowrap font-bold">
|
||||
<span className="mr-2">{label}</span>
|
||||
{~~change !== 0 && !hideComparison && (
|
||||
<span className="mr-2 capitalize">{label}</span>
|
||||
{change !== 0 && !hideComparison && (
|
||||
<Tag color={change * (reverseColors ? -1 : 1) >= 0 ? 'green' : 'red'}>
|
||||
{change > 0 && '+'}
|
||||
{format(change)}
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showPreviousPeriod && (
|
||||
<div className="mt-2 lg:mt-4 opacity-60">
|
||||
<div className="flex items-center whitespace-nowrap font-bold text-4xl">
|
||||
{format(prev)}
|
||||
</div>
|
||||
<div className="flex items-center whitespace-nowrap font-bold">
|
||||
<span className="mr-2">Previous {label}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Button, message, Spin } from 'antd';
|
||||
import { Button, message, Spin, Switch } from 'antd';
|
||||
import React, { useMemo } from 'react';
|
||||
import { Column, ColumnConfig } from '@ant-design/charts';
|
||||
import { SyncOutlined } from '@ant-design/icons';
|
||||
@ -23,6 +23,7 @@ import { MonitorHealthBar } from '../monitor/MonitorHealthBar';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { AppRouterOutput, trpc } from '../../api/trpc';
|
||||
import { getUserTimezone } from '../../api/model/user';
|
||||
import { useGlobalStateStore } from '../../store/global';
|
||||
|
||||
export const WebsiteOverview: React.FC<{
|
||||
website: WebsiteInfo;
|
||||
@ -32,6 +33,9 @@ export const WebsiteOverview: React.FC<{
|
||||
const { website, showDateFilter = false, actions } = props;
|
||||
const { startDate, endDate, unit, refresh } = useGlobalRangeDate();
|
||||
const navigate = useNavigate();
|
||||
const showPreviousPeriod = useGlobalStateStore(
|
||||
(state) => state.showPreviousPeriod
|
||||
);
|
||||
|
||||
const {
|
||||
pageviews,
|
||||
@ -113,7 +117,19 @@ export const WebsiteOverview: React.FC<{
|
||||
<div className="flex mb-10 flex-wrap justify-between">
|
||||
<div className="flex-1">{stats && <MetricsBar stats={stats} />}</div>
|
||||
|
||||
<div className="flex items-center gap-2 justify-end w-full lg:w-1/3">
|
||||
<div className="flex items-center gap-2 justify-end flex-wrap w-full lg:w-1/3">
|
||||
<div className="mr-2">
|
||||
<Switch
|
||||
checked={showPreviousPeriod}
|
||||
onChange={(checked) =>
|
||||
useGlobalStateStore.setState({
|
||||
showPreviousPeriod: checked,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<span className="ml-1">Previous period</span>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="large"
|
||||
icon={<SyncOutlined />}
|
||||
@ -140,51 +156,51 @@ export const MetricsBar: React.FC<{
|
||||
stats: AppRouterOutput['website']['stats'];
|
||||
}> = React.memo((props) => {
|
||||
const { pageviews, uniques, bounces, totaltime } = props.stats || {};
|
||||
const num = Math.min(uniques.value, bounces.value);
|
||||
const diffs = {
|
||||
pageviews: pageviews.value - pageviews.change,
|
||||
uniques: uniques.value - uniques.change,
|
||||
bounces: bounces.value - bounces.change,
|
||||
totaltime: totaltime.value - totaltime.change,
|
||||
};
|
||||
const bouncesNum = Math.min(uniques.value, bounces.value) / uniques.value;
|
||||
const prevBouncesNum = Math.min(uniques.prev, bounces.prev) / uniques.prev;
|
||||
|
||||
return (
|
||||
<div className="flex gap-5 flex-wrap w-full">
|
||||
<MetricCard
|
||||
label="Views"
|
||||
label="views"
|
||||
value={pageviews.value}
|
||||
change={pageviews.change}
|
||||
prev={pageviews.prev}
|
||||
change={pageviews.value - pageviews.prev}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Visitors"
|
||||
label="visitors"
|
||||
value={uniques.value}
|
||||
change={uniques.change}
|
||||
prev={uniques.prev}
|
||||
change={uniques.value - uniques.prev}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Bounce rate"
|
||||
label="bounce rate"
|
||||
reverseColors={true}
|
||||
value={uniques.value ? (num / uniques.value) * 100 : 0}
|
||||
value={uniques.value ? bouncesNum * 100 : 0}
|
||||
prev={uniques.prev ? prevBouncesNum * 100 : 0}
|
||||
change={
|
||||
uniques.value && uniques.change
|
||||
? (num / uniques.value) * 100 -
|
||||
(Math.min(diffs.uniques, diffs.bounces) / diffs.uniques) *
|
||||
100 || 0
|
||||
uniques.value && uniques.prev
|
||||
? bouncesNum * 100 - prevBouncesNum * 100 || 0
|
||||
: 0
|
||||
}
|
||||
format={(n) => formatNumber(n) + '%'}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Average visit time"
|
||||
label="average visit time"
|
||||
value={
|
||||
totaltime.value && pageviews.value
|
||||
? totaltime.value / (pageviews.value - bounces.value)
|
||||
: 0
|
||||
}
|
||||
prev={
|
||||
totaltime.prev && pageviews.prev
|
||||
? totaltime.prev / (pageviews.prev - bounces.prev)
|
||||
: 0
|
||||
}
|
||||
change={
|
||||
totaltime.value && pageviews.value
|
||||
? (diffs.totaltime / (diffs.pageviews - diffs.bounces) -
|
||||
totaltime.value / (pageviews.value - bounces.value)) *
|
||||
-1 || 0
|
||||
? totaltime.value / (pageviews.value - bounces.value) -
|
||||
totaltime.prev / (pageviews.prev - bounces.prev) || 0
|
||||
: 0
|
||||
}
|
||||
format={(n) =>
|
||||
|
@ -18,10 +18,12 @@ interface GlobalState {
|
||||
dateRange: DateRange;
|
||||
startDate: Dayjs | null;
|
||||
endDate: Dayjs | null;
|
||||
showPreviousPeriod: boolean;
|
||||
}
|
||||
|
||||
export const useGlobalStateStore = create<GlobalState>(() => ({
|
||||
dateRange: DateRange.Last24Hours,
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
showPreviousPeriod: false,
|
||||
}));
|
||||
|
@ -15,7 +15,7 @@ export const websiteFilterSchema = z.object({
|
||||
|
||||
const websiteStatsItemType = z.object({
|
||||
value: z.number(),
|
||||
change: z.number(),
|
||||
prev: z.number(),
|
||||
});
|
||||
|
||||
export const websiteStatsSchema = z.object({
|
||||
|
@ -91,7 +91,7 @@ export const http: MonitorProvider<{
|
||||
|
||||
return diff;
|
||||
} catch (err) {
|
||||
logger.error('run monitor http error', String(err));
|
||||
logger.error(`run monitor(${monitor.id}) http error`, String(err));
|
||||
return -1;
|
||||
}
|
||||
},
|
||||
|
@ -178,12 +178,14 @@ export const websiteRouter = router({
|
||||
]);
|
||||
|
||||
const stats = Object.keys(metrics[0]).reduce((obj, key) => {
|
||||
const current = Number(metrics[0][key]) || 0;
|
||||
const prev = Number(prevPeriod[0][key]) || 0;
|
||||
obj[key] = {
|
||||
value: Number(metrics[0][key]) || 0,
|
||||
change: Number(metrics[0][key]) - Number(prevPeriod[0][key]) || 0,
|
||||
value: current,
|
||||
prev,
|
||||
};
|
||||
return obj;
|
||||
}, {} as Record<string, { value: number; change: number }>);
|
||||
}, {} as Record<string, { value: number; prev: number }>);
|
||||
|
||||
return websiteStatsSchema.parse(stats);
|
||||
}),
|
||||
|
Loading…
Reference in New Issue
Block a user