Skip to content

Commit

Permalink
feat(EstimateFilter): new estimate picker
Browse files Browse the repository at this point in the history
  • Loading branch information
asabotovich committed Sep 28, 2023
1 parent 10e7d3e commit ec2da4a
Show file tree
Hide file tree
Showing 27 changed files with 352 additions and 343 deletions.
2 changes: 0 additions & 2 deletions src/components/DashboardPage/DashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ export const DashboardPage = ({ user, ssrTime, defaultPresetFallback }: External
queryState,
queryString,
setTagsFilterOutside,
setEstimateFilter,
setStarredFilter,
setWatchingFilter,
setFulltextFilter,
Expand Down Expand Up @@ -174,7 +173,6 @@ export const DashboardPage = ({ user, ssrTime, defaultPresetFallback }: External
preset={currentPreset}
presets={userFilters}
onSearchChange={setFulltextFilter}
onEstimateChange={setEstimateFilter}
onStarredChange={setStarredFilter}
onWatchingChange={setWatchingFilter}
onPresetChange={setPreset}
Expand Down
5 changes: 3 additions & 2 deletions src/components/Estimate/Estimate.i18n/en.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
{
"Year title": "Year",
"Year clue": "If you are selecting only year it means goal will be full year long",
"Year clue": "If you are selecting only year it means the deadline will be the end of the year. For example: {end}.",
"Quarter title": "Quarter",
"Quarter clue": "This is the date shortcut: Q1, Q2, Q3, Q4. Currently is",
"Quarter clue": "This is the date shortcut: Q1, Q2, Q3, Q4. Currently is {quarter}. End is {end}.",
"Date title": "Strict date",
"Date clue": "Or you can choose a date if you have strict deadline.",
"Date is past": "Selected date is past"
}
5 changes: 3 additions & 2 deletions src/components/Estimate/Estimate.i18n/ru.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
{
"Year title": "Год",
"Year clue": "Если Вы выберите только год, цель будет до конца выбранного года.",
"Year clue": "Если выбрать только год, то датой будет выбран конец года, например: {end}.",
"Quarter title": "Квартал",
"Quarter clue": "Это кварталы выбранного года: Q1, Q2, Q3, Q4. Сейчас",
"Quarter clue": "Можно указать квартал: Q1, Q2, Q3, Q4. Сейчас {quarter}, заканчивается {end}.",
"Date title": "Дата",
"Date clue": "Или можно указать точную дату вручную, если есть строгий дедлайн.",
"Date is past": "Выбранная дата уже прошла"
}
216 changes: 119 additions & 97 deletions src/components/Estimate/Estimate.tsx
Original file line number Diff line number Diff line change
@@ -1,63 +1,102 @@
import React, { ComponentProps, ReactNode, useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import styled from 'styled-components';
import { gapXs, gapS, warn0 } from '@taskany/colors';
import { nullable, Text } from '@taskany/bricks';
import { IconExclamationSmallOutline } from '@taskany/icons';

import { createDateRange, isPastDate, getQuarterFromDate, getYearFromDate } from '../../utils/dateTime';
import {
createDateRange,
isPastDate,
getQuarterFromDate,
getYearFromDate,
getDateString,
getRelativeQuarterRange,
createLocaleDate,
createYearRange,
} from '../../utils/dateTime';
import { useLocale } from '../../hooks/useLocale';
import { DateType, DateRange } from '../../types/date';
import { EstimateDate } from '../EstimateDate';
import { EstimateDate } from '../EstimateDate/EstimateDate';
import { EstimateQuarter } from '../EstimateQuarter';
import { EstimateYear } from '../EstimateYear';
import { Option } from '../../types/estimate';
import { EstimatePopup } from '../EstimatePopup';

import { tr } from './Estimate.i18n';

type EstimateValue = {
const StyledWrapper = styled.div`
display: flex;
flex-direction: column;
gap: ${gapS};
`;

const StyledWarningWrapper = styled.div`
display: flex;
align-items: center;
gap: ${gapXs};
`;

const StyledIconExclamationSmallOutline = styled(IconExclamationSmallOutline)`
border-radius: 100%;
align-items: center;
display: block;
background: ${warn0};
font-size: 0;
`;

export type EstimateValue = {
type: DateType;
range: DateRange;
};

export interface EstimateProps {
mask: string;
placeholder: string;
value?: EstimateValue;
placement?: ComponentProps<typeof EstimatePopup>['placement'];
error?: { message?: string };
renderTrigger: (values: { onClick: () => void }) => ReactNode;
onChange?: (value?: EstimateValue) => void;
onClose?: () => void;
}

export const Estimate: React.FC<EstimateProps> = ({
mask,
placeholder,
value,
placement,
error,
renderTrigger,
onChange,
onClose,
}) => {
const [readOnly, setReadOnly] = useState({ year: true, quarter: true, date: true });
export const getReadOnlyFields = (type: EstimateValue['type']) => {
if (type === 'Strict') {
return { year: true, quarter: true, date: false };
}
if (type === 'Quarter') {
return { year: false, quarter: false, date: true };
}
return { year: false, quarter: true, date: true };
};

const currentQuarterRange = getRelativeQuarterRange('current');
const currentQuarter = getQuarterFromDate(currentQuarterRange.end);
const currentYearRange = createYearRange(getYearFromDate(new Date()));

export const Estimate = React.forwardRef<HTMLDivElement, EstimateProps>(({ value, onChange }, ref) => {
const locale = useLocale();
const [readOnly, setReadOnly] = useState(
value ? getReadOnlyFields(value.type) : { year: true, quarter: true, date: true },
);
const [year, setYear] = useState(value ? getYearFromDate(value.range.end) : undefined);
const [quarter, setQuarter] = useState(value ? getQuarterFromDate(value.range.end) : undefined);
const [date, setDate] = useState(value ? value.range.end : undefined);

const onOpen = useCallback(() => {
if (value?.type === 'Strict') {
setReadOnly({ year: true, quarter: true, date: false });
} else if (value?.type === 'Quarter') {
setReadOnly({ year: false, quarter: false, date: true });
} else {
setReadOnly({ year: false, quarter: true, date: true });
}
}, [value]);
const onChangeHandler = useCallback(
(newValue?: EstimateValue) => {
const oldStartDate = value && value.range.start ? getDateString(value.range.start) : null;
const newStartDate = newValue && newValue.range.start ? getDateString(newValue.range.start) : null;

const oldEndDate = value ? getDateString(value.range.end) : null;
const newEndDate = newValue ? getDateString(newValue.range.end) : null;

if (value?.type !== newValue?.type || oldStartDate !== newStartDate || oldEndDate !== newEndDate) {
onChange?.(newValue);
}
},
[value, onChange],
);

useEffect(() => {
if (readOnly.quarter && readOnly.year && readOnly.date) {
return;
}

if (!readOnly.quarter) {
onChange?.(
onChangeHandler?.(
year
? {
range: createDateRange(year, quarter),
Expand All @@ -66,7 +105,7 @@ export const Estimate: React.FC<EstimateProps> = ({
: undefined,
);
} else if (!readOnly.year) {
onChange?.(
onChangeHandler?.(
year
? {
range: createDateRange(year),
Expand All @@ -75,7 +114,7 @@ export const Estimate: React.FC<EstimateProps> = ({
: undefined,
);
} else {
onChange?.(
onChangeHandler?.(
date
? {
range: { end: date },
Expand All @@ -84,73 +123,56 @@ export const Estimate: React.FC<EstimateProps> = ({
: undefined,
);
}
}, [year, quarter, date, onChange, readOnly]);

const options = useMemo(
() => [
{
title: tr('Year title'),
clue: tr('Year clue'),
renderItem: (option: Option) => (
<EstimateYear
key={option.title}
option={option}
value={year}
readOnly={readOnly.year}
onChange={setYear}
setReadOnly={setReadOnly}
/>
),
},
{
title: tr('Quarter title'),
clue: `${tr('Quarter clue')} ${getQuarterFromDate(new Date())}.`,
renderItem: (option: Option) => (
<EstimateQuarter
key={option.title}
option={option}
value={quarter}
readOnly={readOnly.quarter}
onChange={setQuarter}
setReadOnly={setReadOnly}
/>
),
},
{
title: tr('Date title'),
clue: null,
renderItem: (option: Option) => (
<EstimateDate
key={option.title}
mask={mask}
placeholder={placeholder}
option={option}
value={date}
readOnly={readOnly.date}
onChange={setDate}
setReadOnly={setReadOnly}
/>
),
},
],
[date, quarter, year, mask, placeholder, readOnly],
);
}, [year, quarter, date, onChangeHandler, readOnly]);

const warning = useMemo(
() => (value && isPastDate(value.range.end) ? { message: tr('Date is past') } : undefined),
[value],
);

return (
<EstimatePopup
items={options}
placement={placement}
error={error}
warning={warning}
renderItem={({ renderItem, ...option }) => renderItem?.(option)}
renderTrigger={renderTrigger}
onClose={onClose}
onOpen={onOpen}
/>
<StyledWrapper ref={ref}>
{nullable(warning, (warn) => (
<StyledWarningWrapper>
<StyledIconExclamationSmallOutline size="xs" />
<Text color={warn0} size="xs">
{warn.message}
</Text>
</StyledWarningWrapper>
))}
<EstimateYear
title={tr('Year title')}
clue={tr
.raw('Year clue', {
end: createLocaleDate(currentYearRange.end, { locale }),
})
.join('')}
value={year}
readOnly={readOnly.year}
onChange={setYear}
setReadOnly={setReadOnly}
/>
<EstimateQuarter
title={tr('Quarter title')}
clue={`${tr
.raw('Quarter clue', {
quarter: currentQuarter,
end: createLocaleDate(currentQuarterRange.end, { locale }),
})
.join('')}`}
value={quarter}
readOnly={readOnly.quarter}
onChange={setQuarter}
setReadOnly={setReadOnly}
/>
<EstimateDate
title={tr('Date title')}
clue={tr('Date clue')}
value={date}
readOnly={readOnly.date}
onChange={setDate}
setReadOnly={setReadOnly}
/>
</StyledWrapper>
);
};
});
4 changes: 4 additions & 0 deletions src/components/EstimateDate/EstimateDate.i18n/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"Date input mask": "99/99/9999",
"Date input mask placeholder": "mm/dd/yyyy"
}
17 changes: 17 additions & 0 deletions src/components/EstimateDate/EstimateDate.i18n/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/* eslint-disable */
// Do not edit, use generator to update
import { i18n, fmt, I18nLangSet } from 'easy-typed-intl';
import getLang from '../../../utils/getLang';

import ru from './ru.json';
import en from './en.json';

export type I18nKey = keyof typeof ru & keyof typeof en;
type I18nLang = 'ru' | 'en';

const keyset: I18nLangSet<I18nKey> = {};

keyset['ru'] = ru;
keyset['en'] = en;

export const tr = i18n<I18nLang, I18nKey>(keyset, fmt, getLang);
4 changes: 4 additions & 0 deletions src/components/EstimateDate/EstimateDate.i18n/ru.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"Date input mask": "99.99.9999",
"Date input mask placeholder": "dd.mm.yyyy"
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,14 @@ import { IconXSolid } from '@taskany/icons';
import { useState, useRef, useCallback, Dispatch, SetStateAction, useEffect } from 'react';
import InputMask from 'react-input-mask';

import { useLocale } from '../hooks/useLocale';
import { currentLocaleDate, parseLocaleDate, createLocaleDate } from '../utils/dateTime';
import { Option } from '../types/estimate';
import { useLocale } from '../../hooks/useLocale';
import { currentLocaleDate, parseLocaleDate, createLocaleDate } from '../../utils/dateTime';
import { Option } from '../../types/estimate';
import { EstimateOption } from '../EstimateOption';

import { EstimateOption } from './EstimateOption';
import { tr } from './EstimateDate.i18n';

interface EstimateDateProps {
mask: string;
placeholder: string;
option: Omit<Option, 'clue'>;
interface EstimateDateProps extends Option {
value?: Date;
readOnly?: boolean;
onChange?: (value?: Date) => void;
Expand All @@ -31,14 +29,7 @@ const isDateFullyFilled = (date: string) => {
return cleanedDate.length === expectedLength;
};

export const EstimateDate: React.FC<EstimateDateProps> = ({
mask,
placeholder,
option,
readOnly,
onChange,
setReadOnly,
}) => {
export const EstimateDate: React.FC<EstimateDateProps> = ({ title, clue, readOnly, onChange, setReadOnly }) => {
const locale = useLocale();
const currentDate = currentLocaleDate({ locale });
const [fullDate, setFullDate] = useState<string>(currentDate);
Expand Down Expand Up @@ -104,11 +95,17 @@ export const EstimateDate: React.FC<EstimateDateProps> = ({

return (
<EstimateOption
title={option.title}
title={title}
clue={clue}
readOnly={readOnly}
onClick={onClick}
renderTrigger={() => (
<InputMask mask={mask} placeholder={placeholder} onChange={onChangeDate} value={fullDate}>
<InputMask
mask={tr('Date input mask')}
placeholder={tr('Date input mask placeholder')}
onChange={onChangeDate}
value={fullDate}
>
{/* @ts-ignore incorrect type in react-input-mask */}
{(props) => (
<Input ref={ref} iconRight={<IconXSolid size="xxs" onClick={onRemoveDate} />} {...props} />
Expand Down
Loading

0 comments on commit ec2da4a

Please sign in to comment.