diff --git a/src/widgets/CalendarWidget/index.js b/src/widgets/CalendarWidget/index.js new file mode 100644 index 0000000..548da58 --- /dev/null +++ b/src/widgets/CalendarWidget/index.js @@ -0,0 +1 @@ +export { default as CalendarWidget } from './ui/CalendarWidget.ui'; diff --git a/src/widgets/CalendarWidget/ui/CalendarWidget.module.css b/src/widgets/CalendarWidget/ui/CalendarWidget.module.css new file mode 100644 index 0000000..9d367e1 --- /dev/null +++ b/src/widgets/CalendarWidget/ui/CalendarWidget.module.css @@ -0,0 +1,4 @@ +.wrapper { + border: 1px solid black; + width: 372px; +} diff --git a/src/widgets/CalendarWidget/ui/CalendarWidget.ui.jsx b/src/widgets/CalendarWidget/ui/CalendarWidget.ui.jsx new file mode 100644 index 0000000..92fa43a --- /dev/null +++ b/src/widgets/CalendarWidget/ui/CalendarWidget.ui.jsx @@ -0,0 +1,27 @@ +import PropTypes from 'prop-types'; + +import classes from './CalendarWidget.module.css'; +import { CalendarWidgetProvider } from './CalendarWidgetContext'; +import { DateWrapper } from './components/DateWrapper'; +import { DayOfWeekRow } from './components/DayOfWeekRow'; +import { HeaderRow } from './components/HeaderRow'; + +const CalendarWidget = (props) => ( + +
+ + + + + +
+
+); + +CalendarWidget.propTypes = { + maxDate: PropTypes.instanceOf(Date), + minDate: PropTypes.instanceOf(Date), + value: PropTypes.instanceOf(Date), +}; + +export default CalendarWidget; diff --git a/src/widgets/CalendarWidget/ui/CalendarWidgetContext.jsx b/src/widgets/CalendarWidget/ui/CalendarWidgetContext.jsx new file mode 100644 index 0000000..92ae75c --- /dev/null +++ b/src/widgets/CalendarWidget/ui/CalendarWidgetContext.jsx @@ -0,0 +1,41 @@ +import PropTypes from 'prop-types'; +import { createContext, useMemo, useState } from 'react'; + +export const CalendarWidgetContext = createContext({}); + +export const CalendarWidgetProvider = (props) => { + const { + children, + maxDate = new Date(2030, 0, 1), + minDate = new Date(1990, 0, 1), + locale = 'ru', + ...restProps + } = props; + + const [targetDate, setTargetDate] = useState(new Date()); + + const contextValue = useMemo(() => ({ + targetYear: targetDate.getFullYear(), + targetMonthIndex: targetDate.getMonth(), + targetDate, + setTargetDate, + minDate, + maxDate, + locale, + ...restProps, + }), [maxDate, minDate, targetDate, locale, restProps]); + + return ( + + {children} + + ); +}; + +CalendarWidgetProvider.propTypes = { + children: PropTypes.node.isRequired, + + maxDate: PropTypes.instanceOf(Date), + minDate: PropTypes.instanceOf(Date), + locale: PropTypes.oneOf(['ru', 'en']), +}; diff --git a/src/widgets/CalendarWidget/ui/components/DateCell/index.js b/src/widgets/CalendarWidget/ui/components/DateCell/index.js new file mode 100644 index 0000000..ab0a98b --- /dev/null +++ b/src/widgets/CalendarWidget/ui/components/DateCell/index.js @@ -0,0 +1 @@ +export { default as DateCell } from './ui/DateCell.ui'; diff --git a/src/widgets/CalendarWidget/ui/components/DateCell/ui/DateCell.spec.jsx b/src/widgets/CalendarWidget/ui/components/DateCell/ui/DateCell.spec.jsx new file mode 100644 index 0000000..ac52c45 --- /dev/null +++ b/src/widgets/CalendarWidget/ui/components/DateCell/ui/DateCell.spec.jsx @@ -0,0 +1,37 @@ +import { render, screen } from '@testing-library/react'; + +import DateCell from './DateCell.ui'; + +describe('Base Render', () => { + const setup = (date) => { + render(); + }; + + test('Check base render', () => { + setup(new Date(2024, 3, 15)); + + const dateCellElem = screen.getByTestId('date-cell'); + expect(dateCellElem).toBeInTheDocument(); + }); + + test('Check base class', () => { + setup(new Date(2024, 3, 15)); + + const dateCellElem = screen.getByTestId('date-cell'); + expect(dateCellElem).toHaveClass('dateCell'); + }); + + test('Is correct date', () => { + setup(new Date(2024, 3, 15)); + + const dateCellElem = screen.getByTestId('date-cell'); + expect(dateCellElem).toHaveTextContent('15'); + }); + + test('Have data-date attribute', () => { + setup(new Date(2024, 3, 15)); + + const dateCellElem = screen.getByTestId('date-cell'); + expect(dateCellElem).toHaveAttribute('data-date', '4/15/2024'); + }); +}); diff --git a/src/widgets/CalendarWidget/ui/components/DateCell/ui/DateCell.style.module.css b/src/widgets/CalendarWidget/ui/components/DateCell/ui/DateCell.style.module.css new file mode 100644 index 0000000..33002eb --- /dev/null +++ b/src/widgets/CalendarWidget/ui/components/DateCell/ui/DateCell.style.module.css @@ -0,0 +1,28 @@ +.dateCell { + width: calc((100% / 7)); + height: 40px; + background: aquamarine; + display: flex; + justify-content: center; + align-items: center; + + &:nth-child(odd):not(:last-child) { + width: calc((100% / 7) - 1px); + border-right: 1px solid black; + } + + &:nth-child(even) { + width: calc((100% / 7) - 1px); + border-right: 1px solid black; + } + + &.today { + color: red; + font-size: 1.2em; + font-weight: bold; + } + + &.outMothRange { + color: darkgrey; + } +} diff --git a/src/widgets/CalendarWidget/ui/components/DateCell/ui/DateCell.ui.jsx b/src/widgets/CalendarWidget/ui/components/DateCell/ui/DateCell.ui.jsx new file mode 100644 index 0000000..26d5b2a --- /dev/null +++ b/src/widgets/CalendarWidget/ui/components/DateCell/ui/DateCell.ui.jsx @@ -0,0 +1,44 @@ +import classnames from 'classnames'; +import PropTypes from 'prop-types'; +import { useContext } from 'react'; + +import { CalendarWidgetContext } from '../../../CalendarWidgetContext'; + +import classes from './DateCell.style.module.css'; + +const DateCell = (props) => { + const { + date, + } = props; + const format = 'en-En'; + + const { targetMonthIndex } = useContext(CalendarWidgetContext); + + const todayDate = new Date(); + const dataDate = new Intl.DateTimeFormat(format).format(date); + const dataToday = new Intl.DateTimeFormat(format).format(todayDate); + const isOutMonthRange = date.getMonth() !== targetMonthIndex; + + return ( +
+ {date.getDate()} +
+ ); +}; + +DateCell.propTypes = { + date: PropTypes.instanceOf(Date).isRequired, +}; + +export default DateCell; diff --git a/src/widgets/CalendarWidget/ui/components/DateRow/index.js b/src/widgets/CalendarWidget/ui/components/DateRow/index.js new file mode 100644 index 0000000..f2df3ce --- /dev/null +++ b/src/widgets/CalendarWidget/ui/components/DateRow/index.js @@ -0,0 +1 @@ +export { default as DateRow } from './ui/DateRow.ui'; diff --git a/src/widgets/CalendarWidget/ui/components/DateRow/ui/DateRow.spec.jsx b/src/widgets/CalendarWidget/ui/components/DateRow/ui/DateRow.spec.jsx new file mode 100644 index 0000000..7371937 --- /dev/null +++ b/src/widgets/CalendarWidget/ui/components/DateRow/ui/DateRow.spec.jsx @@ -0,0 +1,65 @@ +import { render, screen } from '@testing-library/react'; + +import DateRow from './DateRow.ui'; + +describe('Test Date Row', () => { + const setup = (cells) => { + render(); + }; + + const setupWeek = () => { + const cells = [ + new Date(2024, 3, 15), + new Date(2024, 3, 16), + new Date(2024, 3, 17), + new Date(2024, 3, 18), + new Date(2024, 3, 19), + new Date(2024, 3, 20), + new Date(2024, 3, 21), + ]; + setup(cells); + }; + + test('base render', () => { + setupWeek(); + + const dateRowElem = screen.getByTestId('date-row'); + expect(dateRowElem).toBeInTheDocument(); + }); + + test('Correct data attribute', () => { + setupWeek(); + + const dateRowElem = screen.getByTestId('date-row'); + expect(dateRowElem).toHaveAttribute('data-row', '4/15/2024-4/21/2024'); + }); + + test('Correct style class', () => { + setupWeek(); + + const dateRowElem = screen.getByTestId('date-row'); + expect(dateRowElem).toHaveClass('dateRow'); + }); + + describe('Test cells', () => { + test('base render', () => { + setupWeek(); + + const dateCellElems = screen.getAllByTestId('date-cell'); + expect(dateCellElems).toHaveLength(7); + }); + + test('is correct cells', () => { + setupWeek(); + + const dateCellElems = screen.getAllByTestId('date-cell'); + expect(dateCellElems[0]).toHaveTextContent(15); + expect(dateCellElems[1]).toHaveTextContent(16); + expect(dateCellElems[2]).toHaveTextContent(17); + expect(dateCellElems[3]).toHaveTextContent(18); + expect(dateCellElems[4]).toHaveTextContent(19); + expect(dateCellElems[5]).toHaveTextContent(20); + expect(dateCellElems[6]).toHaveTextContent(21); + }); + }); +}); diff --git a/src/widgets/CalendarWidget/ui/components/DateRow/ui/DateRow.style.module.css b/src/widgets/CalendarWidget/ui/components/DateRow/ui/DateRow.style.module.css new file mode 100644 index 0000000..7e1bb92 --- /dev/null +++ b/src/widgets/CalendarWidget/ui/components/DateRow/ui/DateRow.style.module.css @@ -0,0 +1,10 @@ +.dateRow { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + + &:not(:last-child) { + border-bottom: 1px solid black; + } +} diff --git a/src/widgets/CalendarWidget/ui/components/DateRow/ui/DateRow.ui.jsx b/src/widgets/CalendarWidget/ui/components/DateRow/ui/DateRow.ui.jsx new file mode 100644 index 0000000..7c6acc2 --- /dev/null +++ b/src/widgets/CalendarWidget/ui/components/DateRow/ui/DateRow.ui.jsx @@ -0,0 +1,35 @@ +import PropTypes from 'prop-types'; + +import { DateCell } from '../../DateCell'; + +import classes from './DateRow.style.module.css'; + +const DateRow = (props) => { + const { + cells, + } = props; + + const format = 'en-En'; + + const firstDate = new Intl.DateTimeFormat(format).format(cells[0]); + const lastDate = new Intl.DateTimeFormat(format).format(cells[cells.length - 1]); + + return ( +
+ {cells.map((item) => ( + + ))} +
+ ); +}; + +DateRow.propTypes = { + cells: PropTypes.arrayOf(PropTypes.instanceOf(Date)), +}; + +export default DateRow; diff --git a/src/widgets/CalendarWidget/ui/components/DateWrapper/index.js b/src/widgets/CalendarWidget/ui/components/DateWrapper/index.js new file mode 100644 index 0000000..e38158d --- /dev/null +++ b/src/widgets/CalendarWidget/ui/components/DateWrapper/index.js @@ -0,0 +1 @@ +export { default as DateWrapper } from './ui/DateWrapper.ui'; diff --git a/src/widgets/CalendarWidget/ui/components/DateWrapper/ui/DateWrapper.spec.jsx b/src/widgets/CalendarWidget/ui/components/DateWrapper/ui/DateWrapper.spec.jsx new file mode 100644 index 0000000..8dc3855 --- /dev/null +++ b/src/widgets/CalendarWidget/ui/components/DateWrapper/ui/DateWrapper.spec.jsx @@ -0,0 +1,24 @@ +import { render, screen } from '@testing-library/react'; + +import { CalendarWidgetProvider } from 'widgets/CalendarWidget/ui/CalendarWidgetContext'; + +import { withProvider } from 'shared/hoc/withProvider'; + +import DateWrapper from './DateWrapper.ui'; + +describe('Base render DateWrapper', () => { + const setup = () => { + const DateWrapperWithProvider = withProvider(CalendarWidgetProvider)(DateWrapper); + + render(); + }; + + test('Base render', () => { + setup(); + + const dateWrapperElem = screen.getByTestId('date-wrapper'); + expect(dateWrapperElem).toBeInTheDocument(); + + expect(dateWrapperElem).toHaveClass('dateWrapper'); + }); +}); diff --git a/src/widgets/CalendarWidget/ui/components/DateWrapper/ui/DateWrapper.styles.module.css b/src/widgets/CalendarWidget/ui/components/DateWrapper/ui/DateWrapper.styles.module.css new file mode 100644 index 0000000..a4b7e8f --- /dev/null +++ b/src/widgets/CalendarWidget/ui/components/DateWrapper/ui/DateWrapper.styles.module.css @@ -0,0 +1,3 @@ +.dateWrapper { + border-top: 1px solid black; +} diff --git a/src/widgets/CalendarWidget/ui/components/DateWrapper/ui/DateWrapper.ui.jsx b/src/widgets/CalendarWidget/ui/components/DateWrapper/ui/DateWrapper.ui.jsx new file mode 100644 index 0000000..fcdbff0 --- /dev/null +++ b/src/widgets/CalendarWidget/ui/components/DateWrapper/ui/DateWrapper.ui.jsx @@ -0,0 +1,61 @@ +import { useCallback, useContext } from 'react'; + +import DateRow from 'widgets/CalendarWidget/ui/components/DateRow/ui/DateRow.ui'; + +import { DAYS_IN_WEEK } from 'shared/constants'; + +// todo fix eslint - no empty line +import { CalendarWidgetContext } from '../../../CalendarWidgetContext'; + +import classes from './DateWrapper.styles.module.css'; + +const DateWrapper = () => { + const { targetYear, targetMonthIndex } = useContext(CalendarWidgetContext); + + const renderDateWrapper = useCallback(() => { + const rowsCount = 6; + const days = []; + + const lastDatePrevMonth = new Date(targetYear, targetMonthIndex, 0); + const startOffset = lastDatePrevMonth.getDay(); + if (startOffset) { + for (let i = 0; i < startOffset; i++) { + const newDate = new Date(targetYear, targetMonthIndex, i - startOffset + 1); + days.push(newDate); + } + } + + const daysPerMonth = new Date(targetYear, targetMonthIndex + 1, 0).getDate(); + // eslint-disable-next-line @stylistic/padding-line-between-statements + for (let i = 1; i <= daysPerMonth; i++) { + const newDate = new Date(targetYear, targetMonthIndex, i); + days.push(newDate); + } + + const endOffset = rowsCount * DAYS_IN_WEEK - days.length; + if (endOffset) { + for (let i = 1; i <= endOffset; i++) { + const newDate = new Date(targetYear, targetMonthIndex + 1, i); + days.push(newDate); + } + } + + const rows = []; + // eslint-disable-next-line @stylistic/padding-line-between-statements + for (let i = 0; i < rowsCount; i++) { + rows.push(days.slice(i * 7, (i + 1) * 7)); + } + + return rows.map((item) => ( + + )); + }, [targetMonthIndex, targetYear]); + + return ( +
+ {renderDateWrapper()} +
+ ); +}; + +export default DateWrapper; diff --git a/src/widgets/CalendarWidget/ui/components/DayOfWeekCell/index.js b/src/widgets/CalendarWidget/ui/components/DayOfWeekCell/index.js new file mode 100644 index 0000000..5476336 --- /dev/null +++ b/src/widgets/CalendarWidget/ui/components/DayOfWeekCell/index.js @@ -0,0 +1 @@ +export { default as DayOfWeekCell } from './ui/DayOfWeekCell.ui'; diff --git a/src/widgets/CalendarWidget/ui/components/DayOfWeekCell/ui/DayOfWeekCell.spec.jsx b/src/widgets/CalendarWidget/ui/components/DayOfWeekCell/ui/DayOfWeekCell.spec.jsx new file mode 100644 index 0000000..357d0df --- /dev/null +++ b/src/widgets/CalendarWidget/ui/components/DayOfWeekCell/ui/DayOfWeekCell.spec.jsx @@ -0,0 +1,136 @@ +import { render, screen } from '@testing-library/react'; + +import { WEEK_DAYS } from 'shared/constants'; +import { withProvider } from 'shared/hoc/withProvider'; + +import { CalendarWidgetProvider } from '../../../CalendarWidgetContext'; + +import DayOfWeekCell from './DayOfWeekCell.ui'; + +describe('Test DayOfWeekCell', () => { + const setup = (day, locale) => { + const DayOfWeekCellComponent = () => ( + + ); + + const Provider = (props) => ( + + ); + + const DayOfWeekCellWithProvider = withProvider(Provider)(DayOfWeekCellComponent); + + render(); + }; + + describe('Basic render sunday', () => { + test('en locale', () => { + setup(WEEK_DAYS.SUNDAY, 'en'); + + const dayOfWeekCellElem = screen.getByTestId('day-of-week-cell'); + expect(dayOfWeekCellElem).toHaveTextContent('Sun.'); + }); + + test('ru locale', () => { + setup(WEEK_DAYS.SUNDAY, 'ru'); + + const dayOfWeekCellElem = screen.getByTestId('day-of-week-cell'); + expect(dayOfWeekCellElem).toHaveTextContent('Вс.'); + }); + }); + + describe('Basic render monday', () => { + test('en locale', () => { + setup(WEEK_DAYS.MONDAY, 'en'); + + const dayOfWeekCellElem = screen.getByTestId('day-of-week-cell'); + expect(dayOfWeekCellElem).toHaveTextContent('Mon.'); + }); + + test('ru locale', () => { + setup(WEEK_DAYS.MONDAY, 'ru'); + + const dayOfWeekCellElem = screen.getByTestId('day-of-week-cell'); + expect(dayOfWeekCellElem).toHaveTextContent('Пн.'); + }); + }); + + describe('Basic render tuesday', () => { + test('en locale', () => { + setup(WEEK_DAYS.TUESDAY, 'en'); + + const dayOfWeekCellElem = screen.getByTestId('day-of-week-cell'); + expect(dayOfWeekCellElem).toHaveTextContent('Tue.'); + }); + + test('ru locale', () => { + setup(WEEK_DAYS.TUESDAY, 'ru'); + + const dayOfWeekCellElem = screen.getByTestId('day-of-week-cell'); + expect(dayOfWeekCellElem).toHaveTextContent('Вт.'); + }); + }); + + describe('Basic render wednesday', () => { + test('en locale', () => { + setup(WEEK_DAYS.WEDNESDAY, 'en'); + + const dayOfWeekCellElem = screen.getByTestId('day-of-week-cell'); + expect(dayOfWeekCellElem).toHaveTextContent('Wed.'); + }); + + test('ru locale', () => { + setup(WEEK_DAYS.WEDNESDAY, 'ru'); + + const dayOfWeekCellElem = screen.getByTestId('day-of-week-cell'); + expect(dayOfWeekCellElem).toHaveTextContent('Ср.'); + }); + }); + + describe('Basic render thursday', () => { + test('en locale', () => { + setup(WEEK_DAYS.THURSDAY, 'en'); + + const dayOfWeekCellElem = screen.getByTestId('day-of-week-cell'); + expect(dayOfWeekCellElem).toHaveTextContent('Thu.'); + }); + + test('ru locale', () => { + setup(WEEK_DAYS.THURSDAY, 'ru'); + + const dayOfWeekCellElem = screen.getByTestId('day-of-week-cell'); + expect(dayOfWeekCellElem).toHaveTextContent('Чт.'); + }); + }); + + describe('Basic render friday', () => { + test('en locale', () => { + setup(WEEK_DAYS.FRIDAY, 'en'); + + const dayOfWeekCellElem = screen.getByTestId('day-of-week-cell'); + expect(dayOfWeekCellElem).toHaveTextContent('Fri.'); + }); + + test('ru locale', () => { + setup(WEEK_DAYS.FRIDAY, 'ru'); + + const dayOfWeekCellElem = screen.getByTestId('day-of-week-cell'); + expect(dayOfWeekCellElem).toHaveTextContent('Пт.'); + }); + }); + + describe('Basic render saturday', () => { + test('en locale', () => { + setup(WEEK_DAYS.SATURDAY, 'en'); + + const dayOfWeekCellElem = screen.getByTestId('day-of-week-cell'); + expect(dayOfWeekCellElem).toHaveTextContent('Sat.'); + }); + + test('ru locale', () => { + setup(WEEK_DAYS.SATURDAY, 'ru'); + + const dayOfWeekCellElem = screen.getByTestId('day-of-week-cell'); + expect(dayOfWeekCellElem).toHaveTextContent('Сб.'); + }); + }); +}); diff --git a/src/widgets/CalendarWidget/ui/components/DayOfWeekCell/ui/DayOfWeekCell.style.module.css b/src/widgets/CalendarWidget/ui/components/DayOfWeekCell/ui/DayOfWeekCell.style.module.css new file mode 100644 index 0000000..c10bbc9 --- /dev/null +++ b/src/widgets/CalendarWidget/ui/components/DayOfWeekCell/ui/DayOfWeekCell.style.module.css @@ -0,0 +1,4 @@ +.content { + max-width: 100%; + font-weight: 600; +} diff --git a/src/widgets/CalendarWidget/ui/components/DayOfWeekCell/ui/DayOfWeekCell.ui.jsx b/src/widgets/CalendarWidget/ui/components/DayOfWeekCell/ui/DayOfWeekCell.ui.jsx new file mode 100644 index 0000000..0dd4836 --- /dev/null +++ b/src/widgets/CalendarWidget/ui/components/DayOfWeekCell/ui/DayOfWeekCell.ui.jsx @@ -0,0 +1,27 @@ +import PropTypes from 'prop-types'; +import { useContext } from 'react'; + +import { WEEK_DAYS_LABEL, WEEK_DAYS } from 'shared/constants'; + +import { CalendarWidgetContext } from '../../../CalendarWidgetContext'; + +import classes from './DayOfWeekCell.style.module.css'; + +const DayOfWeekCell = (props) => { + const { day } = props; + const { locale } = useContext(CalendarWidgetContext); + const content = WEEK_DAYS_LABEL[day][locale]?.short; + + return ( + + {content} + . + + ); +}; + +DayOfWeekCell.propTypes = { + day: PropTypes.oneOf(Object.values(WEEK_DAYS)).isRequired, +}; + +export default DayOfWeekCell; diff --git a/src/widgets/CalendarWidget/ui/components/DayOfWeekRow/index.js b/src/widgets/CalendarWidget/ui/components/DayOfWeekRow/index.js new file mode 100644 index 0000000..238cd25 --- /dev/null +++ b/src/widgets/CalendarWidget/ui/components/DayOfWeekRow/index.js @@ -0,0 +1 @@ +export { default as DayOfWeekRow } from './ui/DayOfWeekRow.ui'; diff --git a/src/widgets/CalendarWidget/ui/components/DayOfWeekRow/ui/DayOfWeekRow.spec.jsx b/src/widgets/CalendarWidget/ui/components/DayOfWeekRow/ui/DayOfWeekRow.spec.jsx new file mode 100644 index 0000000..7fd1783 --- /dev/null +++ b/src/widgets/CalendarWidget/ui/components/DayOfWeekRow/ui/DayOfWeekRow.spec.jsx @@ -0,0 +1,29 @@ +import { render, screen } from '@testing-library/react'; + +import { withProvider } from 'shared/hoc/withProvider'; + +import { CalendarWidgetProvider } from '../../../CalendarWidgetContext'; + +import DayOfWeekRow from './DayOfWeekRow.ui'; + +describe('Test DayOfWeekRow', () => { + const setup = () => { + const DayOfWeekRowWithProvider = withProvider(CalendarWidgetProvider)(DayOfWeekRow); + + render(); + }; + + test('Basic render row', () => { + setup(); + + const dayOfWeekRowElem = screen.getByTestId('day-of-week-row'); + expect(dayOfWeekRowElem).toBeInTheDocument(); + }); + + test('Basic render cells', () => { + setup(); + + const dayOfWeekCellElems = screen.getAllByTestId('day-of-week-cell'); + expect(dayOfWeekCellElems).toHaveLength(7); + }); +}); diff --git a/src/widgets/CalendarWidget/ui/components/DayOfWeekRow/ui/DayOfWeekRow.style.module.css b/src/widgets/CalendarWidget/ui/components/DayOfWeekRow/ui/DayOfWeekRow.style.module.css new file mode 100644 index 0000000..20062f0 --- /dev/null +++ b/src/widgets/CalendarWidget/ui/components/DayOfWeekRow/ui/DayOfWeekRow.style.module.css @@ -0,0 +1,24 @@ +.dayOfWeekRow { + display: flex; + justify-content: space-between; + align-items: center; + border-top: 1px solid black; + + >div { + width: calc(100% / 7 - 1px); + max-width: calc(100% / 7 - 1px); + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; + + &:last-child { + width: calc(100% / 7); + max-width: calc(100% / 7); + } + + &:not(:last-child) { + border-right: 1px solid black; + } + } +} diff --git a/src/widgets/CalendarWidget/ui/components/DayOfWeekRow/ui/DayOfWeekRow.ui.jsx b/src/widgets/CalendarWidget/ui/components/DayOfWeekRow/ui/DayOfWeekRow.ui.jsx new file mode 100644 index 0000000..c2110d1 --- /dev/null +++ b/src/widgets/CalendarWidget/ui/components/DayOfWeekRow/ui/DayOfWeekRow.ui.jsx @@ -0,0 +1,39 @@ +import { WEEK_DAYS } from 'shared/constants'; + +import { DayOfWeekCell } from '../../DayOfWeekCell'; + +import classes from './DayOfWeekRow.style.module.css'; + +const DayOfWeekRow = () => ( +
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+); + +export default DayOfWeekRow; diff --git a/src/widgets/CalendarWidget/ui/components/HeaderRow/index.js b/src/widgets/CalendarWidget/ui/components/HeaderRow/index.js new file mode 100644 index 0000000..257aa52 --- /dev/null +++ b/src/widgets/CalendarWidget/ui/components/HeaderRow/index.js @@ -0,0 +1 @@ +export { default as HeaderRow } from './ui/HeaderRow.ui'; diff --git a/src/widgets/CalendarWidget/ui/components/HeaderRow/ui/HeaderRow.spec.jsx b/src/widgets/CalendarWidget/ui/components/HeaderRow/ui/HeaderRow.spec.jsx new file mode 100644 index 0000000..644feb2 --- /dev/null +++ b/src/widgets/CalendarWidget/ui/components/HeaderRow/ui/HeaderRow.spec.jsx @@ -0,0 +1,77 @@ +import { render, screen } from '@testing-library/react'; + +import { withProvider } from 'shared/hoc/withProvider'; + +import { CalendarWidgetProvider } from '../../../CalendarWidgetContext'; + +import HeaderRow from './HeaderRow.ui'; + +describe('Test HeaderRow', () => { + const setup = () => { + const HeaderRowWithProvider = withProvider(CalendarWidgetProvider)(HeaderRow); + + render(); + }; + + test('Basic render', () => { + setup(); + + const headerRowElem = screen.getByTestId('header-row'); + expect(headerRowElem).toBeInTheDocument(); + }); + + test('Render month select', () => { + setup(); + + const headerRowElem = screen.getByTestId('header-row'); + const monthSelectElem = screen.getByTestId('month-select'); + + expect(headerRowElem).toContainElement(monthSelectElem); + expect(monthSelectElem).toBeInTheDocument(); + expect(monthSelectElem).toBeVisible(); + }); + + test('Render prev month button', () => { + setup(); + + const headerRowElem = screen.getByTestId('header-row'); + const prevMonthButtonElem = screen.getByTestId('prev-month-button'); + + expect(headerRowElem).toContainElement(prevMonthButtonElem); + expect(prevMonthButtonElem).toBeInTheDocument(); + expect(prevMonthButtonElem).toBeVisible(); + }); + + test('Render today button', () => { + setup(); + + const headerRowElem = screen.getByTestId('header-row'); + const todayButtonElem = screen.getByTestId('today-button'); + + expect(headerRowElem).toContainElement(todayButtonElem); + expect(todayButtonElem).toBeInTheDocument(); + expect(todayButtonElem).toBeVisible(); + }); + + test('Render next month button', () => { + setup(); + + const headerRowElem = screen.getByTestId('header-row'); + const nextMonthButtonElem = screen.getByTestId('next-month-button'); + + expect(headerRowElem).toContainElement(nextMonthButtonElem); + expect(nextMonthButtonElem).toBeInTheDocument(); + expect(nextMonthButtonElem).toBeVisible(); + }); + + test('Render year select', () => { + setup(); + + const headerRowElem = screen.getByTestId('header-row'); + const yearSelectElem = screen.getByTestId('year-select'); + + expect(headerRowElem).toContainElement(yearSelectElem); + expect(yearSelectElem).toBeInTheDocument(); + expect(yearSelectElem).toBeVisible(); + }); +}); diff --git a/src/widgets/CalendarWidget/ui/components/HeaderRow/ui/HeaderRow.styles.module.css b/src/widgets/CalendarWidget/ui/components/HeaderRow/ui/HeaderRow.styles.module.css new file mode 100644 index 0000000..9105e66 --- /dev/null +++ b/src/widgets/CalendarWidget/ui/components/HeaderRow/ui/HeaderRow.styles.module.css @@ -0,0 +1,9 @@ +.headerRow { + display: flex; + justify-content: space-between; + align-items: center; + + >div { + background: dimgrey; + } +} diff --git a/src/widgets/CalendarWidget/ui/components/HeaderRow/ui/HeaderRow.ui.jsx b/src/widgets/CalendarWidget/ui/components/HeaderRow/ui/HeaderRow.ui.jsx new file mode 100644 index 0000000..3716020 --- /dev/null +++ b/src/widgets/CalendarWidget/ui/components/HeaderRow/ui/HeaderRow.ui.jsx @@ -0,0 +1,35 @@ +import { MonthSelect } from '../../MonthSelect'; +import { NextMonthButton } from '../../NextMonthButton'; +import { PrevMonthButton } from '../../PrevMonthButton'; +import { TodayButton } from '../../TodayButton'; +import { YearSelect } from '../../YearSelect'; + +import classes from './HeaderRow.styles.module.css'; + +const HeaderRow = () => ( +
+
+ +
+ +
+ + < + + + + Сегодня + + + + > + +
+ +
+ +
+
+); + +export default HeaderRow; diff --git a/src/widgets/CalendarWidget/ui/components/MonthSelect/index.js b/src/widgets/CalendarWidget/ui/components/MonthSelect/index.js new file mode 100644 index 0000000..c9d19ac --- /dev/null +++ b/src/widgets/CalendarWidget/ui/components/MonthSelect/index.js @@ -0,0 +1 @@ +export { default as MonthSelect } from './ui/MonthSelect.ui'; diff --git a/src/widgets/CalendarWidget/ui/components/MonthSelect/ui/MonthSelect.spec.jsx b/src/widgets/CalendarWidget/ui/components/MonthSelect/ui/MonthSelect.spec.jsx new file mode 100644 index 0000000..238745c --- /dev/null +++ b/src/widgets/CalendarWidget/ui/components/MonthSelect/ui/MonthSelect.spec.jsx @@ -0,0 +1,52 @@ +import { render, screen } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; + +import { MONTHS } from 'shared/constants'; +import { withProvider } from 'shared/hoc/withProvider'; + +import { CalendarWidgetProvider } from '../../../CalendarWidgetContext'; + +import MonthSelect from './MonthSelect.ui'; + +describe('Test MonthSelect', () => { + let user; + + beforeEach(() => { + user = userEvent.setup(); + }); + + const setup = (props) => { + const MonthSelectComponent = () => ( + + ); + const MonthSelectWithProvider = withProvider(CalendarWidgetProvider)(MonthSelectComponent); + + render(); + }; + + test('Basic render', async () => { + setup(); + + const monthSelectElem = screen.getByTestId('month-select'); + + expect(monthSelectElem).toBeInTheDocument(); + expect(monthSelectElem).toBeVisible(); + + await user.selectOptions(monthSelectElem, String(MONTHS.JULY)); + expect(monthSelectElem).toHaveValue('6'); + expect(monthSelectElem).toHaveDisplayValue(['Июль']); + }); + + test('onChange handler', async () => { + const onChange = jest.fn(); + + setup({ onChange }); + + const monthSelectElem = screen.getByTestId('month-select'); + await user.selectOptions(monthSelectElem, String(MONTHS.JULY)); + + expect(onChange).toHaveBeenCalled(); + const newDate = new Date(2024, 6); + expect(onChange).toHaveBeenCalledWith(newDate); + }); +}); diff --git a/src/widgets/CalendarWidget/ui/components/MonthSelect/ui/MonthSelect.ui.jsx b/src/widgets/CalendarWidget/ui/components/MonthSelect/ui/MonthSelect.ui.jsx new file mode 100644 index 0000000..1ed43fe --- /dev/null +++ b/src/widgets/CalendarWidget/ui/components/MonthSelect/ui/MonthSelect.ui.jsx @@ -0,0 +1,61 @@ +import PropTypes from 'prop-types'; +import { useContext } from 'react'; + +import { MONTHS, MONTHS_LABEL } from 'shared/constants'; +import { lastDayOfMonthDate } from 'shared/utils/date'; + +import { CalendarWidgetContext } from '../../../CalendarWidgetContext'; + +const MonthSelect = (props) => { + const { + onChange, + ...restProps + } = props; + // todo eslint sort + const { + targetYear, + targetMonthIndex, + setTargetDate, + minDate, + maxDate, + locale, + } = useContext(CalendarWidgetContext); + + const onChangeHandler = (e) => { + const newDate = new Date(targetYear, Number(e.target.value)); + setTargetDate(newDate); + onChange?.(newDate); + }; + + const createOption = (value) => { + const newDate = new Date(targetYear, value); + + return ( + + ); + }; + + return ( + + ); +}; + +MonthSelect.propTypes = { + onChange: PropTypes.func, +}; + +export default MonthSelect; diff --git a/src/widgets/CalendarWidget/ui/components/NextMonthButton/index.js b/src/widgets/CalendarWidget/ui/components/NextMonthButton/index.js new file mode 100644 index 0000000..be91df4 --- /dev/null +++ b/src/widgets/CalendarWidget/ui/components/NextMonthButton/index.js @@ -0,0 +1 @@ +export { default as NextMonthButton } from './ui/NextMonthButton.ui'; diff --git a/src/widgets/CalendarWidget/ui/components/NextMonthButton/ui/NextMonthButton.spec.jsx b/src/widgets/CalendarWidget/ui/components/NextMonthButton/ui/NextMonthButton.spec.jsx new file mode 100644 index 0000000..e8266d6 --- /dev/null +++ b/src/widgets/CalendarWidget/ui/components/NextMonthButton/ui/NextMonthButton.spec.jsx @@ -0,0 +1,43 @@ +import { render, screen } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; + +import { withProvider } from 'shared/hoc/withProvider'; + +import { CalendarWidgetProvider } from '../../../CalendarWidgetContext'; + +import NextMonthButton from './NextMonthButton.ui'; + +describe('Test NextMonthButton', () => { + let user; + + beforeEach(() => { + user = userEvent.setup(); + }); + + const setup = (props) => { + const NextMonthButtonComponent = () => ( + + ); + const NextMonthButtonWithProvider = withProvider(CalendarWidgetProvider)(NextMonthButtonComponent); + + render(); + }; + + test('Basic render', () => { + setup(); + + const nextMontButtonElem = screen.getByTestId('next-month-button'); + expect(nextMontButtonElem).toBeInTheDocument(); + expect(nextMontButtonElem).toBeVisible(); + }); + + test('onClick handler', async () => { + const onClick = jest.fn(); + setup({ onClick }); + + const nextMontButtonElem = screen.getByTestId('next-month-button'); + await user.click(nextMontButtonElem); + + expect(onClick).toHaveBeenCalled(); + }); +}); diff --git a/src/widgets/CalendarWidget/ui/components/NextMonthButton/ui/NextMonthButton.ui.jsx b/src/widgets/CalendarWidget/ui/components/NextMonthButton/ui/NextMonthButton.ui.jsx new file mode 100644 index 0000000..b7001c7 --- /dev/null +++ b/src/widgets/CalendarWidget/ui/components/NextMonthButton/ui/NextMonthButton.ui.jsx @@ -0,0 +1,46 @@ +import PropTypes from 'prop-types'; +import { useContext } from 'react'; + +import { CalendarWidgetContext } from '../../../CalendarWidgetContext'; + +const NextMonthButton = (props) => { + const { + children = 'next', + disabled, + onClick, + ...restProps + } = props; + const { + targetYear, + targetMonthIndex, + setTargetDate, + maxDate, + } = useContext(CalendarWidgetContext); + + const nextDate = new Date(targetYear, targetMonthIndex + 1); + + const onClickHandler = () => { + setTargetDate(nextDate); + onClick?.(nextDate); + }; + + return ( + + ); +}; + +NextMonthButton.propTypes = { + children: PropTypes.node, + onClick: PropTypes.func, + disabled: PropTypes.bool, +}; + +export default NextMonthButton; diff --git a/src/widgets/CalendarWidget/ui/components/PrevMonthButton/index.js b/src/widgets/CalendarWidget/ui/components/PrevMonthButton/index.js new file mode 100644 index 0000000..5a64373 --- /dev/null +++ b/src/widgets/CalendarWidget/ui/components/PrevMonthButton/index.js @@ -0,0 +1 @@ +export { default as PrevMonthButton } from './ui/PrevMonthButton.ui'; diff --git a/src/widgets/CalendarWidget/ui/components/PrevMonthButton/ui/PrevMonthButton.spec.jsx b/src/widgets/CalendarWidget/ui/components/PrevMonthButton/ui/PrevMonthButton.spec.jsx new file mode 100644 index 0000000..3e1de67 --- /dev/null +++ b/src/widgets/CalendarWidget/ui/components/PrevMonthButton/ui/PrevMonthButton.spec.jsx @@ -0,0 +1,43 @@ +import { render, screen } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; + +import { withProvider } from 'shared/hoc/withProvider'; + +import { CalendarWidgetProvider } from '../../../CalendarWidgetContext'; + +import PrevMonthButton from './PrevMonthButton.ui'; + +describe('Test NextMonthButton', () => { + let user; + + beforeEach(() => { + user = userEvent.setup(); + }); + + const setup = (props) => { + const PrevMonthButtonComponent = () => ( + + ); + const PrevMonthButtonWithProvider = withProvider(CalendarWidgetProvider)(PrevMonthButtonComponent); + + render(); + }; + + test('Basic render', () => { + setup(); + + const prevMontButtonElem = screen.getByTestId('prev-month-button'); + expect(prevMontButtonElem).toBeInTheDocument(); + expect(prevMontButtonElem).toBeVisible(); + }); + + test('onClick handler', async () => { + const onClick = jest.fn(); + setup({ onClick }); + + const prevMontButtonElem = screen.getByTestId('prev-month-button'); + await user.click(prevMontButtonElem); + + expect(onClick).toHaveBeenCalled(); + }); +}); diff --git a/src/widgets/CalendarWidget/ui/components/PrevMonthButton/ui/PrevMonthButton.ui.jsx b/src/widgets/CalendarWidget/ui/components/PrevMonthButton/ui/PrevMonthButton.ui.jsx new file mode 100644 index 0000000..45bca7e --- /dev/null +++ b/src/widgets/CalendarWidget/ui/components/PrevMonthButton/ui/PrevMonthButton.ui.jsx @@ -0,0 +1,46 @@ +import PropTypes from 'prop-types'; +import { useContext } from 'react'; + +import { CalendarWidgetContext } from '../../../CalendarWidgetContext'; + +const PrevMonthButton = (props) => { + const { + children = 'prev', + onClick, + disabled, + ...restProps + } = props; + const { + targetYear, + targetMonthIndex, + setTargetDate, + minDate, + } = useContext(CalendarWidgetContext); + + const prevDate = new Date(targetYear, targetMonthIndex, 0); + + const onClickHandler = () => { + setTargetDate(prevDate); + onClick?.(prevDate); + }; + + return ( + + ); +}; + +PrevMonthButton.propTypes = { + children: PropTypes.node, + onClick: PropTypes.func, + disabled: PropTypes.bool, +}; + +export default PrevMonthButton; diff --git a/src/widgets/CalendarWidget/ui/components/TodayButton/index.js b/src/widgets/CalendarWidget/ui/components/TodayButton/index.js new file mode 100644 index 0000000..92dd317 --- /dev/null +++ b/src/widgets/CalendarWidget/ui/components/TodayButton/index.js @@ -0,0 +1 @@ +export { default as TodayButton } from './ui/TodayButton.ui'; diff --git a/src/widgets/CalendarWidget/ui/components/TodayButton/ui/TodayButton.spec.jsx b/src/widgets/CalendarWidget/ui/components/TodayButton/ui/TodayButton.spec.jsx new file mode 100644 index 0000000..5e47dcf --- /dev/null +++ b/src/widgets/CalendarWidget/ui/components/TodayButton/ui/TodayButton.spec.jsx @@ -0,0 +1,56 @@ +import { render, screen } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; + +import { withProvider } from 'shared/hoc/withProvider'; + +import { CalendarWidgetProvider } from '../../../CalendarWidgetContext'; + +import TodayButton from './TodayButton.ui'; + +describe('Test NextMonthButton', () => { + let user; + + beforeEach(() => { + user = userEvent.setup(); + }); + + const setup = (props) => { + const TodayButtonComponent = () => ( + + ); + const TodayButtonWithProvider = withProvider(CalendarWidgetProvider)(TodayButtonComponent); + + render(); + }; + + test('Basic render', () => { + setup(); + + const todayButtonElem = screen.getByTestId('today-button'); + expect(todayButtonElem).toBeInTheDocument(); + expect(todayButtonElem).toBeVisible(); + }); + + test('onClick handler', async () => { + const onClick = jest.fn(); + setup({ onClick }); + + const todayButtonElem = screen.getByTestId('today-button'); + await user.click(todayButtonElem); + + expect(onClick).toHaveBeenCalled(); + + const onClickArgument = onClick.mock.calls[0][0]; + const calledDate = new Date(onClickArgument); + const calledYear = calledDate.getFullYear(); + const calledMonthIndex = calledDate.getMonth(); + const calledDateNumber = calledDate.getDate(); + const calledString = `${calledYear}-${calledMonthIndex}-${calledDateNumber}`; + + const currentYear = new Date().getFullYear(); + const currentMonthIndex = new Date().getMonth(); + const currentDateNumber = new Date().getDate(); + const expectedString = `${currentYear}-${currentMonthIndex}-${currentDateNumber}`; + expect(calledString).toBe(expectedString); + }); +}); diff --git a/src/widgets/CalendarWidget/ui/components/TodayButton/ui/TodayButton.ui.jsx b/src/widgets/CalendarWidget/ui/components/TodayButton/ui/TodayButton.ui.jsx new file mode 100644 index 0000000..a23d7e6 --- /dev/null +++ b/src/widgets/CalendarWidget/ui/components/TodayButton/ui/TodayButton.ui.jsx @@ -0,0 +1,38 @@ +import PropTypes from 'prop-types'; +import { useContext } from 'react'; + +import { CalendarWidgetContext } from '../../../CalendarWidgetContext'; + +const TodayButton = (props) => { + const { + children = 'today', + onClick, + ...restProps + } = props; + + const { setTargetDate } = useContext(CalendarWidgetContext); + + const onClickHandler = () => { + const newDate = new Date(); + setTargetDate(newDate); + onClick?.(newDate); + }; + + return ( + + ); +}; + +TodayButton.propTypes = { + children: PropTypes.node, + onClick: PropTypes.func, +}; + +export default TodayButton; diff --git a/src/widgets/CalendarWidget/ui/components/YearSelect/index.js b/src/widgets/CalendarWidget/ui/components/YearSelect/index.js new file mode 100644 index 0000000..7ac62c0 --- /dev/null +++ b/src/widgets/CalendarWidget/ui/components/YearSelect/index.js @@ -0,0 +1 @@ +export { default as YearSelect } from './ui/YearSelect.ui'; diff --git a/src/widgets/CalendarWidget/ui/components/YearSelect/ui/YearSelect.spec.jsx b/src/widgets/CalendarWidget/ui/components/YearSelect/ui/YearSelect.spec.jsx new file mode 100644 index 0000000..80f3778 --- /dev/null +++ b/src/widgets/CalendarWidget/ui/components/YearSelect/ui/YearSelect.spec.jsx @@ -0,0 +1,53 @@ +import { render, screen } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; + +import { withProvider } from 'shared/hoc/withProvider'; + +import { CalendarWidgetProvider } from '../../../CalendarWidgetContext'; + +import YearSelect from './YearSelect.ui'; + +describe('Test YearSelect', () => { + let user; + + beforeEach(() => { + user = userEvent.setup(); + }); + + const setup = (props) => { + const YearSelectComponent = () => ( + + ); + const YearSelectWithProvider = withProvider(CalendarWidgetProvider)(YearSelectComponent); + + render(); + }; + + test('Basic render', async () => { + setup(); + + const yearSelectElem = screen.getByTestId('year-select'); + + expect(yearSelectElem).toBeInTheDocument(); + expect(yearSelectElem).toBeVisible(); + + await user.selectOptions(yearSelectElem, '2020'); + expect(yearSelectElem).toHaveValue('2020'); + expect(yearSelectElem).toHaveDisplayValue(['2020']); + }); + + test('onChange handler', async () => { + const onChange = jest.fn(); + + setup({ onChange }); + + const yearSelectElem = screen.getByTestId('year-select'); + await user.selectOptions(yearSelectElem, '2020'); + + expect(onChange).toHaveBeenCalled(); + const currentDate = new Date(); + const currentMonthIndex = currentDate.getMonth(); + const newDate = new Date(2020, currentMonthIndex); + expect(onChange).toHaveBeenCalledWith(newDate); + }); +}); diff --git a/src/widgets/CalendarWidget/ui/components/YearSelect/ui/YearSelect.ui.jsx b/src/widgets/CalendarWidget/ui/components/YearSelect/ui/YearSelect.ui.jsx new file mode 100644 index 0000000..69a920c --- /dev/null +++ b/src/widgets/CalendarWidget/ui/components/YearSelect/ui/YearSelect.ui.jsx @@ -0,0 +1,72 @@ +import PropTypes from 'prop-types'; +import { useContext } from 'react'; + +import { CalendarWidgetContext } from '../../../CalendarWidgetContext'; + +const YearSelect = (props) => { + const { + onChange, + ...restProps + } = props; + // todo eslint fix + const { + targetYear, targetMonthIndex, setTargetDate, minDate, maxDate, + } = useContext(CalendarWidgetContext); + + const onChangeHandler = (event) => { + let newDate = new Date(event.target.value, targetMonthIndex); + + if (newDate < minDate) { + newDate = minDate; + } + + if (newDate > maxDate) { + newDate = maxDate; + } + + setTargetDate(newDate); + onChange?.(newDate); + }; + + const renderOptions = () => { + const minYear = minDate.getFullYear(); + const maxYear = maxDate.getFullYear(); + const options = []; + + for (let i = minYear; i <= maxYear; i++) { + options.push(i); + } + + return options.map((item) => { + // todo refactor + const newDate = new Date(item, targetMonthIndex + 1, 0); + const newDate2 = new Date(item, targetMonthIndex); + const disabled = newDate2 > maxDate || newDate < minDate; + + return ( + + ); + }); + }; + + return ( + + ); +}; + +YearSelect.propTypes = { + onChange: PropTypes.func, +}; + +export default YearSelect;