Created new dropdown component to select relative or absolute date ranges

This commit is contained in:
Alejandro Celaya
2020-12-14 22:58:15 +01:00
parent 288f6e2cf8
commit 4e236a80de
13 changed files with 288 additions and 34 deletions

View File

@@ -4,7 +4,7 @@ import { isEmpty, pipe } from 'ramda';
import moment from 'moment';
import SearchField from '../utils/SearchField';
import Tag from '../tags/helpers/Tag';
import DateRangeRow from '../utils/DateRangeRow';
import DateRangeRow from '../utils/dates/DateRangeRow';
import { formatDate } from '../utils/helpers/date';
import ColorGenerator from '../utils/services/ColorGenerator';
import { ShortUrlsListParams } from './reducers/shortUrlsListParams';

View File

@@ -1,7 +1,3 @@
.sorting-dropdown__menu {
width: 100%;
}
.sorting-dropdown__menu--link.sorting-dropdown__menu--link {
min-width: 11rem;
}

View File

@@ -35,7 +35,7 @@ export default function SortingDropdown<T extends string = string>(
</DropdownToggle>
<DropdownMenu
right={right}
className={classNames('sorting-dropdown__menu', { 'sorting-dropdown__menu--link': !isButton })}
className={classNames('w-100', { 'sorting-dropdown__menu--link': !isButton })}
>
{toPairs(items).map(([ fieldKey, fieldValue ]) => (
<DropdownItem key={fieldKey} active={orderField === fieldKey} onClick={handleItemClick(fieldKey as T)}>

View File

@@ -1,9 +1,8 @@
import moment from 'moment';
import DateInput from './DateInput';
import DateInput from '../DateInput';
import { DateRange } from './types';
interface DateRangeRowProps {
startDate?: moment.Moment | null;
endDate?: moment.Moment | null;
interface DateRangeRowProps extends DateRange {
onStartDateChange: (date: moment.Moment | null) => void;
onEndDateChange: (date: moment.Moment | null) => void;
disabled?: boolean;
@@ -16,7 +15,7 @@ const DateRangeRow = (
<div className="col-md-6">
<DateInput
selected={startDate}
placeholderText="Since"
placeholderText="Since..."
isClearable
maxDate={endDate ?? undefined}
disabled={disabled}
@@ -27,7 +26,7 @@ const DateRangeRow = (
<DateInput
className="mt-2 mt-md-0"
selected={endDate}
placeholderText="Until"
placeholderText="Until..."
isClearable
minDate={startDate ?? undefined}
disabled={disabled}

View File

@@ -0,0 +1,18 @@
@import '../../utils/mixins/vertical-align';
.date-range-selector__btn.date-range-selector__btn,
.date-range-selector__btn.date-range-selector__btn:not(:disabled):not(.disabled):active,
.date-range-selector__btn.date-range-selector__btn:not(:disabled):not(.disabled).active,
.date-range-selector__btn.date-range-selector__btn:not(:disabled):not(.disabled):hover,
.show > .date-range-selector__btn.date-range-selector__btn.dropdown-toggle {
color: #6c757d;
background-color: white;
text-align: left;
border-color: rgba(0, 0, 0, .125);
}
.date-range-selector__btn.date-range-selector__btn:after {
@include vertical-align();
right: .75rem;
}

View File

@@ -0,0 +1,84 @@
import { useState } from 'react';
import { Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap';
import { useToggle } from '../helpers/hooks';
import {
DateInterval,
DateRange,
dateRangeIsEmpty,
rangeOrIntervalToString,
intervalToDateRange,
rangeIsInterval,
} from './types';
import DateRangeRow from './DateRangeRow';
import './DateRangeSelector.scss';
interface DateRangeSelectorProps {
initialDateRange?: DateInterval | DateRange;
disabled?: boolean;
onDatesChange: (dateRange: DateRange) => void;
}
export const DateRangeSelector = ({ onDatesChange, initialDateRange, disabled = false }: DateRangeSelectorProps) => {
const [ isOpen, toggle ] = useToggle();
const [ activeInterval, setActiveInterval ] = useState(
rangeIsInterval(initialDateRange) ? initialDateRange : undefined,
);
const [ activeDateRange, setActiveDateRange ] = useState(
!rangeIsInterval(initialDateRange) ? initialDateRange : undefined,
);
const updateDateRange = (dateRange: DateRange) => {
setActiveInterval(undefined);
setActiveDateRange(dateRange);
onDatesChange(dateRange);
};
const updateInterval = (dateInterval?: DateInterval) => () => {
setActiveInterval(dateInterval);
setActiveDateRange(undefined);
onDatesChange(intervalToDateRange(dateInterval));
};
return (
<Dropdown isOpen={isOpen} toggle={toggle} disabled={disabled}>
<DropdownToggle caret className="date-range-selector__btn btn-block" color="primary">
{rangeOrIntervalToString(activeInterval ?? activeDateRange) ?? 'All visits'}
</DropdownToggle>
<DropdownMenu className="w-100">
<DropdownItem
active={activeInterval === undefined && dateRangeIsEmpty(activeDateRange)}
onClick={updateInterval(undefined)}
>
All visits
</DropdownItem>
<DropdownItem divider />
<DropdownItem active={activeInterval === 'today'} onClick={updateInterval('today')}>Today</DropdownItem>
<DropdownItem active={activeInterval === 'yesterday'} onClick={updateInterval('yesterday')}>
Yesterday
</DropdownItem>
<DropdownItem active={activeInterval === 'last7Days'} onClick={updateInterval('last7Days')}>
Last 7 days
</DropdownItem>
<DropdownItem active={activeInterval === 'last30Days'} onClick={updateInterval('last30Days')}>
Last 30 days
</DropdownItem>
<DropdownItem active={activeInterval === 'last90Days'} onClick={updateInterval('last90Days')}>
Last 90 days
</DropdownItem>
<DropdownItem active={activeInterval === 'last180days'} onClick={updateInterval('last180days')}>
Last 180 days
</DropdownItem>
<DropdownItem active={activeInterval === 'last365Days'} onClick={updateInterval('last365Days')}>
Last 365 days
</DropdownItem>
<DropdownItem divider />
<DropdownItem header>Custom:</DropdownItem>
<DropdownItem text>
<DateRangeRow
{...activeDateRange}
onStartDateChange={(startDate) => updateDateRange({ ...activeDateRange, startDate })}
onEndDateChange={(endDate) => updateDateRange({ ...activeDateRange, endDate })}
/>
</DropdownItem>
</DropdownMenu>
</Dropdown>
);
};

View File

@@ -0,0 +1,80 @@
import moment from 'moment';
import { filter, isEmpty } from 'ramda';
import { formatInternational } from '../../helpers/date';
export interface DateRange {
startDate?: moment.Moment | null;
endDate?: moment.Moment | null;
}
export type DateInterval = 'today' | 'yesterday' | 'last7Days' | 'last30Days' | 'last90Days' | 'last180days' | 'last365Days';
export const dateRangeIsEmpty = (dateRange?: DateRange): boolean => dateRange === undefined
|| isEmpty(filter(Boolean, dateRange as any));
export const rangeIsInterval = (range?: DateRange | DateInterval): range is DateInterval => typeof range === 'string';
const INTERVAL_TO_STRING_MAP: Record<DateInterval, string> = {
today: 'Today',
yesterday: 'Yesterday',
last7Days: 'Last 7 days',
last30Days: 'Last 30 days',
last90Days: 'Last 90 days',
last180days: 'Last 180 days',
last365Days: 'Last 365 days',
};
const dateRangeToString = (range?: DateRange): string | undefined => {
if (!range || dateRangeIsEmpty(range)) {
return undefined;
}
if (range.startDate && !range.endDate) {
return `Since ${formatInternational(range.startDate)}`;
}
if (!range.startDate && range.endDate) {
return `Until ${formatInternational(range.endDate)}`;
}
return `${formatInternational(range.startDate)} - ${formatInternational(range.endDate)}`;
};
export const rangeOrIntervalToString = (range?: DateRange | DateInterval): string | undefined => {
if (!range) {
return undefined;
}
if (!rangeIsInterval(range)) {
return dateRangeToString(range);
}
return INTERVAL_TO_STRING_MAP[range];
};
export const intervalToDateRange = (dateInterval?: DateInterval): DateRange => {
if (!dateInterval) {
return {};
}
switch (dateInterval) {
case 'today':
return { startDate: moment().startOf('day'), endDate: moment() };
case 'yesterday':
const yesterday = moment().subtract(1, 'day');
return { startDate: yesterday.startOf('day'), endDate: yesterday.endOf('day') };
case 'last7Days':
return { startDate: moment().subtract(7, 'days').startOf('day'), endDate: moment() };
case 'last30Days':
return { startDate: moment().subtract(30, 'days').startOf('day'), endDate: moment() };
case 'last90Days':
return { startDate: moment().subtract(90, 'days').startOf('day'), endDate: moment() };
case 'last180days':
return { startDate: moment().subtract(180, 'days').startOf('day'), endDate: moment() };
case 'last365Days':
return { startDate: moment().subtract(365, 'days').startOf('day'), endDate: moment() };
}
return {};
};

View File

@@ -12,3 +12,5 @@ const formatDateFromFormat = (date?: NullableDate, format?: string): OptionalStr
export const formatDate = (format = 'YYYY-MM-DD') => (date?: NullableDate) => formatDateFromFormat(date, format);
export const formatIsoDate = (date?: NullableDate) => formatDateFromFormat(date, undefined);
export const formatInternational = formatDate();

View File

@@ -5,7 +5,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCalendarAlt, faMapMarkedAlt, faList, faChartPie } from '@fortawesome/free-solid-svg-icons';
import { IconDefinition } from '@fortawesome/fontawesome-common-types';
import moment from 'moment';
import DateRangeRow from '../utils/DateRangeRow';
import { DateRangeSelector } from '../utils/dates/DateRangeSelector';
import Message from '../utils/Message';
import { formatDate } from '../utils/helpers/date';
import { ShlinkVisitsParams } from '../utils/services/types';
@@ -225,12 +225,12 @@ const VisitsStats: FC<VisitsStatsProps> = ({ children, visitsInfo, getVisits, ca
<section className="mt-4">
<div className="row flex-md-row-reverse">
<div className="col-lg-7 col-xl-6">
<DateRangeRow
<DateRangeSelector
disabled={loading}
startDate={startDate}
endDate={endDate}
onStartDateChange={setStartDate}
onEndDateChange={setEndDate}
onDatesChange={({ startDate: newStartDate, endDate: newEndDate }) => {
setStartDate(newStartDate ?? null);
setEndDate(newEndDate ?? null);
}}
/>
</div>
{visits.length > 0 && (