feat: add date range component

This commit is contained in:
moonrailgun 2023-10-08 18:43:31 +08:00
parent 086c8ee3df
commit 59b44c041e
6 changed files with 302 additions and 66 deletions

View File

@ -1,5 +1,5 @@
{ {
"name": "vite-react-typescript-starter", "name": "tianji",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"scripts": { "scripts": {

View File

@ -1,60 +1,161 @@
import React from 'react'; import React, { useState } from 'react';
import { Dropdown, Select } from 'antd'; import { Button, DatePicker, Dropdown, MenuProps, Modal, Space } from 'antd';
import { Dayjs } from 'dayjs';
import { DownOutlined } from '@ant-design/icons';
import { DateRange, useGlobalStateStore } from '../store/global';
import { compact } from 'lodash-es'; import { compact } from 'lodash-es';
import clsx from 'clsx';
import { useGlobalRangeDate } from '../hooks/useGlobalRangeDate';
export const DateFilter: React.FC<{ const { RangePicker } = DatePicker;
showAllTime?: boolean;
}> = React.memo((props) => { interface DateFilterProps {
const options = compact([ className?: string;
{ label: 'Today', value: '1day' }, }
{ export const DateFilter: React.FC<DateFilterProps> = React.memo((props) => {
label: 'Last 24 hours', const [showPicker, setShowPicker] = useState(false);
value: '24hour', const [showDropdown, setShowDropdown] = useState(false);
const { label, startDate, endDate } = useGlobalRangeDate();
const [range, setRange] = useState<[Dayjs, Dayjs]>([startDate, endDate]);
const menu: MenuProps = {
onClick: () => {
setShowDropdown(false);
}, },
{ items: compact([
label: 'Yesterday', {
value: '-1day', label: 'Today',
}, onClick: () => {
{ useGlobalStateStore.setState({ dateRange: DateRange.Today });
label: 'This Week', },
value: '1week', },
}, {
{ label: 'Last 24 Hours',
label: 'Last 7 days', onClick: () => {
value: '7day', useGlobalStateStore.setState({ dateRange: DateRange.Last24Hours });
}, },
{ },
label: 'This Month', {
value: '1month', label: 'Yesterday',
}, onClick: () => {
{ useGlobalStateStore.setState({ dateRange: DateRange.Yesterday });
label: 'Last 30 days', },
value: '30day', },
}, {
{ type: 'divider',
label: 'Last 90 days', },
value: '90day', {
}, label: 'This week',
{ label: 'This year', value: '1year' }, onClick: () => {
props.showAllTime === true && { useGlobalStateStore.setState({ dateRange: DateRange.ThisWeek });
label: 'All time', },
value: 'all', },
}, {
{ label: 'Last 7 days',
label: 'Custom range', onClick: () => {
value: 'custom', useGlobalStateStore.setState({ dateRange: DateRange.Last7Days });
}, },
]); },
{
type: 'divider',
},
{
label: 'This Month',
onClick: () => {
useGlobalStateStore.setState({ dateRange: DateRange.ThisMonth });
},
},
{
label: 'Last 30 days',
onClick: () => {
useGlobalStateStore.setState({ dateRange: DateRange.Last30Days });
},
},
{
label: 'Last 90 days',
onClick: () => {
useGlobalStateStore.setState({ dateRange: DateRange.Last90Days });
},
},
{
label: 'This year',
onClick: () => {
useGlobalStateStore.setState({ dateRange: DateRange.ThisYear });
},
},
{
type: 'divider',
},
{
label: 'Custom',
onClick: () => {
setShowPicker(true);
},
},
] as MenuProps['items']),
};
return ( return (
<div> <>
<Select <Dropdown
className="min-w-[10rem]" className={props.className}
size="large" menu={menu}
options={options} trigger={['click']}
defaultValue="24hour" open={showDropdown}
/> onOpenChange={(open) => setShowDropdown(open)}
</div> >
<Button size="large">
<Space>
{label}
<DownOutlined
className={clsx(
'transition-transform scale-y-75',
showDropdown ? 'rotate-180' : 'rotate-0'
)}
/>
</Space>
</Button>
</Dropdown>
{showPicker && (
<Modal
title="Select your date range"
open={showPicker}
onCancel={() => setShowPicker(false)}
onOk={() => {
useGlobalStateStore.setState({
dateRange: DateRange.Custom,
startDate: range[0],
endDate: range[1],
});
setShowPicker(false);
}}
>
<div className="text-center">
<RangePicker
allowEmpty={[false, false]}
showTime={{
format: 'HH:mm',
showHour: true,
showNow: true,
showMinute: true,
showSecond: false,
}}
format="YYYY-MM-DD HH:mm"
value={range}
onChange={(range) => {
if (!range || !range[0] || !range[1]) {
return;
}
setRange([range[0], range[1]]);
}}
/>
</div>
</Modal>
)}
</>
); );
}); });
DateFilter.displayName = 'DateFilter'; DateFilter.displayName = 'DateFilter';

View File

@ -1,4 +1,4 @@
import { Button, message } from 'antd'; import { Button, message, Spin } from 'antd';
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { Column, ColumnConfig } from '@ant-design/charts'; import { Column, ColumnConfig } from '@ant-design/charts';
import { SyncOutlined } from '@ant-design/icons'; import { SyncOutlined } from '@ant-design/icons';
@ -23,14 +23,14 @@ import { MetricCard } from '../MetricCard';
import { formatNumber, formatShortTime } from '../../utils/common'; import { formatNumber, formatShortTime } from '../../utils/common';
import { useTheme } from '../../hooks/useTheme'; import { useTheme } from '../../hooks/useTheme';
import { WebsiteOnlineCount } from '../WebsiteOnlineCount'; import { WebsiteOnlineCount } from '../WebsiteOnlineCount';
import { useGlobalRangeDate } from '../../hooks/useGlobalRangeDate';
export const WebsiteOverview: React.FC<{ export const WebsiteOverview: React.FC<{
website: WebsiteInfo; website: WebsiteInfo;
actions?: React.ReactNode; actions?: React.ReactNode;
}> = React.memo((props) => { }> = React.memo((props) => {
const unit: DateUnit = 'hour'; const unit: DateUnit = 'hour';
const startDate = dayjs().subtract(1, 'day').add(1, unit).startOf(unit); const { startDate, endDate } = useGlobalRangeDate();
const endDate = dayjs().endOf(unit);
const { const {
pageviews, pageviews,
@ -72,12 +72,10 @@ export const WebsiteOverview: React.FC<{
]; ];
}, [pageviews, sessions, unit]); }, [pageviews, sessions, unit]);
if (isLoadingPageview || isLoadingStats) { const loading = isLoadingPageview || isLoadingStats;
return <Loading />;
}
return ( return (
<div> <Spin spinning={loading}>
<div className="flex"> <div className="flex">
<div className="flex flex-1 text-2xl font-bold items-center"> <div className="flex flex-1 text-2xl font-bold items-center">
<span className="mr-2" title={props.website.domain ?? ''}> <span className="mr-2" title={props.website.domain ?? ''}>
@ -111,14 +109,16 @@ export const WebsiteOverview: React.FC<{
onClick={handleRefresh} onClick={handleRefresh}
/> />
<DateFilter /> <div>
<DateFilter />
</div>
</div> </div>
</div> </div>
<div> <div>
<StatsChart data={chartData} unit={unit} /> <StatsChart data={chartData} unit={unit} />
</div> </div>
</div> </Spin>
); );
}); });
WebsiteOverview.displayName = 'WebsiteOverview'; WebsiteOverview.displayName = 'WebsiteOverview';

View File

@ -0,0 +1,107 @@
import { CalendarOutlined } from '@ant-design/icons';
import dayjs, { Dayjs } from 'dayjs';
import { useMemo } from 'react';
import { DateRange, useGlobalStateStore } from '../store/global';
export function useGlobalRangeDate(): {
label: React.ReactNode;
startDate: Dayjs;
endDate: Dayjs;
} {
const { dateRange, startDate, endDate } = useGlobalStateStore();
return useMemo(() => {
if (dateRange === DateRange.Custom) {
const _startDate = startDate ?? dayjs().subtract(1, 'day');
const _endDate = endDate ?? dayjs();
const isSameDate = dayjs(_startDate).isSame(_endDate, 'day');
return {
label: (
<div className="flex gap-2 items-center flex-nowrap">
<CalendarOutlined />
<span>
{`${dayjs(_startDate).format('YYYY-MM-DD HH:mm')} - ${dayjs(
_endDate
).format(isSameDate ? 'HH:mm' : 'YYYY-MM-DD HH:mm')}`}
</span>
</div>
),
startDate: _startDate,
endDate: _endDate,
};
}
if (dateRange === DateRange.Today) {
return {
label: 'Today',
startDate: dayjs().startOf('day'),
endDate: dayjs().endOf('day'),
};
}
if (dateRange === DateRange.Yesterday) {
return {
label: 'Yesterday',
startDate: dayjs().subtract(1, 'day').startOf('day'),
endDate: dayjs().subtract(1, 'day').endOf('day'),
};
}
if (dateRange === DateRange.ThisWeek) {
return {
label: 'This week',
startDate: dayjs().startOf('week'),
endDate: dayjs().endOf('week'),
};
}
if (dateRange === DateRange.Last7Days) {
return {
label: 'Last 7 days',
startDate: dayjs().subtract(7, 'day').startOf('day'),
endDate: dayjs().endOf('day'),
};
}
if (dateRange === DateRange.ThisMonth) {
return {
label: 'This month',
startDate: dayjs().startOf('month'),
endDate: dayjs().endOf('month'),
};
}
if (dateRange === DateRange.Last30Days) {
return {
label: 'Last 30 days',
startDate: dayjs().subtract(30, 'day').startOf('day'),
endDate: dayjs().endOf('day'),
};
}
if (dateRange === DateRange.Last90Days) {
return {
label: 'Last 90 days',
startDate: dayjs().subtract(90, 'day').startOf('day'),
endDate: dayjs().endOf('day'),
};
}
if (dateRange === DateRange.ThisYear) {
return {
label: 'Last 90 days',
startDate: dayjs().startOf('year'),
endDate: dayjs().endOf('year'),
};
}
// default last 24 hour
return {
label: 'Last 24 hours',
startDate: dayjs().subtract(1, 'day'),
endDate: dayjs(),
};
}, [dateRange, startDate, endDate]);
}

View File

@ -1,5 +1,4 @@
import { Card, Divider } from 'antd'; import { Card } from 'antd';
import dayjs from 'dayjs';
import React from 'react'; import React from 'react';
import { useParams } from 'react-router'; import { useParams } from 'react-router';
import { trpc } from '../../api/trpc'; import { trpc } from '../../api/trpc';
@ -8,6 +7,7 @@ import { Loading } from '../../components/Loading';
import { NotFoundTip } from '../../components/NotFoundTip'; import { NotFoundTip } from '../../components/NotFoundTip';
import { MetricsTable } from '../../components/website/MetricsTable'; import { MetricsTable } from '../../components/website/MetricsTable';
import { WebsiteOverview } from '../../components/website/WebsiteOverview'; import { WebsiteOverview } from '../../components/website/WebsiteOverview';
import { useGlobalRangeDate } from '../../hooks/useGlobalRangeDate';
import { useCurrentWorkspaceId } from '../../store/user'; import { useCurrentWorkspaceId } from '../../store/user';
export const WebsiteDetail: React.FC = React.memo(() => { export const WebsiteDetail: React.FC = React.memo(() => {
@ -17,6 +17,7 @@ export const WebsiteDetail: React.FC = React.memo(() => {
workspaceId, workspaceId,
websiteId: websiteId!, websiteId: websiteId!,
}); });
const { startDate, endDate } = useGlobalRangeDate();
if (!websiteId) { if (!websiteId) {
return <ErrorTip />; return <ErrorTip />;
@ -30,8 +31,8 @@ export const WebsiteDetail: React.FC = React.memo(() => {
return <NotFoundTip />; return <NotFoundTip />;
} }
const startAt = dayjs().subtract(1, 'day').unix() * 1000; const startAt = startDate.unix() * 1000;
const endAt = dayjs().unix() * 1000; const endAt = endDate.unix() * 1000;
return ( return (
<div className="py-6"> <div className="py-6">

View File

@ -0,0 +1,27 @@
import { Dayjs } from 'dayjs';
import { create } from 'zustand';
export enum DateRange {
Last24Hours,
Today,
Yesterday,
ThisWeek,
Last7Days,
ThisMonth,
Last30Days,
Last90Days,
ThisYear,
Custom,
}
interface GlobalState {
dateRange: DateRange;
startDate: Dayjs | null;
endDate: Dayjs | null;
}
export const useGlobalStateStore = create<GlobalState>(() => ({
dateRange: DateRange.Last24Hours,
startDate: null,
endDate: null,
}));