From cb72521811ae2a59fbb171d3f6d797c50d90eb44 Mon Sep 17 00:00:00 2001 From: Andrey Kopylov Date: Thu, 9 Jul 2026 17:48:58 +0500 Subject: [PATCH 01/14] [logic-and-ui-improves] change components without colors --- src/components/AmountInput/AmountInput.tsx | 2 +- .../AutoHeightToggle/AutoHeightToggle.tsx | 34 +- src/components/Bone/Bone.tsx | 22 +- .../ButtonBase/ButtonBase.module.scss | 1 - src/components/ButtonBase/ButtonBase.tsx | 9 +- .../ButtonRangePicker.module.scss | 3 - .../ButtonRangePicker/ButtonRangePicker.tsx | 22 +- src/components/ButtonRangePicker/messages.ts | 2 +- src/components/Chart/Chart.tsx | 39 +- .../Chart/ExtraRewards/ExtraRewards.tsx | 330 ++++++++++++++++ src/components/Chart/ExtraRewards/messages.ts | 38 ++ src/components/Chart/Legend/Amount/Amount.tsx | 26 +- src/components/Chart/Legend/Legend.tsx | 4 +- .../Chart/Legend/Percentage/Percentage.tsx | 4 +- src/components/Chart/Legend/Time/Time.tsx | 6 +- src/components/Chart/NoItems/NoItems.tsx | 7 +- src/components/Chart/Skeleton/Skeleton.tsx | 8 +- src/components/Chart/util/global.d.ts | 14 + src/components/Chart/util/useCreateChart.ts | 2 +- src/components/Chart/util/useSeries.ts | 60 --- src/components/Chart/util/useSettings.ts | 94 ----- src/components/Counter/Counter.module.scss | 21 +- src/components/Counter/Counter.tsx | 8 +- .../DatePicker/Calendar/Calendar.tsx | 270 ++++++------- .../Day.module.scss => Cell/Cell.module.scss} | 0 .../Calendar/{Day/Day.tsx => Cell/Cell.tsx} | 16 +- .../Days.module.scss} | 0 .../DatePicker/Calendar/Days/Days.tsx | 88 +++++ .../DatePicker/Calendar/Header/Header.tsx | 83 ++++ .../DatePicker/Calendar/Months/Months.tsx | 57 +++ .../DatePicker/Calendar/Years/Years.tsx | 51 +++ .../DatePicker/Calendar/util/useDays.ts | 128 +++--- src/components/DatePicker/DatePicker.tsx | 19 +- src/components/Dropdown/Dropdown.tsx | 25 +- .../DropdownView/Dropdown.module.scss | 10 + .../Dropdown/DropdownView/DropdownView.tsx | 25 +- src/components/Dropdown/Option/Option.tsx | 14 +- .../Option/OptionTitle/OptionTitle.tsx | 4 +- src/components/Form/Form.tsx | 4 +- src/components/FormValid/FormValid.tsx | 9 +- src/components/Href/Href.tsx | 18 +- src/components/Icon/Icon.module.scss | 8 +- src/components/Icon/Icon.tsx | 18 +- src/components/Image/Image.tsx | 2 +- src/components/Image/util/images.ts | 79 +++- .../ImagesProvider/images/connector/MMI.svg | 31 -- .../images/connector/braveBrowser.svg | 24 -- .../images/connector/ledgerLive.svg | 1 - .../images/connector/metamask.svg | 31 -- .../images/connector/monitorAddress.svg | 103 +---- .../ImagesProvider/images/connector/okx.svg | 1 - .../images/connector/portis.svg | 16 - .../ImagesProvider/images/connector/rabby.svg | 24 -- .../ImagesProvider/images/connector/taho.svg | 4 - .../images/connector/trustWallet.svg | 4 - .../ImagesProvider/images/connector/zengo.svg | 19 - .../ImagesProvider/images/currency/aud.svg | 6 +- .../ImagesProvider/images/currency/cny.svg | 6 +- .../ImagesProvider/images/currency/eur.svg | 4 +- .../ImagesProvider/images/currency/gbp.svg | 6 +- .../ImagesProvider/images/currency/jpy.svg | 4 +- .../ImagesProvider/images/currency/krw.svg | 5 +- .../ImagesProvider/images/currency/usd.svg | 4 +- .../ImagesProvider/images/icon/actions.svg | 6 + .../ImagesProvider/images/icon/arrow.svg | 5 + .../ImagesProvider/images/icon/book.svg | 5 + .../ImagesProvider/images/icon/calendar.svg | 9 +- .../ImagesProvider/images/icon/caret.svg | 3 + .../ImagesProvider/images/icon/chats.svg | 3 + .../ImagesProvider/images/icon/check.svg | 5 +- .../ImagesProvider/images/icon/close.svg | 5 +- .../ImagesProvider/images/icon/collapse.svg | 6 + .../ImagesProvider/images/icon/control.svg | 11 + .../ImagesProvider/images/icon/copy.svg | 5 +- .../ImagesProvider/images/icon/dao.svg | 7 + .../ImagesProvider/images/icon/dots.svg | 5 + .../ImagesProvider/images/icon/earth.svg | 6 +- .../ImagesProvider/images/icon/edit.svg | 5 + .../ImagesProvider/images/icon/email.svg | 3 + .../ImagesProvider/images/icon/filter.svg | 3 + .../ImagesProvider/images/icon/gas.svg | 8 +- .../ImagesProvider/images/icon/gear.svg | 4 +- .../ImagesProvider/images/icon/home.svg | 4 +- .../ImagesProvider/images/icon/info.svg | 4 +- .../ImagesProvider/images/icon/lamp.svg | 4 + .../ImagesProvider/images/icon/link.svg | 8 +- .../ImagesProvider/images/icon/loader.svg | 18 +- .../ImagesProvider/images/icon/lock.svg | 3 + .../ImagesProvider/images/icon/longArrow.svg | 4 + .../ImagesProvider/images/icon/metaVault.svg | 7 + .../ImagesProvider/images/icon/minus.svg | 3 + .../ImagesProvider/images/icon/money.svg | 9 +- .../images/icon/notification.svg | 6 + .../ImagesProvider/images/icon/percent.svg | 5 + .../ImagesProvider/images/icon/plus.svg | 5 +- .../ImagesProvider/images/icon/plusThin.svg | 1 + .../ImagesProvider/images/icon/question.svg | 4 - .../ImagesProvider/images/icon/rewards.svg | 6 + .../ImagesProvider/images/icon/scrollUp.svg | 5 +- .../ImagesProvider/images/icon/search.svg | 6 +- .../ImagesProvider/images/icon/send.svg | 4 + .../ImagesProvider/images/icon/share.svg | 5 + .../ImagesProvider/images/icon/shield.svg | 5 + .../ImagesProvider/images/icon/storage.svg | 3 + .../ImagesProvider/images/icon/support.svg | 7 + .../ImagesProvider/images/icon/theme.svg | 14 +- .../ImagesProvider/images/icon/token.svg | 3 + .../ImagesProvider/images/icon/trash.svg | 5 + .../ImagesProvider/images/icon/undo.svg | 1 + .../ImagesProvider/images/icon/up.svg | 3 - .../ImagesProvider/images/icon/upload.svg | 8 +- .../ImagesProvider/images/icon/user.svg | 3 + .../ImagesProvider/images/icon/users.svg | 6 + .../ImagesProvider/images/icon/validators.svg | 3 + .../images/icon/vaultDeposit.svg | 10 + .../ImagesProvider/images/icon/warning.svg | 6 +- .../ImagesProvider/images/image/cancel.svg | 1 + .../ImagesProvider/images/image/error.svg | 20 +- .../ImagesProvider/images/image/file.svg | 23 ++ .../images/image/information.svg | 20 +- .../ImagesProvider/images/image/party.svg | 13 + .../ImagesProvider/images/image/progress.svg | 1 + .../ImagesProvider/images/image/success.svg | 20 +- .../ImagesProvider/images/image/warning.svg | 13 + src/components/ImagesProvider/images/index.ts | 142 +++++-- .../ImagesProvider/images/language/de.svg | 27 +- .../ImagesProvider/images/language/en.svg | 35 +- .../ImagesProvider/images/language/es.svg | 370 ++++++++++++++++-- .../ImagesProvider/images/language/fr.svg | 27 +- .../ImagesProvider/images/language/pt.svg | 141 ++++++- .../ImagesProvider/images/language/ru.svg | 27 +- .../ImagesProvider/images/language/zh.svg | 30 +- .../images/network/arbitrum.svg | 16 + .../ImagesProvider/images/social/discord.svg | 3 + .../images/social/discordLine.svg | 3 + .../ImagesProvider/images/social/facebook.svg | 3 + .../images/social/facebookLine.svg | 3 + .../ImagesProvider/images/social/telegram.svg | 3 + .../images/social/telegramLine.svg | 4 + .../ImagesProvider/images/social/twitter.svg | 3 + .../images/social/twitterLine.svg | 5 + .../ImagesProvider/images/theme/moon.svg | 242 +----------- .../ImagesProvider/images/theme/sun.svg | 125 +----- .../ImagesProvider/images/token/ETH-dark.svg | 15 + .../ImagesProvider/images/token/GNO-dark.svg | 6 + src/components/Input/Input.tsx | 59 ++- .../Input/InputButton/InputButton.tsx | 4 +- .../Input/InputView/InputView.module.scss | 17 +- src/components/Input/InputView/InputView.tsx | 157 +++++--- src/components/Logo/Logo.tsx | 31 +- src/components/Modal/Content/Content.tsx | 4 +- src/components/Modal/Content/Title/Title.tsx | 2 +- src/components/Modal/Modal.module.scss | 24 +- src/components/Modal/Modal.tsx | 30 +- src/components/Modal/util/useOverlayClick.ts | 4 +- .../Notification/Notification.module.scss | 36 +- src/components/Notification/Notification.tsx | 9 +- src/components/PopupInfo/PopupInfo.tsx | 6 +- src/components/Select/Select.tsx | 2 +- .../Select/SelectButton/SelectButton.tsx | 15 +- .../Select/SelectView/SelectView.tsx | 6 +- .../SelectWithLabelButton.tsx | 33 +- src/components/TabsView/TabsView.tsx | 12 +- src/components/Text/Text.tsx | 46 +-- .../TextWithTooltip/TextWithTooltip.tsx | 45 +-- src/components/Token/Token.tsx | 3 +- src/components/TokenAmount/TokenAmount.tsx | 12 +- .../TokenAmountInput/Balance/Balance.tsx | 63 --- .../TokenAmountInput/Balance/messages.ts | 20 - .../TokenAmountInput/Input/Input.tsx | 69 ---- .../TokenAmountInput/Token/Token.tsx | 38 -- .../TokenAmountInput/TokenAmountInput.tsx | 6 +- .../TokenAmountInputView/Balance/Balance.tsx | 65 +-- .../TokenAmountInputView/Balance/messages.ts | 2 +- .../TokenAmountInputView.tsx | 58 ++- src/components/Tooltip/Tooltip.tsx | 2 +- .../TransactionView/TransactionView.tsx | 8 +- .../Transactions/TransactionView/messages.ts | 2 +- .../Transactions/Transactions.module.scss | 2 +- src/components/Transactions/util/index.ts | 2 +- src/components/Transactions/util/useAction.ts | 2 +- 181 files changed, 2698 insertions(+), 1808 deletions(-) delete mode 100644 src/components/ButtonRangePicker/ButtonRangePicker.module.scss create mode 100644 src/components/Chart/ExtraRewards/ExtraRewards.tsx create mode 100644 src/components/Chart/ExtraRewards/messages.ts delete mode 100644 src/components/Chart/util/useSeries.ts delete mode 100644 src/components/Chart/util/useSettings.ts rename src/components/DatePicker/Calendar/{Day/Day.module.scss => Cell/Cell.module.scss} (100%) rename src/components/DatePicker/Calendar/{Day/Day.tsx => Cell/Cell.tsx} (73%) rename src/components/DatePicker/Calendar/{Calendar.module.scss => Days/Days.module.scss} (100%) create mode 100644 src/components/DatePicker/Calendar/Days/Days.tsx create mode 100644 src/components/DatePicker/Calendar/Header/Header.tsx create mode 100644 src/components/DatePicker/Calendar/Months/Months.tsx create mode 100644 src/components/DatePicker/Calendar/Years/Years.tsx delete mode 100644 src/components/ImagesProvider/images/connector/MMI.svg delete mode 100644 src/components/ImagesProvider/images/connector/braveBrowser.svg delete mode 100644 src/components/ImagesProvider/images/connector/metamask.svg delete mode 100644 src/components/ImagesProvider/images/connector/okx.svg delete mode 100644 src/components/ImagesProvider/images/connector/portis.svg delete mode 100644 src/components/ImagesProvider/images/connector/rabby.svg delete mode 100644 src/components/ImagesProvider/images/connector/taho.svg delete mode 100644 src/components/ImagesProvider/images/connector/trustWallet.svg delete mode 100644 src/components/ImagesProvider/images/connector/zengo.svg create mode 100644 src/components/ImagesProvider/images/icon/actions.svg create mode 100644 src/components/ImagesProvider/images/icon/arrow.svg create mode 100644 src/components/ImagesProvider/images/icon/book.svg create mode 100644 src/components/ImagesProvider/images/icon/caret.svg create mode 100644 src/components/ImagesProvider/images/icon/chats.svg create mode 100644 src/components/ImagesProvider/images/icon/collapse.svg create mode 100644 src/components/ImagesProvider/images/icon/control.svg create mode 100644 src/components/ImagesProvider/images/icon/dao.svg create mode 100644 src/components/ImagesProvider/images/icon/dots.svg create mode 100644 src/components/ImagesProvider/images/icon/edit.svg create mode 100644 src/components/ImagesProvider/images/icon/email.svg create mode 100644 src/components/ImagesProvider/images/icon/filter.svg create mode 100644 src/components/ImagesProvider/images/icon/lamp.svg create mode 100644 src/components/ImagesProvider/images/icon/lock.svg create mode 100644 src/components/ImagesProvider/images/icon/longArrow.svg create mode 100644 src/components/ImagesProvider/images/icon/metaVault.svg create mode 100644 src/components/ImagesProvider/images/icon/minus.svg create mode 100644 src/components/ImagesProvider/images/icon/notification.svg create mode 100644 src/components/ImagesProvider/images/icon/percent.svg create mode 100644 src/components/ImagesProvider/images/icon/plusThin.svg delete mode 100644 src/components/ImagesProvider/images/icon/question.svg create mode 100644 src/components/ImagesProvider/images/icon/rewards.svg create mode 100644 src/components/ImagesProvider/images/icon/send.svg create mode 100644 src/components/ImagesProvider/images/icon/share.svg create mode 100644 src/components/ImagesProvider/images/icon/shield.svg create mode 100644 src/components/ImagesProvider/images/icon/storage.svg create mode 100644 src/components/ImagesProvider/images/icon/support.svg create mode 100644 src/components/ImagesProvider/images/icon/token.svg create mode 100644 src/components/ImagesProvider/images/icon/trash.svg create mode 100644 src/components/ImagesProvider/images/icon/undo.svg delete mode 100644 src/components/ImagesProvider/images/icon/up.svg create mode 100644 src/components/ImagesProvider/images/icon/user.svg create mode 100644 src/components/ImagesProvider/images/icon/users.svg create mode 100644 src/components/ImagesProvider/images/icon/validators.svg create mode 100644 src/components/ImagesProvider/images/icon/vaultDeposit.svg create mode 100644 src/components/ImagesProvider/images/image/cancel.svg create mode 100644 src/components/ImagesProvider/images/image/file.svg create mode 100644 src/components/ImagesProvider/images/image/party.svg create mode 100644 src/components/ImagesProvider/images/image/progress.svg create mode 100644 src/components/ImagesProvider/images/image/warning.svg create mode 100644 src/components/ImagesProvider/images/network/arbitrum.svg create mode 100644 src/components/ImagesProvider/images/social/discord.svg create mode 100644 src/components/ImagesProvider/images/social/discordLine.svg create mode 100644 src/components/ImagesProvider/images/social/facebook.svg create mode 100644 src/components/ImagesProvider/images/social/facebookLine.svg create mode 100644 src/components/ImagesProvider/images/social/telegram.svg create mode 100644 src/components/ImagesProvider/images/social/telegramLine.svg create mode 100644 src/components/ImagesProvider/images/social/twitter.svg create mode 100644 src/components/ImagesProvider/images/social/twitterLine.svg create mode 100644 src/components/ImagesProvider/images/token/ETH-dark.svg create mode 100644 src/components/ImagesProvider/images/token/GNO-dark.svg delete mode 100644 src/components/TokenAmountInput/Balance/Balance.tsx delete mode 100644 src/components/TokenAmountInput/Balance/messages.ts delete mode 100644 src/components/TokenAmountInput/Input/Input.tsx delete mode 100644 src/components/TokenAmountInput/Token/Token.tsx diff --git a/src/components/AmountInput/AmountInput.tsx b/src/components/AmountInput/AmountInput.tsx index 53dcf60d..35a42089 100644 --- a/src/components/AmountInput/AmountInput.tsx +++ b/src/components/AmountInput/AmountInput.tsx @@ -35,7 +35,7 @@ const AmountInput: React.FC = (props) => { return ( = (props) => { const childrenRef = useRef(null) const containerRef = useRef(null) - useIsomorphicLayoutEffect(() => { - const isInit = containerRef.current && contentRef.current && childrenRef.current - - if (!isInit) { + const updateHeight = useCallback(() => { + if (!containerRef.current || !childrenRef.current) { return } - if (isOpen) { - const contentHeight = contentRef.current?.clientHeight as number - const containerHeight = childrenRef.current?.clientHeight as number - const marginTop = parseFloat(getComputedStyle(contentRef.current as HTMLDivElement).marginTop) + if (isOpen && contentRef.current) { + const contentHeight = contentRef.current.clientHeight + const childrenHeight = childrenRef.current.clientHeight + const marginTop = parseFloat(getComputedStyle(contentRef.current).marginTop) - if (containerRef.current) { - containerRef.current.style.height = `calc(${padding * 2}px + ${contentHeight + containerHeight + marginTop}px)` - } + containerRef.current.style.height = `calc(${padding * 2}px + ${contentHeight + childrenHeight + marginTop}px)` } else { - const height = childrenRef.current?.clientHeight + const height = childrenRef.current.clientHeight - if (containerRef.current) { - containerRef.current.style.height = `calc(${padding * 2}px + ${height}px)` - } + containerRef.current.style.height = `calc(${padding * 2}px + ${height}px)` } + }, [ isOpen, padding ]) + + useIsomorphicLayoutEffect(() => { + updateHeight() }, [ isOpen, toggleContent ]) + useEventListener('resize', updateHeight) + const containerClassName = cx(className, s.container, `p-${padding} relative`) const toggleContentClassName = cx(contentClassName, s.content, { diff --git a/src/components/Bone/Bone.tsx b/src/components/Bone/Bone.tsx index 494d6254..eb7088ef 100644 --- a/src/components/Bone/Bone.tsx +++ b/src/components/Bone/Bone.tsx @@ -47,26 +47,34 @@ const Bone: React.FC = (props: any) => { } }, [ aspect ]) - const boneClassName = cx(s.bone, s[`delay-${delay}`], className, { - 'relative': !/absolute/.test(className || ''), - 'rounded-4': !/rounded/.test(className || ''), + const textSizeClassName = cx({ [`text-${textSize}`]: textSize, 'w-full': wFull, }) + const boneClassName = cx(s.bone, s[`delay-${delay}`], className, textSizeClassName, { + 'relative': !/absolute/.test(className || ''), + 'rounded-4': !/rounded/.test(className || ''), + }) + if (textSize) { return (
+ > +
+
) } return (
{ diff --git a/src/components/ButtonBase/ButtonBase.module.scss b/src/components/ButtonBase/ButtonBase.module.scss index 3ff5c249..3f774bee 100644 --- a/src/components/ButtonBase/ButtonBase.module.scss +++ b/src/components/ButtonBase/ButtonBase.module.scss @@ -1,6 +1,5 @@ .wrapper { @include no-user-select; - @include keyboard-focus; &[disabled] { pointer-events: none; diff --git a/src/components/ButtonBase/ButtonBase.tsx b/src/components/ButtonBase/ButtonBase.tsx index b1852e97..b6d5edc2 100644 --- a/src/components/ButtonBase/ButtonBase.tsx +++ b/src/components/ButtonBase/ButtonBase.tsx @@ -6,6 +6,8 @@ import s from './ButtonBase.module.scss' import intl from 'modules/intl' +type ButtonElement = HTMLAnchorElement | HTMLButtonElement + export type ButtonBaseProps = { children?: React.ReactNode className?: string @@ -21,8 +23,9 @@ export type ButtonBaseProps = { disabled?: boolean dataTestId?: string ariaLabel?: string | Intl.Message - ref?: React.RefObject - onClick?: React.MouseEventHandler + ref?: React.RefObject + onKeyDown?: React.KeyboardEventHandler + onClick?: React.MouseEventHandler } const ButtonBase: React.FC = (props) => { @@ -45,7 +48,7 @@ const ButtonBase: React.FC = (props) => { ? translations.formatMessage(ariaLabel, ariaLabel.values) : ariaLabel - const handleClick = useCallback((event: React.MouseEvent) => { + const handleClick = useCallback((event: React.MouseEvent) => { if (disabled) { return } diff --git a/src/components/ButtonRangePicker/ButtonRangePicker.module.scss b/src/components/ButtonRangePicker/ButtonRangePicker.module.scss deleted file mode 100644 index 3f5466fb..00000000 --- a/src/components/ButtonRangePicker/ButtonRangePicker.module.scss +++ /dev/null @@ -1,3 +0,0 @@ -.rounded { - border-radius: 11px; -} diff --git a/src/components/ButtonRangePicker/ButtonRangePicker.tsx b/src/components/ButtonRangePicker/ButtonRangePicker.tsx index 1b996bf8..6bdcbea9 100644 --- a/src/components/ButtonRangePicker/ButtonRangePicker.tsx +++ b/src/components/ButtonRangePicker/ButtonRangePicker.tsx @@ -1,13 +1,13 @@ import React, { useMemo } from 'react' -import cx from 'classnames' import { useTabButton } from 'hooks' +import theme from 'modules/theme' import forms from 'modules/forms' +import cx from 'classnames' import Text from '../Text/Text' import ButtonBase from '../ButtonBase/ButtonBase' import type { SelectProps } from '../Select/Select' -import s from './ButtonRangePicker.module.scss' import messages from './messages' @@ -21,6 +21,7 @@ export type ButtonRangePickerProps = { const ButtonRangePicker: React.FC = (props) => { const { className, field, dataTestId, range } = props + const { isDark } = theme.useData() const { value } = forms.useFieldValue(field) const activeIndex = useMemo(() => { @@ -37,7 +38,7 @@ const ButtonRangePicker: React.FC = (props) => { className={cx( className, 'relative', - 'flex rounded-12 border border-secondary/10' + 'flex rounded-16 border border-dark/10' )} data-testid={dataTestId} > @@ -45,29 +46,28 @@ const ButtonRangePicker: React.FC = (props) => { range.map((item, index) => ( field.setValue(item.value)} > )) }
) diff --git a/src/components/ButtonRangePicker/messages.ts b/src/components/ButtonRangePicker/messages.ts index 1d06d1a6..d4ffda23 100644 --- a/src/components/ButtonRangePicker/messages.ts +++ b/src/components/ButtonRangePicker/messages.ts @@ -5,7 +5,7 @@ export default { fr: 'Bouton de sélection de plage', es: 'Botón selector de rango', pt: 'Botão de seleção de intervalo', - de: 'Bereichauswahltaste', + de: 'Bereichsauswahl', zh: '范围选择按钮', }, } diff --git a/src/components/Chart/Chart.tsx b/src/components/Chart/Chart.tsx index fee2fb30..30ec32fb 100644 --- a/src/components/Chart/Chart.tsx +++ b/src/components/Chart/Chart.tsx @@ -4,6 +4,7 @@ import cx from 'classnames' import Legend from './Legend/Legend' import NoItems from './NoItems/NoItems' import Skeleton from './Skeleton/Skeleton' +import ExtraRewards from './ExtraRewards/ExtraRewards' import NotConnected from './NotConnected/NotConnected' import { useChart, Views } from './util' @@ -21,6 +22,7 @@ export type ChartProps = { isNotConnected?: boolean hideRightScale?: boolean showLegendOnHover?: boolean + showExtraRewards?: boolean pointType: Charts.PointType noItemsDescription?: Intl.Message expandSettings?: Charts.ExpandSettings @@ -28,7 +30,18 @@ export type ChartProps = { } const Chart: React.FC = (props) => { - const { className, token, height, pointType, dataTestId, legendClassName, noItemsDescription, connect } = props + const { + className, + data, + token, + height, + pointType, + dataTestId, + legendClassName, + showExtraRewards, + noItemsDescription, + connect, + } = props const { view, @@ -67,15 +80,25 @@ const Chart: React.FC = (props) => { } { view === Views.Legend && ( -
- -
+ ) : ( +
+ +
+ ) ) }
diff --git a/src/components/Chart/ExtraRewards/ExtraRewards.tsx b/src/components/Chart/ExtraRewards/ExtraRewards.tsx new file mode 100644 index 00000000..8caf3078 --- /dev/null +++ b/src/components/Chart/ExtraRewards/ExtraRewards.tsx @@ -0,0 +1,330 @@ +import React, { useEffect, useRef, RefObject, useMemo } from 'react' +import type { MouseEventParams } from 'lightweight-charts' +import { createSelector, useSelector } from 'store' +import { methods } from 'helpers' +import theme from 'modules/theme' +import date from 'modules/date' +import cx from 'classnames' + +import Icon from '../../Icon/Icon' +import Text from '../../Text/Text' +import Logo from '../../Logo/Logo' + +import messages from './messages' + + +type ExtraRewardsProps = { + className?: string + token: Tokens + data: Charts.MainData + series?: Charts.Series + chart: Charts.Chart | null + container: RefObject +} + +type RewardsData = { + message: Intl.Message + rowRef: React.RefObject + fiatRef: React.RefObject + amountRef: React.RefObject +} + +const storeSelector = createSelector([ + (store: Store) => store.fiatRates.data, + (store: Store) => store.currency.selected, + (store: Store) => store.currency.symbol, +], (fiatRates, currency, currencySymbol) => ({ + currencySymbol, + fiatRates, + currency, +})) + +const ExtraRewards: React.FC = (props) => { + const { token, data, chart, series, container, className } = props + + const { isDark } = theme.useData() + + const { fiatRates, currency, currencySymbol } = useSelector(storeSelector) + + const tooltipRef = useRef(null) + + const boostRowRef = useRef(null) + const boostAmountRef = useRef(null) + const boostFiatRef = useRef(null) + + const stakeRowRef = useRef(null) + const stakeAmountRef = useRef(null) + const stakeFiatRef = useRef(null) + + const totalRowRef = useRef(null) + const totalAmountRef = useRef(null) + const totalFiatRef = useRef(null) + + const timeRef = useRef(null) + + const tooltipClassName = cx( + ` + hidden absolute w-[235px] overflow-hidden rounded-8 shadow-coal + border border-dark/10 backdrop-blur-[30px] z-modal pointer-events-none + `, + { + 'bg-white/70': !isDark, + 'bg-dark/80': isDark, + }, + className + ) + + useEffect(() => { + const containerElement = container.current + const tooltipElement = tooltipRef.current + + if (!containerElement || !chart || !tooltipElement) { + return + } + + const formatFiatAmount = (value: number) => { + const rate = fiatRates[token][currency] + const result = Number((rate * value).toFixed(2)) + const formattedResult = methods.numericalReduction(Math.abs(result)) + + const prefix = result < 0 ? '-' : '' + + const fiatAmount = formattedResult === '0.00' + ? `< ${prefix}${currencySymbol} 0.01` + : `${prefix}${currencySymbol}${formattedResult}` + + return `(${fiatAmount})` + } + + const updateTooltipContent = (extra: Charts.ExtraData) => { + const rows: Array<{ + amountRef: React.RefObject + fiatRef: React.RefObject + rowRef: React.RefObject + value: number + }> = [] + + if (!extra.stakeRewards && !extra.boostRewards) { + tooltipRef?.current?.classList.add('hidden!') + + return + } + else { + tooltipRef?.current?.classList.remove('hidden!') + } + + if (extra.stakeRewards) { + stakeRowRef.current?.classList.remove('hidden!') + rows.push({ amountRef: stakeAmountRef, fiatRef: stakeFiatRef, value: extra.stakeRewards, rowRef: stakeRowRef }) + } + else { + stakeRowRef.current?.classList.add('hidden!') + } + + if (extra.boostRewards) { + boostRowRef.current?.classList.remove('hidden!') + totalRowRef.current?.classList.remove('hidden!') + + rows.push( + { amountRef: boostAmountRef, fiatRef: boostFiatRef, value: extra.boostRewards, rowRef: boostRowRef }, + { amountRef: totalAmountRef, fiatRef: totalFiatRef, value: extra.stakeRewards + extra.boostRewards, rowRef: totalRowRef } + ) + } + else { + boostRowRef.current?.classList.add('hidden!') + totalRowRef.current?.classList.add('hidden!') + } + + for (const { amountRef, fiatRef, value, rowRef } of rows) { + if (rowRef.current) { + rowRef.current.style.display = value ? 'flex' : 'none' + } + + if (amountRef?.current && value) { + amountRef.current.innerHTML = methods.formatTokenValue(String(value), true) + } + + if (fiatRef?.current && value) { + fiatRef.current.innerHTML = formatFiatAmount(Number(value)) + } + } + + if (timeRef.current) { + timeRef.current.innerHTML = date + .time(Number(extra.time) * 1000) + .utcOffset(0, false) + .format('DD MMM YYYY') + } + } + + const hideTooltip = () => { + tooltipElement.style.display = 'none' + } + + const positionTooltip = (x: number, y: number) => { + const rect = containerElement.getBoundingClientRect() + + const width = tooltipElement.offsetWidth + const height = tooltipElement.offsetHeight + + const offset = 12 + + let left = x + offset + + if (left > rect.width - width) { + left = x - offset - width + } + + let top = y + offset + + if (top > rect.height - height) { + top = y - offset - height + } + + tooltipElement.style.top = `${top}px` + tooltipElement.style.display = 'block' + tooltipElement.style.left = `${left}px` + } + + const findExtraDataByTime = (params: MouseEventParams): Charts.ExtraData | null => { + const { time } = params + + for (const block of data) { + const currentPoint = block.data.find(pt => pt?.extraData && pt.time === time) + + if (currentPoint?.extraData) { + return currentPoint.extraData + } + } + + return null + } + + const onCrosshairMove = (params: MouseEventParams) => { + const pointerPoint = params?.point + + if (!pointerPoint) { + return hideTooltip() + } + + const extraData = findExtraDataByTime(params) + + if (!extraData || !series) { + return hideTooltip() + } + + const data = series.data() + + const point: Charts.Point = params?.time + ? params.seriesData.get(series as any) + : data[data.length - 1] + + updateTooltipContent({ ...extraData, time: point.time }) + + tooltipElement.style.display = 'block' + positionTooltip(pointerPoint.x, pointerPoint.y) + } + + chart.subscribeCrosshairMove(onCrosshairMove) + + const onMouseLeave = () => hideTooltip() + containerElement.addEventListener('mouseleave', onMouseLeave) + + return () => { + chart.unsubscribeCrosshairMove(onCrosshairMove) + containerElement.removeEventListener('mouseleave', onMouseLeave) + } + }, [ data, chart, container, tooltipClassName, fiatRates, token, series, currency, currencySymbol ]) + + const rewardsData: RewardsData[] = useMemo(() => { + return [ + { + rowRef: boostRowRef, + fiatRef: boostFiatRef, + message: messages.boost, + amountRef: boostAmountRef, + }, + { + rowRef: stakeRowRef, + fiatRef: stakeFiatRef, + message: messages.stake, + amountRef: stakeAmountRef, + }, + { + rowRef: totalRowRef, + fiatRef: totalFiatRef, + message: messages.total, + amountRef: totalAmountRef, + }, + ] + }, []) + + return ( +
+
+ +
+ + +
+
+ { + rewardsData.map(({ message, fiatRef, amountRef, rowRef }, index) => ( +
+ +
+
+ + + +
+
+
+ )) + } +
+ ) +} + + +export default React.memo(ExtraRewards) diff --git a/src/components/Chart/ExtraRewards/messages.ts b/src/components/Chart/ExtraRewards/messages.ts new file mode 100644 index 00000000..6a4f2926 --- /dev/null +++ b/src/components/Chart/ExtraRewards/messages.ts @@ -0,0 +1,38 @@ +export default { + boost: { + en: 'Boost', + ru: 'Буст', + fr: 'Boost', + es: 'Boost', + pt: 'Boost', + de: 'Boost', + zh: 'Boost', + }, + stake: { + en: 'Stake', + ru: 'Стейкинг', + fr: 'Staking', + es: 'Staking', + pt: 'Staking', + de: 'Staking', + zh: '质押', + }, + total: { + en: 'Total', + ru: 'Всего', + fr: 'Total', + es: 'Total', + pt: 'Total', + de: 'Gesamt', + zh: '总计', + }, + rewards: { + en: 'Your rewards', + ru: 'Ваши награды', + fr: 'Vos récompenses', + es: 'Sus recompensas', + pt: 'Suas recompensas', + de: 'Ihre Belohnungen', + zh: '您的奖励', + }, +} diff --git a/src/components/Chart/Legend/Amount/Amount.tsx b/src/components/Chart/Legend/Amount/Amount.tsx index 69dc8d01..4ff978ed 100644 --- a/src/components/Chart/Legend/Amount/Amount.tsx +++ b/src/components/Chart/Legend/Amount/Amount.tsx @@ -1,7 +1,6 @@ import React, { useRef, useCallback } from 'react' -import { createSelector } from '@reduxjs/toolkit' +import { createSelector, useSelector } from 'store' import { methods } from 'helpers' -import { useStore } from 'hooks' import cx from 'classnames' import Text from '../../../Text/Text' @@ -29,7 +28,7 @@ const storeSelector = createSelector([ const Amount: React.FC = (props) => { const { className, token, chart, series } = props - const { fiatRates, currency, currencySymbol } = useStore(storeSelector) + const { fiatRates, currency, currencySymbol } = useSelector(storeSelector) const fiatTooltipRef = useRef(null) const tokenValueTooltipRef = useRef(null) @@ -41,14 +40,20 @@ const Amount: React.FC = (props) => { } if (fiatTooltipRef.current) { + if (!point.value) { + fiatTooltipRef.current.innerHTML = `(${currencySymbol} 0.00)` + + return + } + const rate = fiatRates[token][currency] const result = Number((rate * Number(point.value)).toFixed(2)) - const perfix = result < 0 ? '-' : '' + const prefix = result < 0 ? '-' : '' const formattedResult = methods.numericalReduction(Math.abs(result)) const fiatAmount = formattedResult === '0.00' - ? `< ${perfix}${currencySymbol} 0.01` - : `${perfix}${currencySymbol}${formattedResult}` + ? `< ${prefix}${currencySymbol} 0.01` + : `${prefix}${currencySymbol}${formattedResult}` fiatTooltipRef.current.innerHTML = `(${fiatAmount})` } @@ -58,19 +63,20 @@ const Amount: React.FC = (props) => { return (
- +
) diff --git a/src/components/Chart/Legend/Legend.tsx b/src/components/Chart/Legend/Legend.tsx index a1f5b366..cf1a36cb 100644 --- a/src/components/Chart/Legend/Legend.tsx +++ b/src/components/Chart/Legend/Legend.tsx @@ -52,12 +52,12 @@ const Legend: React.FC = (props) => { { Boolean(options?.legendTitle) && ( ) } diff --git a/src/components/Chart/Legend/Percentage/Percentage.tsx b/src/components/Chart/Legend/Percentage/Percentage.tsx index 5442d2cb..96587a2f 100644 --- a/src/components/Chart/Legend/Percentage/Percentage.tsx +++ b/src/components/Chart/Legend/Percentage/Percentage.tsx @@ -30,9 +30,9 @@ const Percentage: React.FC = (props) => { return ( ) diff --git a/src/components/Chart/Legend/Time/Time.tsx b/src/components/Chart/Legend/Time/Time.tsx index 25a98424..f48bff89 100644 --- a/src/components/Chart/Legend/Time/Time.tsx +++ b/src/components/Chart/Legend/Time/Time.tsx @@ -1,5 +1,4 @@ import React, { useRef, useCallback } from 'react' -import type { Time } from 'lightweight-charts' import date from 'modules/date' import cx from 'classnames' @@ -25,6 +24,7 @@ const Time: React.FC = (props) => { if (timeTooltipRef.current) { timeTooltipRef.current.innerHTML = date .time(Number(time) * 1000) + .utcOffset(0, false) .format('DD MMM YYYY') } }, []) @@ -42,10 +42,10 @@ const Time: React.FC = (props) => { />
) diff --git a/src/components/Chart/NoItems/NoItems.tsx b/src/components/Chart/NoItems/NoItems.tsx index 93889ae1..9f4b15ab 100644 --- a/src/components/Chart/NoItems/NoItems.tsx +++ b/src/components/Chart/NoItems/NoItems.tsx @@ -17,17 +17,18 @@ const NoItems: React.FC = (props) => {
{ description && ( ) } diff --git a/src/components/Chart/Skeleton/Skeleton.tsx b/src/components/Chart/Skeleton/Skeleton.tsx index db9ec367..594b51c9 100644 --- a/src/components/Chart/Skeleton/Skeleton.tsx +++ b/src/components/Chart/Skeleton/Skeleton.tsx @@ -1,5 +1,5 @@ import React from 'react' -import device from 'modules/device' +import { device } from 'sw-utils/modules' import Bone from '../../Bone/Bone' @@ -10,15 +10,15 @@ type SkeletonProps = { const Skeleton: React.FC = ({ className }) => { const { isDesktop } = device.useData() - const columns = isDesktop ? 6 : 4 + const columns = !isDesktop ? 4 : 6 return (
- +
- +
{ diff --git a/src/components/Chart/util/global.d.ts b/src/components/Chart/util/global.d.ts index 047af803..c43753a2 100644 --- a/src/components/Chart/util/global.d.ts +++ b/src/components/Chart/util/global.d.ts @@ -23,16 +23,30 @@ declare global { legendLine?: string } + type ExtraData = { + boostRewards: number + stakeRewards: number + time?: number | Time + } + type TimePoint = { time: Time value: number + extraData?: ExtraData } type Point = { + extraData?: ExtraData time: Time | number value: number } + type CacheData = { + apy: Point[] + balance: Point[] + rewards: Point[] + } + type Style = 'bar' | 'line' type PointType = 'fiat' | 'percent' diff --git a/src/components/Chart/util/useCreateChart.ts b/src/components/Chart/util/useCreateChart.ts index c8d3f935..d0e33062 100644 --- a/src/components/Chart/util/useCreateChart.ts +++ b/src/components/Chart/util/useCreateChart.ts @@ -45,7 +45,7 @@ const useCreateChart = (input: Input): State => { const chart = createChart(container.current as HTMLElement, params) if (data.length > 1 && style === 'bar') { - throw new Error("The chart cannot have more than one data set to work with a column view") + throw new Error('The chart cannot have more than one data set to work with a column view') } const dataArr = data.map(({ data, options }) => { diff --git a/src/components/Chart/util/useSeries.ts b/src/components/Chart/util/useSeries.ts deleted file mode 100644 index 012d6f51..00000000 --- a/src/components/Chart/util/useSeries.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { useCallback, useMemo } from 'react' -import theme, { ThemeColor } from 'modules/theme' -import { LineStyle } from 'lightweight-charts' -import type { AreaSeriesPartialOptions, HistogramSeriesPartialOptions, CreatePriceLineOptions, LineWidth } from 'lightweight-charts' - - -type Input = { - style: Charts.Style -} - -type Output = { - defaultSeries: AreaSeriesPartialOptions - getPriceLineSeries: (price: number) => CreatePriceLineOptions -} - -const getDefaultSeriesOptions = (themeValue: ThemeColor): AreaSeriesPartialOptions => ({ - lineWidth: 2, - topColor: themeValue === ThemeColor.Light ? '#d6e6ff24' : '#232123ba', - lineColor: themeValue === ThemeColor.Light ? '#3559e4' : '#627eea', - bottomColor: themeValue === ThemeColor.Light ? '#74aafc91' : '#74aafc78', - lastValueVisible: false, - priceLineVisible: false, -}) - -const getHistogramSeriesOptions = (themeValue: ThemeColor): HistogramSeriesPartialOptions => ({ - color: themeValue === ThemeColor.Light ? '#74a2fc99' : '#617ee9e0', - priceLineVisible: false, - lastValueVisible: false, - priceFormat: { - precision: 5, - type: 'price', - minMove: 0.00001, - }, -}) - -const useSeries = (input: Input): Output => { - const { style } = input - - const { themeValue } = theme.useData() - - const isLight = themeValue === ThemeColor.Light - - const getPriceLineSeries = useCallback((price: number) => ({ - price, - axisLabelVisible: true, - lineWidth: 1 as LineWidth, - lineStyle: LineStyle.LargeDashed, - color: isLight ? '#6b6b94' : '#787887', - }), [ isLight ]) - - return useMemo(() => ({ - defaultSeries: style === 'line' - ? getDefaultSeriesOptions(themeValue) - : getHistogramSeriesOptions(themeValue), - getPriceLineSeries, - }), [ style, themeValue, getPriceLineSeries ]) -} - - -export default useSeries diff --git a/src/components/Chart/util/useSettings.ts b/src/components/Chart/util/useSettings.ts deleted file mode 100644 index c6e313eb..00000000 --- a/src/components/Chart/util/useSettings.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { useMemo } from 'react' -import intl from 'modules/intl' -import theme, { ThemeColor } from 'modules/theme' - -import type { DeepPartial, ChartOptionsBase } from 'lightweight-charts' -import { LineStyle, LineWidth, CrosshairMode } from 'lightweight-charts' - - -type Input = { - style: Charts.Style - hideRightPriceScale?: boolean - formatLabel: (value: number) => string | number -} - -const useSettings = (input: Input) => { - const { style, hideRightPriceScale, formatLabel } = input - - const { themeValue } = theme.useData() - const intlControls = intl.useIntl() - - const isLight = themeValue === ThemeColor.Light - - const darkColor = isLight ? '#00060F' : '#FFFFFF' - const gridColor = isLight ? '#00040e26' : '#FFFFFF12' - const darkOpacityColor = isLight ? '#00040e0d' : '#FFFFFF12' - - return useMemo>(() => { - return ({ - autoSize: true, - localization: { - locale: intlControls.locale, - priceFormatter: (value: number) => formatLabel(value), - }, - handleScale: { - axisPressedMouseMove: false, - }, - handleScroll: { - vertTouchDrag: false, - horzTouchDrag: false, - }, - grid: { - vertLines: { - visible: false, - }, - horzLines: { - color: gridColor, - style: LineStyle.SparseDotted, - }, - }, - layout: { - attributionLogo: false, - textColor: darkColor, - background: { color: 'transparent' }, - }, - timeScale: { - timeVisible: true, - fixLeftEdge: true, - fixRightEdge: true, - ticksVisible: false, - borderColor: darkOpacityColor, - }, - rightPriceScale: { - autoScale: false, - entireTextOnly: true, - visible: !hideRightPriceScale, - borderColor: darkOpacityColor, - scaleMargins: { - top: style === 'line' ? 0.25 : 0.2, - bottom: style === 'line' ? 0.1 : 0, - }, - }, - crosshair: { - horzLine: { - visible: true, - labelVisible: false, - width: 1 as LineWidth, - style: LineStyle.Solid, - color: darkOpacityColor, - }, - mode: CrosshairMode.Magnet, - vertLine: { - visible: true, - labelVisible: false, - width: 2 as LineWidth, - style: LineStyle.Solid, - color: darkOpacityColor, - }, - }, - }) - }, [ gridColor, darkColor, darkOpacityColor, style, hideRightPriceScale, intlControls.locale, formatLabel ]) -} - - -export default useSettings diff --git a/src/components/Counter/Counter.module.scss b/src/components/Counter/Counter.module.scss index 82edbed8..895a583f 100644 --- a/src/components/Counter/Counter.module.scss +++ b/src/components/Counter/Counter.module.scss @@ -2,25 +2,10 @@ min-width: 20px; height: 20px; - @include dark-theme { - color: $color-dark; - background-color: rgba($color-dark-rgb, .2); - - :global(.bg-inherit) { - background-color: $color-dark; - } - } - - @include light-theme { - color: $color-primary; - background-color: rgba($color-primary-rgb, .1); - - :global(.bg-inherit) { - background-color: $color-primary; - } - } + color: $color-dark; + background-color: rgba($color-white-rgb, .4); &.disabled { - background-color: rgba($color-secondary-rgb, .2); + opacity: 0.3; } } diff --git a/src/components/Counter/Counter.tsx b/src/components/Counter/Counter.tsx index 6faaaa95..0d547dcb 100644 --- a/src/components/Counter/Counter.tsx +++ b/src/components/Counter/Counter.tsx @@ -18,7 +18,6 @@ export type CounterProps = { const Counter: React.FC = (props) => { const { className, count, disabled = false, ref, isFetching } = props - const textColor = disabled ? 'secondary' : 'inherit' const counterClassName = cx(className, s.counter, 'px-6 rounded-24 inline-flex items-center justify-center', { [s.disabled]: disabled, }) @@ -33,14 +32,15 @@ const Counter: React.FC = (props) => { ) : ( ) diff --git a/src/components/DatePicker/Calendar/Calendar.tsx b/src/components/DatePicker/Calendar/Calendar.tsx index 9008561f..2b6ce497 100644 --- a/src/components/DatePicker/Calendar/Calendar.tsx +++ b/src/components/DatePicker/Calendar/Calendar.tsx @@ -1,18 +1,15 @@ -import React, { useMemo } from 'react' +import React, { useCallback, useMemo, useState } from 'react' import forms from 'modules/forms' import date from 'modules/date' import cx from 'classnames' -import Bone from '../../Bone/Bone' -import Icon from '../../Icon/Icon' -import Text from '../../Text/Text' -import ButtonBase from '../../ButtonBase/ButtonBase' +import Days from './Days/Days' +import Years from './Years/Years' +import Months from './Months/Months' +import Header from './Header/Header' -import Day from './Day/Day' import { useDays, getDate } from './util' -import s from './Calendar.module.scss' - export type CalendarProps = { className?: string @@ -22,158 +19,149 @@ export type CalendarProps = { onChange?: () => void } +type Level = 'month' | 'year' | 'decade' + const Calendar: React.FC = (props) => { const { className, field, minDate, maxDate, onChange } = props + const [ level, setLevel ] = useState('month') + const { value: selectedDay } = forms.useFieldValue(field) - const { days, title, weekDays, yearMonth, weeksOffset, weeksCount, isLocaleFetching, setMonth } = useDays({ - field, - animationDuration: 350, - }) + + const { + days, title, year, months, years, weekDays, yearMonth, isLocaleFetching, + decadeStart, yearTitle, decadeTitle, setMonth, setYear, selectMonth, selectYear, + } = useDays({ field }) const minimalDate = useMemo(() => getDate(minDate), [ minDate ]) const maximalDate = useMemo(() => getDate(maxDate), [ maxDate ]) + const selectedDate = useMemo(() => getDate(selectedDay), [ selectedDay ]) - const [ firstDayOfMonth, lastDayOfMonth ] = useMemo(() => { + const [ rangeStart, rangeEnd ] = useMemo(() => { const yearMonthDate = date.time(yearMonth, 'YYYY-MM') - return [ - yearMonthDate.startOf('month'), - yearMonthDate.endOf('month'), - ] - }, [ yearMonth ]) + if (level === 'year') { + return [ yearMonthDate.startOf('year'), yearMonthDate.endOf('year') ] + } + + if (level === 'decade') { + return [ + yearMonthDate.set('year', decadeStart).startOf('year'), + yearMonthDate.set('year', decadeStart + 9).endOf('year'), + ] + } + + return [ yearMonthDate.startOf('month'), yearMonthDate.endOf('month') ] + }, [ level, yearMonth, decadeStart ]) + + const isLeftButtonDisabled = useMemo(() => ( + minimalDate ? !rangeStart.isAfter(minimalDate) : false + ), [ rangeStart, minimalDate ]) + + const isRightButtonDisabled = useMemo(() => ( + maximalDate ? !rangeEnd.isBefore(maximalDate) : false + ), [ rangeEnd, maximalDate ]) + + const headerTitle = { + month: title, + year: yearTitle, + decade: decadeTitle, + }[level] - const isLeftButtonDisabled = useMemo(() => { - if (minimalDate) { - return !firstDayOfMonth.isAfter(minimalDate) + const handlePrev = useCallback(() => { + if (level === 'month') { + setMonth(-1) + + return } - }, [ firstDayOfMonth, minimalDate ]) - const isRightButtonDisabled = useMemo(() => { - if (maximalDate) { - return !lastDayOfMonth.isBefore(maximalDate) + setYear(level === 'decade' ? -10 : -1) + }, [ level, setMonth, setYear ]) + + const handleNext = useCallback(() => { + if (level === 'month') { + setMonth(1) + + return } - }, [ lastDayOfMonth, maximalDate ]) + + setYear(level === 'decade' ? 10 : 1) + }, [ level, setMonth, setYear ]) + + const handleTitleClick = useCallback(() => { + setLevel(level === 'month' ? 'year' : 'decade') + }, [ level ]) + + const handleSelectDay = useCallback((fullDate: string) => { + field.setValue(fullDate) + + if (typeof onChange === 'function') { + onChange() + } + }, [ field, onChange ]) + + const handleSelectMonth = useCallback((month: number) => { + selectMonth(month) + setLevel('month') + }, [ selectMonth ]) + + const handleSelectYear = useCallback((value: number) => { + selectYear(value) + setLevel('year') + }, [ selectYear ]) return (
-
- setMonth(-1)} - > - + { + level === 'month' && ( + + ) + } + { + level === 'year' && ( + - - { - isLocaleFetching ? ( -
- -
- ) : ( - - ) - } - setMonth(1)} - > - - -
-
- { - weekDays.map((weekDay) => ( - isLocaleFetching ? ( -
- -
- ) : ( - - ) - )) - } -
-
-
0 ? `-${weeksOffset * 32}px` : '0', - }} - > - { - days.map((day) => { - const dayDate = getDate(day.fullDate) as Date.Time - - return ( - { - field.setValue(day.fullDate) - - if (typeof onChange === 'function') { - onChange() - } - }} - /> - ) - }) - } -
-
+ ) + }
) } diff --git a/src/components/DatePicker/Calendar/Day/Day.module.scss b/src/components/DatePicker/Calendar/Cell/Cell.module.scss similarity index 100% rename from src/components/DatePicker/Calendar/Day/Day.module.scss rename to src/components/DatePicker/Calendar/Cell/Cell.module.scss diff --git a/src/components/DatePicker/Calendar/Day/Day.tsx b/src/components/DatePicker/Calendar/Cell/Cell.tsx similarity index 73% rename from src/components/DatePicker/Calendar/Day/Day.tsx rename to src/components/DatePicker/Calendar/Cell/Cell.tsx index 553fd5e9..3ea65a32 100644 --- a/src/components/DatePicker/Calendar/Day/Day.tsx +++ b/src/components/DatePicker/Calendar/Cell/Cell.tsx @@ -1,13 +1,13 @@ import React from 'react' import cx from 'classnames' -import ButtonBase from '../../../ButtonBase/ButtonBase' import Text from '../../../Text/Text' +import ButtonBase from '../../../ButtonBase/ButtonBase' -import s from './Day.module.scss' +import s from './Cell.module.scss' -type DayProps = { +type CellProps = { className?: string title: string isActive?: boolean @@ -16,7 +16,7 @@ type DayProps = { onClick: () => void } -const Day: React.FC = (props) => { +const Cell: React.FC = (props) => { const { className, title, isActive, isSelected, isDisabled, onClick } = props const defaultClassName = cx({ @@ -29,7 +29,7 @@ const Day: React.FC = (props) => { return ( = (props) => { > ) } -export default React.memo(Day) +export default React.memo(Cell) diff --git a/src/components/DatePicker/Calendar/Calendar.module.scss b/src/components/DatePicker/Calendar/Days/Days.module.scss similarity index 100% rename from src/components/DatePicker/Calendar/Calendar.module.scss rename to src/components/DatePicker/Calendar/Days/Days.module.scss diff --git a/src/components/DatePicker/Calendar/Days/Days.tsx b/src/components/DatePicker/Calendar/Days/Days.tsx new file mode 100644 index 00000000..d8aebae4 --- /dev/null +++ b/src/components/DatePicker/Calendar/Days/Days.tsx @@ -0,0 +1,88 @@ +import React from 'react' +import cx from 'classnames' + +import Text from '../../../Text/Text' +import Bone from '../../../Bone/Bone' + +import Cell from '../Cell/Cell' +import { getDate } from '../util' + +import s from './Days.module.scss' + + +type DayItem = { + date: string + fullDate: string + yearMonth: string +} + +type DaysProps = { + days: DayItem[] + weekDays: string[] + yearMonth: string + selectedDay?: string + isLocaleFetching: boolean + minimalDate: Date.Time | null + maximalDate: Date.Time | null + onSelect: (fullDate: string) => void +} + +const Days: React.FC = (props) => { + const { days, weekDays, yearMonth, selectedDay, isLocaleFetching, minimalDate, maximalDate, onSelect } = props + + return ( + <> +
+ { + weekDays.map((weekDay) => ( + isLocaleFetching ? ( +
+ +
+ ) : ( + + ) + )) + } +
+
+ { + days.map((day) => { + const dayDate = getDate(day.fullDate) as Date.Time + + return ( + onSelect(day.fullDate)} + /> + ) + }) + } +
+ + ) +} + + +export default React.memo(Days) diff --git a/src/components/DatePicker/Calendar/Header/Header.tsx b/src/components/DatePicker/Calendar/Header/Header.tsx new file mode 100644 index 00000000..91b3d280 --- /dev/null +++ b/src/components/DatePicker/Calendar/Header/Header.tsx @@ -0,0 +1,83 @@ +import React from 'react' +import cx from 'classnames' + +import Icon from '../../../Icon/Icon' +import Text from '../../../Text/Text' +import Bone from '../../../Bone/Bone' +import ButtonBase from '../../../ButtonBase/ButtonBase' + + +type HeaderProps = { + title: string + isLocaleFetching: boolean + isTitleDisabled: boolean + isLeftDisabled?: boolean + isRightDisabled?: boolean + onPrev: () => void + onNext: () => void + onTitleClick: () => void +} + +const Header: React.FC = (props) => { + const { title, isLocaleFetching, isTitleDisabled, isLeftDisabled, isRightDisabled, onPrev, onNext, onTitleClick } = props + + return ( +
+ + + + { + isLocaleFetching ? ( +
+ +
+ ) : ( + + + + ) + } + + + +
+ ) +} + + +export default React.memo(Header) diff --git a/src/components/DatePicker/Calendar/Months/Months.tsx b/src/components/DatePicker/Calendar/Months/Months.tsx new file mode 100644 index 00000000..e224f989 --- /dev/null +++ b/src/components/DatePicker/Calendar/Months/Months.tsx @@ -0,0 +1,57 @@ +import React from 'react' +import date from 'modules/date' + +import Cell from '../Cell/Cell' + + +type MonthItem = { + month: number + title: string +} + +type MonthsProps = { + year: number + months: MonthItem[] + yearMonth: string + selectedDate: Date.Time | null + minimalDate: Date.Time | null + maximalDate: Date.Time | null + onSelect: (month: number) => void +} + +const Months: React.FC = (props) => { + const { year, months, yearMonth, selectedDate, minimalDate, maximalDate, onSelect } = props + + const base = date.time(yearMonth, 'YYYY-MM') + + return ( +
+ { + months.map(({ month, title }) => { + const monthDate = base.set('month', month) + + const isDisabled = ( + minimalDate?.isAfter(monthDate.endOf('month')) + || maximalDate?.isBefore(monthDate.startOf('month')) + ) + const isSelected = selectedDate?.year() === year && selectedDate?.month() === month + + return ( + onSelect(month)} + /> + ) + }) + } +
+ ) +} + + +export default React.memo(Months) diff --git a/src/components/DatePicker/Calendar/Years/Years.tsx b/src/components/DatePicker/Calendar/Years/Years.tsx new file mode 100644 index 00000000..d90314f5 --- /dev/null +++ b/src/components/DatePicker/Calendar/Years/Years.tsx @@ -0,0 +1,51 @@ +import React from 'react' +import date from 'modules/date' + +import Cell from '../Cell/Cell' + + +type YearsProps = { + years: number[] + yearMonth: string + decadeStart: number + selectedDate: Date.Time | null + minimalDate: Date.Time | null + maximalDate: Date.Time | null + onSelect: (year: number) => void +} + +const Years: React.FC = (props) => { + const { years, yearMonth, decadeStart, selectedDate, minimalDate, maximalDate, onSelect } = props + + const base = date.time(yearMonth, 'YYYY-MM') + + return ( +
+ { + years.map((value) => { + const yearDate = base.set('year', value) + + const isDisabled = ( + minimalDate?.isAfter(yearDate.endOf('year')) + || maximalDate?.isBefore(yearDate.startOf('year')) + ) + + return ( + = decadeStart && value <= decadeStart + 9} + isSelected={selectedDate?.year() === value} + isDisabled={isDisabled} + onClick={() => onSelect(value)} + /> + ) + }) + } +
+ ) +} + + +export default React.memo(Years) diff --git a/src/components/DatePicker/Calendar/util/useDays.ts b/src/components/DatePicker/Calendar/util/useDays.ts index d6abe537..507972eb 100644 --- a/src/components/DatePicker/Calendar/util/useDays.ts +++ b/src/components/DatePicker/Calendar/util/useDays.ts @@ -1,9 +1,13 @@ import { useCallback, useMemo } from 'react' import intl from 'modules/intl' import date from 'modules/date' -import { useChangeEffect, useFieldListener, useObjectState } from 'hooks' +import { useFieldListener, useObjectState } from 'hooks' +const format = 'YYYY-MM' + +const weeksInGrid = 6 + type GetCalendarRangeInput = { firstDay: Date.Time lastDay: Date.Time @@ -11,12 +15,9 @@ type GetCalendarRangeInput = { type Input = { field: Forms.Field - animationDuration: number } -const format = 'YYYY-MM' - -const useDays = ({ field, animationDuration }: Input) => { +const useDays = ({ field }: Input) => { const time = date.useTime() const { locale } = intl.useIntl() @@ -35,7 +36,7 @@ const useDays = ({ field, animationDuration }: Input) => { return result }, [ field ]) - const getStateFromField = useCallback((initialDate?: string) => { + const getYearMonthFromField = useCallback((initialDate?: string) => { const { year, month } = parseFieldDate() let currentDate = initialDate ? time(initialDate, format) : time() @@ -44,21 +45,17 @@ const useDays = ({ field, animationDuration }: Input) => { currentDate = currentDate.set('year', year) } if (month) { - // in day js months are zero-based currentDate = currentDate.set('month', month - 1) } - const yearMonth = currentDate.format(format) - const initialYearMonth = yearMonth - - return { yearMonth, initialYearMonth } + return currentDate.format(format) }, [ time, parseFieldDate ]) - const [ { yearMonth, initialYearMonth }, setState ] = useObjectState(getStateFromField()) + const [ { yearMonth }, setState ] = useObjectState({ yearMonth: getYearMonthFromField() }) const onFieldChange = useCallback(() => { - setState({ yearMonth: getStateFromField(yearMonth).yearMonth }) - }, [ yearMonth, setState, getStateFromField ]) + setState({ yearMonth: getYearMonthFromField(yearMonth) }) + }, [ yearMonth, setState, getYearMonthFromField ]) useFieldListener(field, onFieldChange) @@ -72,6 +69,28 @@ const useDays = ({ field, animationDuration }: Input) => { }) }, [ yearMonth, time, setState ]) + const setYear = useCallback((count: number) => { + const nextYearMonth = time(yearMonth, format) + .add(count, 'year') + .format(format) + + setState({ + yearMonth: nextYearMonth, + }) + }, [ yearMonth, time, setState ]) + + const selectMonth = useCallback((month: number) => { + setState({ + yearMonth: time(yearMonth, format).set('month', month).format(format), + }) + }, [ yearMonth, time, setState ]) + + const selectYear = useCallback((value: number) => { + setState({ + yearMonth: time(yearMonth, format).set('year', value).format(format), + }) + }, [ yearMonth, time, setState ]) + const weekDays = useMemo(() => { return new Array(7) .fill(null) @@ -81,15 +100,11 @@ const useDays = ({ field, animationDuration }: Input) => { }, [ time ]) const getCalendarRange = useCallback((yearMonth: string) => { - const selectedMonth = time(yearMonth, format) - const startOfMonth = selectedMonth.startOf('month') - const endOfMonth = selectedMonth.endOf('month') - + const startOfMonth = time(yearMonth, format).startOf('month') const startDiff = weekDays.indexOf(startOfMonth.format('dd')) - const endDiff = 6 - weekDays.indexOf(endOfMonth.format('dd')) const firstDay = startOfMonth.subtract(startDiff, 'day') - const lastDay = endOfMonth.add(endDiff, 'day') + const lastDay = firstDay.add(weeksInGrid * 7 - 1, 'day') return { firstDay, @@ -97,14 +112,6 @@ const useDays = ({ field, animationDuration }: Input) => { } }, [ weekDays, time ]) - const monthRange = useMemo(() => { - return getCalendarRange(yearMonth) - }, [ yearMonth, getCalendarRange ]) - - const initialMonthRange = useMemo(() => { - return getCalendarRange(initialYearMonth) - }, [ initialYearMonth, getCalendarRange ]) - const getDays = useCallback(({ firstDay, lastDay }: GetCalendarRangeInput) => { return new Array(lastDay.diff(firstDay, 'day') + 1) .fill(null) @@ -122,59 +129,66 @@ const useDays = ({ field, animationDuration }: Input) => { }, []) const days = useMemo(() => { - const daysDiff = monthRange.firstDay.diff(initialMonthRange.firstDay, 'day') + return getDays(getCalendarRange(yearMonth)) + }, [ yearMonth, getCalendarRange, getDays ]) - const firstDay = daysDiff > 0 - ? initialMonthRange.firstDay - : monthRange.firstDay + const title = useMemo(() => time(yearMonth, format).format('MMMM, YYYY'), [ yearMonth, time ]) - const lastDay = daysDiff > 0 - ? monthRange.lastDay - : initialMonthRange.lastDay + const current = useMemo(() => time(yearMonth, format), [ yearMonth, time ]) - return getDays({ firstDay, lastDay }) - }, [ monthRange, initialMonthRange, getDays ]) + const year = useMemo(() => current.year(), [ current ]) - const weeksCount = useMemo(() => { - return monthRange.lastDay.diff(monthRange.firstDay, 'week') + 1 - }, [ monthRange ]) + const decadeStart = useMemo(() => Math.floor(year / 10) * 10, [ year ]) - const weeksOffset = useMemo(() => { - return initialMonthRange.firstDay.diff(monthRange.firstDay, 'week') - }, [ monthRange, initialMonthRange ]) + const months = useMemo(() => ( + new Array(12).fill(null).map((_, index) => ({ + month: index, + title: current.set('month', index).format('MMM'), + })) + ), [ current ]) - const title = useMemo(() => time(yearMonth, format).format('MMMM, YYYY'), [ yearMonth, time ]) + const years = useMemo(() => ( + new Array(12).fill(null).map((_, index) => decadeStart - 1 + index) + ), [ decadeStart ]) - const isLocaleFetching = useMemo(() => locale !== time().locale(), [ time, locale ]) + const yearTitle = useMemo(() => String(year), [ year ]) - useChangeEffect<[ string, number ]>(() => { - const timeout = setTimeout(() => { - setState({ initialYearMonth: yearMonth }) - }, animationDuration) + const decadeTitle = useMemo(() => `${decadeStart} – ${decadeStart + 9}`, [ decadeStart ]) - return () => { - clearTimeout(timeout) - } - }, [ yearMonth, animationDuration ]) + const isLocaleFetching = useMemo(() => locale !== time().locale(), [ time, locale ]) return useMemo(() => ({ days, + year, title, + years, + months, weekDays, yearMonth, - weeksCount, - weeksOffset, + yearTitle, + decadeStart, + decadeTitle, isLocaleFetching, + setYear, setMonth, + selectYear, + selectMonth, }), [ + year, days, title, + years, + months, weekDays, + yearTitle, yearMonth, - weeksCount, - weeksOffset, + decadeStart, + decadeTitle, isLocaleFetching, + setYear, setMonth, + selectYear, + selectMonth, ]) } diff --git a/src/components/DatePicker/DatePicker.tsx b/src/components/DatePicker/DatePicker.tsx index e82d112a..ee86128e 100644 --- a/src/components/DatePicker/DatePicker.tsx +++ b/src/components/DatePicker/DatePicker.tsx @@ -1,22 +1,27 @@ import React, { Fragment } from 'react' import cx from 'classnames' +import theme from 'modules/theme' import { offset } from '@floating-ui/react' +import type { Placement } from '@floating-ui/react' import { autoUpdate, useFloating } from '@floating-ui/react-dom' import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react' -import Input from '../Input/Input' -import type { InputProps } from '../Input/Input' +import Input, { InputProps } from '../Input/Input' import Calendar, { CalendarProps } from './Calendar/Calendar' -export type DatePickerProps = Omit & Omit +export type DatePickerProps = Omit & Omit & { + placement?: Placement +} const DatePicker: React.FC = (props) => { - const { className, field, minDate, maxDate, ...rest } = props + const { className, field, minDate, maxDate, placement = 'bottom-start', ...rest } = props + + const { isDark } = theme.useData() const { refs, floatingStyles } = useFloating({ - placement: 'bottom-start', + placement, middleware: [ offset(10), ], @@ -48,7 +53,9 @@ const DatePicker: React.FC = (props) => { = (props) => { {...rest} > { - ({ selected }) => ( -