feat: add date range component
This commit is contained in:
parent
086c8ee3df
commit
59b44c041e
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "vite-react-typescript-starter",
|
||||
"name": "tianji",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
|
@ -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' },
|
||||
{
|
||||
label: 'Last 24 hours',
|
||||
value: '24hour',
|
||||
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);
|
||||
},
|
||||
{
|
||||
label: 'Yesterday',
|
||||
value: '-1day',
|
||||
},
|
||||
{
|
||||
label: 'This Week',
|
||||
value: '1week',
|
||||
},
|
||||
{
|
||||
label: 'Last 7 days',
|
||||
value: '7day',
|
||||
},
|
||||
{
|
||||
label: 'This Month',
|
||||
value: '1month',
|
||||
},
|
||||
{
|
||||
label: 'Last 30 days',
|
||||
value: '30day',
|
||||
},
|
||||
{
|
||||
label: 'Last 90 days',
|
||||
value: '90day',
|
||||
},
|
||||
{ label: 'This year', value: '1year' },
|
||||
props.showAllTime === true && {
|
||||
label: 'All time',
|
||||
value: 'all',
|
||||
},
|
||||
{
|
||||
label: 'Custom range',
|
||||
value: 'custom',
|
||||
},
|
||||
]);
|
||||
items: compact([
|
||||
{
|
||||
label: 'Today',
|
||||
onClick: () => {
|
||||
useGlobalStateStore.setState({ dateRange: DateRange.Today });
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Last 24 Hours',
|
||||
onClick: () => {
|
||||
useGlobalStateStore.setState({ dateRange: DateRange.Last24Hours });
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Yesterday',
|
||||
onClick: () => {
|
||||
useGlobalStateStore.setState({ dateRange: DateRange.Yesterday });
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
label: 'This week',
|
||||
onClick: () => {
|
||||
useGlobalStateStore.setState({ dateRange: DateRange.ThisWeek });
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Last 7 days',
|
||||
onClick: () => {
|
||||
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 (
|
||||
<div>
|
||||
<Select
|
||||
className="min-w-[10rem]"
|
||||
size="large"
|
||||
options={options}
|
||||
defaultValue="24hour"
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
<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';
|
||||
|
@ -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}
|
||||
/>
|
||||
|
||||
<DateFilter />
|
||||
<div>
|
||||
<DateFilter />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<StatsChart data={chartData} unit={unit} />
|
||||
</div>
|
||||
</div>
|
||||
</Spin>
|
||||
);
|
||||
});
|
||||
WebsiteOverview.displayName = 'WebsiteOverview';
|
||||
|
107
src/client/hooks/useGlobalRangeDate.tsx
Normal file
107
src/client/hooks/useGlobalRangeDate.tsx
Normal 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]);
|
||||
}
|
@ -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">
|
||||
|
27
src/client/store/global.ts
Normal file
27
src/client/store/global.ts
Normal 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,
|
||||
}));
|
Loading…
Reference in New Issue
Block a user