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,
"version": "0.0.0",
"scripts": {

View File

@ -1,60 +1,161 @@
import React from 'react';
import { Dropdown, Select } from 'antd';
import React, { useState } from 'react';
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 clsx from 'clsx';
import { useGlobalRangeDate } from '../hooks/useGlobalRangeDate';
export const DateFilter: React.FC<{
showAllTime?: boolean;
}> = React.memo((props) => {
const options = compact([
{ label: 'Today', value: '1day' },
const { RangePicker } = DatePicker;
interface DateFilterProps {
className?: string;
}
export const DateFilter: React.FC<DateFilterProps> = React.memo((props) => {
const [showPicker, setShowPicker] = useState(false);
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: 'Last 24 hours',
value: '24hour',
label: 'Today',
onClick: () => {
useGlobalStateStore.setState({ dateRange: DateRange.Today });
},
},
{
label: 'Last 24 Hours',
onClick: () => {
useGlobalStateStore.setState({ dateRange: DateRange.Last24Hours });
},
},
{
label: 'Yesterday',
value: '-1day',
onClick: () => {
useGlobalStateStore.setState({ dateRange: DateRange.Yesterday });
},
},
{
label: 'This Week',
value: '1week',
type: 'divider',
},
{
label: 'This week',
onClick: () => {
useGlobalStateStore.setState({ dateRange: DateRange.ThisWeek });
},
},
{
label: 'Last 7 days',
value: '7day',
onClick: () => {
useGlobalStateStore.setState({ dateRange: DateRange.Last7Days });
},
},
{
type: 'divider',
},
{
label: 'This Month',
value: '1month',
onClick: () => {
useGlobalStateStore.setState({ dateRange: DateRange.ThisMonth });
},
},
{
label: 'Last 30 days',
value: '30day',
onClick: () => {
useGlobalStateStore.setState({ dateRange: DateRange.Last30Days });
},
},
{
label: 'Last 90 days',
value: '90day',
onClick: () => {
useGlobalStateStore.setState({ dateRange: DateRange.Last90Days });
},
{ label: 'This year', value: '1year' },
props.showAllTime === true && {
label: 'All time',
value: 'all',
},
{
label: 'Custom range',
value: 'custom',
label: 'This year',
onClick: () => {
useGlobalStateStore.setState({ dateRange: DateRange.ThisYear });
},
]);
},
{
type: 'divider',
},
{
label: 'Custom',
onClick: () => {
setShowPicker(true);
},
},
] as MenuProps['items']),
};
return (
<div>
<Select
className="min-w-[10rem]"
size="large"
options={options}
defaultValue="24hour"
<>
<Dropdown
className={props.className}
menu={menu}
trigger={['click']}
open={showDropdown}
onOpenChange={(open) => setShowDropdown(open)}
>
<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';

View File

@ -1,4 +1,4 @@
import { Button, message } from 'antd';
import { Button, message, Spin } from 'antd';
import React, { useMemo } from 'react';
import { Column, ColumnConfig } from '@ant-design/charts';
import { SyncOutlined } from '@ant-design/icons';
@ -23,14 +23,14 @@ import { MetricCard } from '../MetricCard';
import { formatNumber, formatShortTime } from '../../utils/common';
import { useTheme } from '../../hooks/useTheme';
import { WebsiteOnlineCount } from '../WebsiteOnlineCount';
import { useGlobalRangeDate } from '../../hooks/useGlobalRangeDate';
export const WebsiteOverview: React.FC<{
website: WebsiteInfo;
actions?: React.ReactNode;
}> = React.memo((props) => {
const unit: DateUnit = 'hour';
const startDate = dayjs().subtract(1, 'day').add(1, unit).startOf(unit);
const endDate = dayjs().endOf(unit);
const { startDate, endDate } = useGlobalRangeDate();
const {
pageviews,
@ -72,12 +72,10 @@ export const WebsiteOverview: React.FC<{
];
}, [pageviews, sessions, unit]);
if (isLoadingPageview || isLoadingStats) {
return <Loading />;
}
const loading = isLoadingPageview || isLoadingStats;
return (
<div>
<Spin spinning={loading}>
<div className="flex">
<div className="flex flex-1 text-2xl font-bold items-center">
<span className="mr-2" title={props.website.domain ?? ''}>
@ -111,14 +109,16 @@ export const WebsiteOverview: React.FC<{
onClick={handleRefresh}
/>
<div>
<DateFilter />
</div>
</div>
</div>
<div>
<StatsChart data={chartData} unit={unit} />
</div>
</div>
</Spin>
);
});
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 dayjs from 'dayjs';
import { Card } from 'antd';
import React from 'react';
import { useParams } from 'react-router';
import { trpc } from '../../api/trpc';
@ -8,6 +7,7 @@ import { Loading } from '../../components/Loading';
import { NotFoundTip } from '../../components/NotFoundTip';
import { MetricsTable } from '../../components/website/MetricsTable';
import { WebsiteOverview } from '../../components/website/WebsiteOverview';
import { useGlobalRangeDate } from '../../hooks/useGlobalRangeDate';
import { useCurrentWorkspaceId } from '../../store/user';
export const WebsiteDetail: React.FC = React.memo(() => {
@ -17,6 +17,7 @@ export const WebsiteDetail: React.FC = React.memo(() => {
workspaceId,
websiteId: websiteId!,
});
const { startDate, endDate } = useGlobalRangeDate();
if (!websiteId) {
return <ErrorTip />;
@ -30,8 +31,8 @@ export const WebsiteDetail: React.FC = React.memo(() => {
return <NotFoundTip />;
}
const startAt = dayjs().subtract(1, 'day').unix() * 1000;
const endAt = dayjs().unix() * 1000;
const startAt = startDate.unix() * 1000;
const endAt = endDate.unix() * 1000;
return (
<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,
}));