Skip to content

Commit

Permalink
feat(Estimate): create new popup for estimate
Browse files Browse the repository at this point in the history
  • Loading branch information
DenisVorop committed Aug 29, 2023
1 parent 26f83a3 commit 54832a9
Show file tree
Hide file tree
Showing 12 changed files with 745 additions and 13 deletions.
8 changes: 8 additions & 0 deletions src/components/Estimate/Estimate.i18n/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Year title": "Year",
"Year clue": "If you are selecting only year it means goal will be full year long",
"Quarter title": "Quarter",
"Quarter clue": "This is the date shortcut: Q1, Q2, Q3, Q4. Currently is",
"Date title": "Strict date",
"Date is past": "Selected date is past"
}
17 changes: 17 additions & 0 deletions src/components/Estimate/Estimate.i18n/index.tsx
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);
8 changes: 8 additions & 0 deletions src/components/Estimate/Estimate.i18n/ru.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Year title": "Год",
"Year clue": "Если Вы выберите только год, цель будет до конца выбранного года.",
"Quarter title": "Квартал",
"Quarter clue": "Это кварталы выбранного года: Q1, Q2, Q3, Q4. Сейчас",
"Date title": "Дата",
"Date is past": "Выбранная дата уже прошла"
}
83 changes: 83 additions & 0 deletions src/components/Estimate/Estimate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import React, { ReactNode } from 'react';

import { isPastDate, parseLocaleDate, quarterFromDate } from '../../utils/dateTime';
import { PopupProps } from '../OutlinePopup';
import { EstimateDate } from '../EstimateDate';
import { EstimateQuarter } from '../EstimateQuarter';
import { EstimateYear } from '../EstimateYear';
import { Estimate as EstimateType, Option } from '../../types/estimate';
import { useLocale } from '../../hooks/useLocale';
import { EstimatePopup } from '../EstimatePopup';

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

interface EstimateProps {
renderTrigger: (values: { onClick: () => void }) => ReactNode;
onChange?: (value?: EstimateType) => void;
onClose?: () => void;
value?: EstimateType;
placeholder: string;
mask: string;
placement?: PopupProps['placement'];
error?: { message?: string };
}

export const Estimate: React.FC<EstimateProps> = ({
renderTrigger,
onChange,
onClose,
value,
mask,
placeholder,
placement,
error,
}) => {
const locale = useLocale();
const options = [
{
title: tr('Year title'),
clue: tr('Year clue'),
renderItem: (option: Option) => <EstimateYear option={option} onChange={onChange} value={value} />,
},
{
title: tr('Quarter title'),
clue: `${tr('Quarter clue')} ${quarterFromDate(new Date())}.`,
renderItem: (option: Option) => <EstimateQuarter option={option} onChange={onChange} value={value} />,
},
{
title: tr('Date title'),
clue: null,
renderItem: (option: Omit<Option, 'clue'>) => (
<EstimateDate placeholder={placeholder} mask={mask} option={option} onChange={onChange} value={value} />
),
},
];

type ReduceOption = (typeof options)[number];
const optionsMap = options.reduce<Record<string, ReduceOption>>((acc, cur) => {
acc[cur.title] = cur;
return acc;
}, {});

const items = options.map(({ renderItem: _, ...rest }) => rest);

const warning =
(value && +value.y < new Date().getFullYear()) ||
(value?.date && isPastDate(parseLocaleDate(value.date, { locale })))
? { message: tr('Date is past') }
: undefined;

return (
<EstimatePopup
items={items}
placement={placement}
error={error}
warning={warning}
renderItem={(option: Option) => {
return optionsMap[option.title].renderItem(option);
}}
renderTrigger={renderTrigger}
onClose={onClose}
/>
);
};
157 changes: 157 additions & 0 deletions src/components/EstimateDate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { Input, Text, useClickOutside } from '@taskany/bricks';
import { danger8, danger9, gapS, gray9, textColor } from '@taskany/colors';
import { IconPlusCircleSolid } from '@taskany/icons';
import { useState, useRef, useEffect, useCallback } from 'react';
import styled from 'styled-components';
import InputMask from 'react-input-mask';

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

const StyledWrapper = styled.div<{ readOnly: boolean }>`
display: flex;
align-items: ${({ readOnly }) => (readOnly ? 'end' : 'center')};
gap: ${gapS};
width: fit-content;
position: relative;
`;

const StyledText = styled(Text)`
white-space: nowrap;
`;

const StyledRemoveButton = styled.div`
border-radius: 100%;
background: red;
height: 12px;
width: 12px;
min-width: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
transform: rotate(45deg);
cursor: pointer;
position: absolute;
right: -4px;
top: -4px;
background-color: ${danger8};
color: ${textColor};
transition: background-color 200ms ease;
&:hover {
background-color: ${danger9};
color: ${textColor};
}
`;

interface EstimateDateProps {
option: Omit<Option, 'clue'>;
mask: string;
placeholder: string;
value?: Estimate;
onChange?: (value?: Estimate) => void;
}

export const EstimateDate: React.FC<EstimateDateProps> = ({ option, mask, placeholder, onChange, value }) => {
const locale = useLocale();
const currentDate = currentLocaleDate({ locale });
const [readOnly, setReadOnly] = useState(true);
const [fullDate, setFullDate] = useState(currentDate);
const ref = useRef(null);

useClickOutside(ref, () => {
if (!fullDate.includes('_')) return;

setFullDate(currentDate);
setReadOnly(true);
value ? (value.date = null) : undefined;
onChange?.(value);
});

useEffect(() => {
if (fullDate.includes('_') || fullDate === currentDate) return;

const values = createValue(fullDate, locale);
onChange?.(values);
}, [currentDate, fullDate, locale, onChange]);

const onCreateEstimate = useCallback(
(date: string | Date) => {
date = parseLocaleDate(date, { locale });
value?.y && date.setFullYear(+value.y);

return createValue(date, locale);
},
[locale, value?.y],
);

useEffect(() => {
if (readOnly) return;

const estimate = onCreateEstimate(value?.date || currentDate);

onChange?.(estimate);
setFullDate(estimate.date);
}, [readOnly, currentDate, onCreateEstimate, onChange, value?.date]);

const onChangeDate = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.target;

if (value.includes('_')) {
setFullDate(value);
return;
}

const date = parseLocaleDate(value, { locale });

if (Number.isNaN(new Date(date).getTime())) {
setFullDate(currentDate);
return;
}

setFullDate(createLocaleDate(date, { locale }));
},
[currentDate, locale],
);

const onRemoveDate = useCallback(() => {
if (!value) return;

value.date = null;
value.q = null;

setReadOnly(true);
setFullDate(currentDate);
onChange?.(value);
}, [currentDate, onChange, value]);

const onClickIcon = useCallback(() => {
const estimate = onCreateEstimate(value?.date || currentDate);

setReadOnly(false);
setFullDate(estimate.date);
onChange?.(estimate);
}, [currentDate, onCreateEstimate, onChange, value?.date]);

return (
<StyledWrapper readOnly={readOnly} key={option.title}>
<StyledText weight="regular" size="s">
{option.title}
</StyledText>
{readOnly ? (
<IconPlusCircleSolid size="xs" color={gray9} onClick={onClickIcon} style={{ cursor: 'pointer' }} />
) : (
<>
<InputMask mask={mask} placeholder={placeholder} onChange={onChangeDate} value={fullDate}>
{/* @ts-ignore incorrect type in react-input-mask */}
{(props) => <Input ref={ref} {...props} />}
</InputMask>
<StyledRemoveButton onClick={onRemoveDate}>+</StyledRemoveButton>
</>
)}
</StyledWrapper>
);
};
Loading

0 comments on commit 54832a9

Please sign in to comment.