-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(Estimate): create new popup for estimate
- Loading branch information
1 parent
26f83a3
commit 54832a9
Showing
12 changed files
with
745 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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": "Выбранная дата уже прошла" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} | ||
/> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
}; |
Oops, something went wrong.