Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
"dayjs": "1.11.13",
"destyle.css": "4.0.1",
"ethers": "6.14.3",
"exceljs": "4.4.0",
"fast-deep-equal": "3.1.3",
"focus-visible": "5.2.1",
"husky": "9.1.7",
Expand All @@ -117,8 +118,7 @@
"react-redux": "9.2.0",
"swiper": "11.2.8",
"ua-parser-js": "2.0.3",
"viem": "2.44.1",
"xlsx": "0.18.5"
"viem": "2.44.1"
},
"pnpm": {
"overrides": {
Expand Down
507 changes: 447 additions & 60 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

270 changes: 129 additions & 141 deletions src/components/DatePicker/Calendar/Calendar.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -22,158 +19,149 @@ export type CalendarProps = {
onChange?: () => void
}

type Level = 'month' | 'year' | 'decade'

const Calendar: React.FC<CalendarProps> = (props) => {
const { className, field, minDate, maxDate, onChange } = props

const [ level, setLevel ] = useState<Level>('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 (
<div className={cx(className, 'w-[224px]')}>
<div className="flex items-center">
<ButtonBase
className={cx('rounded-4', {
'opacity-10': isLeftButtonDisabled,
})}
disabled={isLeftButtonDisabled}
onClick={() => setMonth(-1)}
>
<Icon
name="arrow/left"
size={20}
color="dark"
<Header
title={headerTitle}
isLocaleFetching={isLocaleFetching}
isTitleDisabled={level === 'decade'}
isLeftDisabled={isLeftButtonDisabled}
isRightDisabled={isRightButtonDisabled}
onPrev={handlePrev}
onNext={handleNext}
onTitleClick={handleTitleClick}
/>
{
level === 'month' && (
<Days
days={days}
weekDays={weekDays}
yearMonth={yearMonth}
selectedDay={selectedDay}
isLocaleFetching={isLocaleFetching}
minimalDate={minimalDate}
maximalDate={maximalDate}
onSelect={handleSelectDay}
/>
)
}
{
level === 'year' && (
<Months
year={year}
months={months}
yearMonth={yearMonth}
selectedDate={selectedDate}
minimalDate={minimalDate}
maximalDate={maximalDate}
onSelect={handleSelectMonth}
/>
</ButtonBase>
{
isLocaleFetching ? (
<div className="flex-1 flex items-center justify-center">
<Bone
className="rounded-4"
w={126}
h={24}
/>
</div>
) : (
<Text
className="flex-1 text-center capitalize"
message={title}
size="t16m"
color="dark"
/>
)
}
<ButtonBase
className={cx('rounded-4', {
'opacity-10': isRightButtonDisabled,
})}
disabled={isRightButtonDisabled}
onClick={() => setMonth(1)}
>
<Icon
name="arrow/right"
size={20}
color="dark"
)
}
{
level === 'decade' && (
<Years
years={years}
yearMonth={yearMonth}
decadeStart={decadeStart}
selectedDate={selectedDate}
minimalDate={minimalDate}
maximalDate={maximalDate}
onSelect={handleSelectYear}
/>
</ButtonBase>
</div>
<div className={cx('grid text-center opacity-80 mt-8 capitalize', s.grid)}>
{
weekDays.map((weekDay) => (
isLocaleFetching ? (
<div
key={weekDay}
className="flex items-center justify-center"
>
<Bone
className="rounded-4"
w={20}
h={20}
delay={1}
/>
</div>
) : (
<Text
key={weekDay}
className="py-4"
message={weekDay}
size="t16m"
color="primary"
/>
)
))
}
</div>
<div
className={cx('relative transition-height duration-350', {
'overflow-hidden': weeksOffset !== 0,
})}
style={{
height: `${weeksCount * 32}px`,
}}
>
<div
className={cx('grid text-center', s.grid, {
'transition-transform duration-350': weeksOffset,
})}
style={{
transform: `translateY(${weeksOffset * 32}px)`,
marginTop: weeksOffset > 0 ? `-${weeksOffset * 32}px` : '0',
}}
>
{
days.map((day) => {
const dayDate = getDate(day.fullDate) as Date.Time

return (
<Day
key={day.fullDate}
title={day.date}
isActive={day.yearMonth === yearMonth}
isDisabled={(
minimalDate?.isAfter(dayDate.endOf('day'))
|| maximalDate?.isBefore(dayDate.startOf('day'))
)}
isSelected={day.fullDate === selectedDay}
onClick={() => {
field.setValue(day.fullDate)

if (typeof onChange === 'function') {
onChange()
}
}}
/>
)
})
}
</div>
</div>
)
}
</div>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import cx from 'classnames'
import ButtonBase from '../../../ButtonBase/ButtonBase'
import Text from '../../../Text/Text'

import s from './Day.module.scss'
import s from './Cell.module.scss'


type DayProps = {
type CellProps = {
className?: string
title: string
isActive?: boolean
Expand All @@ -16,7 +16,7 @@ type DayProps = {
onClick: () => void
}

const Day: React.FC<DayProps> = (props) => {
const Cell: React.FC<CellProps> = (props) => {
const { className, title, isActive, isSelected, isDisabled, onClick } = props

const defaultClassName = cx({
Expand All @@ -29,7 +29,7 @@ const Day: React.FC<DayProps> = (props) => {

return (
<ButtonBase
className={cx(className, 'transition-opacity duration-500 rounded-4 py-4', {
className={cx(className, 'transition-colors rounded-4 py-4', {
[defaultClassName]: !isDisabled,
'opacity-10': isDisabled,
})}
Expand All @@ -46,4 +46,4 @@ const Day: React.FC<DayProps> = (props) => {
}


export default React.memo(Day)
export default React.memo(Cell)
Loading
Loading