From 11e0958e52ce95f88ae38fcea7a8b97da0000a3b Mon Sep 17 00:00:00 2001 From: Daniil Fomenko Date: Sun, 4 May 2025 19:16:18 +0300 Subject: [PATCH 01/11] deleted unnecessary check of screen size --- src/pages/Main/components/Calendar/DaysList/index.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/pages/Main/components/Calendar/DaysList/index.tsx b/src/pages/Main/components/Calendar/DaysList/index.tsx index d0ddd64..744eddf 100644 --- a/src/pages/Main/components/Calendar/DaysList/index.tsx +++ b/src/pages/Main/components/Calendar/DaysList/index.tsx @@ -1,21 +1,18 @@ import {useSelector} from 'react-redux'; import {selectMonth, selectYear} from '@/redux/date/selectors'; -import {useScreenSize} from '@/services/hooks'; import {generateDatesArray} from '@/services/utils'; import Day from '../Day'; const DaysList = () => { - const screenSize = useScreenSize(); const year = useSelector(selectYear); const month = useSelector(selectMonth); const dates = generateDatesArray(year as string, month as string); return ( -
+
{dates.map(date => ( ))} From 3a85fad2b1c59ebef34897ecbc82b590ea924bdd Mon Sep 17 00:00:00 2001 From: Daniil Fomenko Date: Tue, 17 Jun 2025 21:49:18 +0300 Subject: [PATCH 02/11] added separated utils for days --- .../components/Calendar/DaysList/index.tsx | 2 +- .../components/Calendar/WeekDays/index.tsx | 2 +- .../Header/CalendarDatePicker/index.tsx | 2 +- .../Header/Controls/ChangeMonthControl.tsx | 2 +- src/redux/date/dateSlice.ts | 2 +- src/services/dateUtils.ts | 80 +++++++++++++++++ src/services/utils.ts | 88 +++++-------------- 7 files changed, 108 insertions(+), 70 deletions(-) create mode 100644 src/services/dateUtils.ts diff --git a/src/pages/Main/components/Calendar/DaysList/index.tsx b/src/pages/Main/components/Calendar/DaysList/index.tsx index 744eddf..d3abf55 100644 --- a/src/pages/Main/components/Calendar/DaysList/index.tsx +++ b/src/pages/Main/components/Calendar/DaysList/index.tsx @@ -1,7 +1,7 @@ import {useSelector} from 'react-redux'; import {selectMonth, selectYear} from '@/redux/date/selectors'; -import {generateDatesArray} from '@/services/utils'; +import {generateDatesArray} from '@/services/dateUtils'; import Day from '../Day'; diff --git a/src/pages/Main/components/Calendar/WeekDays/index.tsx b/src/pages/Main/components/Calendar/WeekDays/index.tsx index 18f6d0f..dfad7c3 100644 --- a/src/pages/Main/components/Calendar/WeekDays/index.tsx +++ b/src/pages/Main/components/Calendar/WeekDays/index.tsx @@ -1,5 +1,5 @@ +import {getWeekDays} from '@/services/dateUtils'; import {useScreenSize} from '@/services/hooks'; -import {getWeekDays} from '@/services/utils'; const WeekDays = () => { const weekDays = getWeekDays(); diff --git a/src/pages/Main/components/Header/CalendarDatePicker/index.tsx b/src/pages/Main/components/Header/CalendarDatePicker/index.tsx index 16122c2..aa7f60d 100644 --- a/src/pages/Main/components/Header/CalendarDatePicker/index.tsx +++ b/src/pages/Main/components/Header/CalendarDatePicker/index.tsx @@ -6,7 +6,7 @@ import Dropdown from '@/components/inputs/Dropdown'; import {setFullDate} from '@/redux/date/dateSlice'; import {selectFullDate, selectMonth, selectYear} from '@/redux/date/selectors'; import {useAppDispatch} from '@/redux/store'; -import {formatDate, getMonthsOptions, getYearsOptions} from '@/services/utils'; +import {formatDate, getMonthsOptions, getYearsOptions} from '@/services/dateUtils'; const CalendarDatePicker = () => { const dispatch = useAppDispatch(); diff --git a/src/pages/Main/components/Header/Controls/ChangeMonthControl.tsx b/src/pages/Main/components/Header/Controls/ChangeMonthControl.tsx index a076586..762aa42 100644 --- a/src/pages/Main/components/Header/Controls/ChangeMonthControl.tsx +++ b/src/pages/Main/components/Header/Controls/ChangeMonthControl.tsx @@ -7,7 +7,7 @@ import ArrowRight from '@/icons/ArrowRight'; import {setFullDate} from '@/redux/date/dateSlice'; import {selectFullDate} from '@/redux/date/selectors'; import {useAppDispatch} from '@/redux/store'; -import {formatDate, getDate} from '@/services/utils'; +import {formatDate, getDate} from '@/services/dateUtils'; enum MonthDirection { NEXT = 'next', diff --git a/src/redux/date/dateSlice.ts b/src/redux/date/dateSlice.ts index 7a435e5..d21fe7e 100644 --- a/src/redux/date/dateSlice.ts +++ b/src/redux/date/dateSlice.ts @@ -1,7 +1,7 @@ import {PayloadAction, createSlice} from '@reduxjs/toolkit'; import moment from 'moment'; -import {formatDate, getDate} from '@/services/utils'; +import {formatDate, getDate} from '@/services/dateUtils'; import {SliceNames} from '../types'; import {TDateState} from './types'; diff --git a/src/services/dateUtils.ts b/src/services/dateUtils.ts new file mode 100644 index 0000000..f4f9dff --- /dev/null +++ b/src/services/dateUtils.ts @@ -0,0 +1,80 @@ +import moment, {Moment, MomentInput} from 'moment'; + +import {TOTAL_DAYS_IN_MONTH} from './constants'; +import {Format, MonthParts} from './types'; + +export const getDate = (date: MomentInput) => { + return moment(date); +}; + +export const createDate = (year: number, month: string, day: number) => { + return moment().year(year).month(month).date(day).format('YYYY-MM-DD'); +}; + +export const formatDate = (date: Moment, format: Format) => { + return date.format(format); +}; + +export const getWeekDays = () => moment.weekdays(true); +export const getMonthsOptions = () => moment.months(); + +export const getYearsOptions = () => { + const currentYear = moment().year(); + const startYear = currentYear - 10; // Adjust the range as needed + + return Array.from({length: 30}, (_, i) => (startYear + i).toString()); +}; + +export const getMonthIndex = (month: string) => { + return formatDate(moment().month(month), 'M'); +}; + +export const getDays = (year: string, month: string) => { + const monthIndex = getMonthIndex(month); + const amountOfDays = moment([year, +monthIndex - 1]).daysInMonth(); + + return Array.from({length: amountOfDays}, (_, i) => (i + 1 < 10 ? `0${i + 1}` : `${i + 1}`)); +}; + +export const generateDatesArray = (year: string, month: string) => { + const monthIndex = getMonthIndex(month); + + const startDate = moment(`${year}-${monthIndex}`, 'YYYY-M'); + const prevMonth = moment(startDate).add(-1, 'M'); + const nextMonth = moment(startDate).add(1, 'M'); + + const prevMonthDates = generateDatesArrayByFullDate(prevMonth); + const currentMonthDates = generateDatesArrayByFullDate(startDate); + const nextMonthDates = generateDatesArrayByFullDate(nextMonth); + + const firstDateOfCurrentMonth = moment(currentMonthDates[0]); + const firstDateOfCurrentMonthIndex = firstDateOfCurrentMonth.weekday(); + + const partOfPrevMonthDates = getPartOfMonthDates(prevMonthDates, firstDateOfCurrentMonthIndex, MonthParts.END); + const partOfNextMonthDates = getPartOfMonthDates( + nextMonthDates, + TOTAL_DAYS_IN_MONTH - [...partOfPrevMonthDates, ...currentMonthDates].length, + MonthParts.START, + ); + + return [...partOfPrevMonthDates, ...currentMonthDates, ...partOfNextMonthDates]; +}; + +const getPartOfMonthDates = (dates: string[], amountOfDays: number, part: MonthParts) => { + if (part === MonthParts.START) { + return dates.slice(0, amountOfDays); + } + + return dates.slice(dates.length - amountOfDays); +}; + +const generateDatesArrayByFullDate = (date: Moment) => { + const dates = []; + const daysInMonth = date.daysInMonth(); + + for (let i = 0; i < daysInMonth; i++) { + dates.push(date.clone().add(i, 'days').format('YYYY-MM-DD')); + } + + return dates; +}; diff --git a/src/services/utils.ts b/src/services/utils.ts index 5cba653..5583d21 100644 --- a/src/services/utils.ts +++ b/src/services/utils.ts @@ -1,76 +1,34 @@ -import moment, {Moment, MomentInput} from 'moment'; +import {uid} from 'uid'; -import {TOTAL_DAYS_IN_MONTH} from './constants'; -import {Format, MonthParts} from './types'; +import {StorageKeys} from './types'; -export const getDate = (date: MomentInput) => { - return moment(date); +type CreateEventProps = { + eventName: string; + description?: string; + date: string; }; -export const formatDate = (date: Moment, format: Format) => { - return date.format(format); -}; - -export const getWeekDays = () => moment.weekdays(true); -export const getMonthsOptions = () => moment.months(); - -export const getYearsOptions = () => { - const currentYear = moment().year(); - const startYear = currentYear - 10; // Adjust the range as needed - - return Array.from({length: 30}, (_, i) => (startYear + i).toString()); -}; +export const createEvent = ({eventName, description, date}: CreateEventProps) => { + const newEvent = { + id: uid(), + title: eventName.trim(), + description: description?.trim(), + date, + color: getRandomColor(), + }; -export const getMonthIndex = (month: string) => { - return formatDate(moment().month(month), 'M'); + return newEvent; }; -export const getDays = (year: string, month: string) => { - const monthIndex = getMonthIndex(month); - const amountOfDays = moment([year, +monthIndex - 1]).daysInMonth(); +export const getLocalStoredValues = (key: StorageKeys, defaultValues?: unknown) => { + const storedValues = localStorage.getItem(key); + const parsedValues = storedValues ? JSON.parse(storedValues) : defaultValues; - return Array.from({length: amountOfDays}, (_, i) => (i + 1 < 10 ? `0${i + 1}` : `${i + 1}`)); + return parsedValues; }; -export const generateDatesArray = (year: string, month: string) => { - const monthIndex = getMonthIndex(month); - - const startDate = moment(`${year}-${monthIndex}`, 'YYYY-M'); - const prevMonth = moment(startDate).add(-1, 'M'); - const nextMonth = moment(startDate).add(1, 'M'); - - const prevMonthDates = generateDatesArrayByFullDate(prevMonth); - const currentMonthDates = generateDatesArrayByFullDate(startDate); - const nextMonthDates = generateDatesArrayByFullDate(nextMonth); - - const firstDateOfCurrentMonth = moment(currentMonthDates[0]); - const firstDateOfCurrentMonthIndex = firstDateOfCurrentMonth.weekday(); - - const partOfPrevMonthDates = getPartOfMonthDates(prevMonthDates, firstDateOfCurrentMonthIndex, MonthParts.END); - const partOfNextMonthDates = getPartOfMonthDates( - nextMonthDates, - TOTAL_DAYS_IN_MONTH - [...partOfPrevMonthDates, ...currentMonthDates].length, - MonthParts.START, - ); - - return [...partOfPrevMonthDates, ...currentMonthDates, ...partOfNextMonthDates]; -}; - -const getPartOfMonthDates = (dates: string[], amountOfDays: number, part: MonthParts) => { - if (part === MonthParts.START) { - return dates.slice(0, amountOfDays); - } - - return dates.slice(dates.length - amountOfDays); -}; - -const generateDatesArrayByFullDate = (date: Moment) => { - const dates = []; - const daysInMonth = date.daysInMonth(); - - for (let i = 0; i < daysInMonth; i++) { - dates.push(date.clone().add(i, 'days').format('YYYY-MM-DD')); - } - - return dates; +export const getRandomColor = () => { + return `#${Math.floor(Math.random() * 16777215) + .toString(16) + .padStart(6, '0')}`; }; From 4d03f5595f025b9d196ff700ceb35d6bff1c058d Mon Sep 17 00:00:00 2001 From: Daniil Fomenko Date: Tue, 17 Jun 2025 21:53:32 +0300 Subject: [PATCH 03/11] deleted/replaced items --- .../{BoardItemForm => BoardEventForm}/form.ts | 6 +- src/components/forms/BoardEventForm/index.tsx | 99 ++++++++++++++++ .../types.tsx | 9 +- src/components/forms/BoardItemForm/index.tsx | 57 --------- .../Calendar/Day/EditTitleForm/form.ts | 5 - .../Calendar/Day/EditTitleForm/index.tsx | 62 ---------- .../Calendar/Day/EditTitleForm/types.ts | 10 -- src/{redux => providers}/redux-provider.tsx | 2 +- src/redux/columns/columnsSlice.ts | 109 ------------------ src/redux/columns/selectors.ts | 10 -- src/redux/columns/types.ts | 42 ------- src/redux/events/eventsSlice.ts | 37 ++++++ src/redux/events/selectors.ts | 3 + src/redux/events/types.ts | 15 +++ 14 files changed, 163 insertions(+), 303 deletions(-) rename src/components/forms/{BoardItemForm => BoardEventForm}/form.ts (72%) create mode 100644 src/components/forms/BoardEventForm/index.tsx rename src/components/forms/{BoardItemForm => BoardEventForm}/types.tsx (53%) delete mode 100644 src/components/forms/BoardItemForm/index.tsx delete mode 100644 src/pages/Main/components/Calendar/Day/EditTitleForm/form.ts delete mode 100644 src/pages/Main/components/Calendar/Day/EditTitleForm/index.tsx delete mode 100644 src/pages/Main/components/Calendar/Day/EditTitleForm/types.ts rename src/{redux => providers}/redux-provider.tsx (86%) delete mode 100644 src/redux/columns/columnsSlice.ts delete mode 100644 src/redux/columns/selectors.ts delete mode 100644 src/redux/columns/types.ts create mode 100644 src/redux/events/eventsSlice.ts create mode 100644 src/redux/events/selectors.ts create mode 100644 src/redux/events/types.ts diff --git a/src/components/forms/BoardItemForm/form.ts b/src/components/forms/BoardEventForm/form.ts similarity index 72% rename from src/components/forms/BoardItemForm/form.ts rename to src/components/forms/BoardEventForm/form.ts index 2dba7e3..c4c8feb 100644 --- a/src/components/forms/BoardItemForm/form.ts +++ b/src/components/forms/BoardEventForm/form.ts @@ -1,11 +1,13 @@ import {object, string} from 'yup'; export const defaultValues = { - field: '', + eventName: '', + eventDescription: '', }; export const validation = object().shape({ - field: string() + eventName: string() .required('This field is required') .test('empty-check', 'Event name can not be empty string.', name => !!name.trim().length), + eventDescription: string(), }); diff --git a/src/components/forms/BoardEventForm/index.tsx b/src/components/forms/BoardEventForm/index.tsx new file mode 100644 index 0000000..063a290 --- /dev/null +++ b/src/components/forms/BoardEventForm/index.tsx @@ -0,0 +1,99 @@ +import {yupResolver} from '@hookform/resolvers/yup'; +import {useCallback} from 'react'; +import {FormProvider, useForm} from 'react-hook-form'; + +import Button from '@/components/Button'; +import InputControl from '@/components/formInputs/InputControl'; +import CheckIcon from '@/icons/CheckIcon'; +import CloseIcon from '@/icons/CloseIcon'; +import EditIcon from '@/icons/EditIcon'; +import {addEvent} from '@/redux/events/eventsSlice'; +import {useAppDispatch} from '@/redux/store'; +import {createEvent} from '@/services/utils'; + +import {validation} from './form'; +import {TBoardEventFormProps, TFormFields} from './types'; + +const BoardEventForm = ({ + actionType = 'edit', + formTitle, + defaultValues = { + eventName: '', + eventDescription: '', + }, + date, + handleModalClose, +}: TBoardEventFormProps) => { + const dispatch = useAppDispatch(); + const methods = useForm({ + resolver: yupResolver(validation), + defaultValues, + mode: 'onSubmit', + }); + + const { + control, + handleSubmit, + formState: {errors, isDirty}, + } = methods; + + const onSubmit = useCallback( + ({eventName, eventDescription}: TFormFields) => { + handleModalClose(); + + if (actionType === 'edit') { + // eslint-disable-next-line no-console + console.log('edit', eventName); + } else { + const newEvent = createEvent({eventName, date, description: eventDescription || ''}); + + dispatch(addEvent(newEvent)); + } + }, + [actionType, date, dispatch, handleModalClose], + ); + + return ( +
+
+ + +
+ ); +}; + +export default BoardEventForm; diff --git a/src/components/forms/BoardItemForm/types.tsx b/src/components/forms/BoardEventForm/types.tsx similarity index 53% rename from src/components/forms/BoardItemForm/types.tsx rename to src/components/forms/BoardEventForm/types.tsx index 31fd0cf..c464883 100644 --- a/src/components/forms/BoardItemForm/types.tsx +++ b/src/components/forms/BoardEventForm/types.tsx @@ -1,13 +1,12 @@ -import {SubmitHandler} from 'react-hook-form'; - export type TFormFields = { - field: string; + eventName: string; + eventDescription?: string; }; -export type TEditFormProps = { +export type TBoardEventFormProps = { actionType?: 'edit' | 'add'; formTitle: string; defaultValues?: TFormFields; - onSubmit: SubmitHandler; + date: string; handleModalClose: () => void; }; diff --git a/src/components/forms/BoardItemForm/index.tsx b/src/components/forms/BoardItemForm/index.tsx deleted file mode 100644 index e4e8080..0000000 --- a/src/components/forms/BoardItemForm/index.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import {yupResolver} from '@hookform/resolvers/yup'; -import {FormProvider, useForm} from 'react-hook-form'; - -import Button from '@/components/Button'; -import InputControl from '@/components/formInputs/InputControl'; -import CheckIcon from '@/icons/CheckIcon'; -import CloseIcon from '@/icons/CloseIcon'; -import EditIcon from '@/icons/EditIcon'; - -import {validation} from './form'; -import {TEditFormProps} from './types'; - -const BoardItemForm = ({actionType = 'edit', formTitle, defaultValues, onSubmit, handleModalClose}: TEditFormProps) => { - const methods = useForm({ - resolver: yupResolver(validation), - defaultValues: defaultValues || {field: ''}, - mode: 'onSubmit', - }); - - const { - control, - handleSubmit, - formState: {errors, isDirty}, - } = methods; - - return ( -
-
- - -
- ); -}; - -export default BoardItemForm; diff --git a/src/pages/Main/components/Calendar/Day/EditTitleForm/form.ts b/src/pages/Main/components/Calendar/Day/EditTitleForm/form.ts deleted file mode 100644 index ba4b513..0000000 --- a/src/pages/Main/components/Calendar/Day/EditTitleForm/form.ts +++ /dev/null @@ -1,5 +0,0 @@ -import {object, string} from 'yup'; - -export const validation = object().shape({ - title: string().required('Title is required'), -}); diff --git a/src/pages/Main/components/Calendar/Day/EditTitleForm/index.tsx b/src/pages/Main/components/Calendar/Day/EditTitleForm/index.tsx deleted file mode 100644 index f3db413..0000000 --- a/src/pages/Main/components/Calendar/Day/EditTitleForm/index.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import {yupResolver} from '@hookform/resolvers/yup'; -import {FormProvider, SubmitHandler, useForm} from 'react-hook-form'; -import {useDispatch} from 'react-redux'; - -import Button from '@/components/Button'; -import InputControl from '@/components/formInputs/InputControl'; -import CheckIcon from '@/icons/CheckIcon'; -import CloseIcon from '@/icons/CloseIcon'; -import {updateColumnTitle} from '@/redux/columns/columnsSlice'; - -import {validation} from './form'; -import {TEditTitleFormProps, TFormValues} from './types'; - -const EditTitleForm = ({column, onCloseEditMode}: TEditTitleFormProps) => { - const dispatch = useDispatch(); - const methods = useForm({ - resolver: yupResolver(validation), - defaultValues: { - title: column.title, - }, - mode: 'onSubmit', - }); - - const { - control, - handleSubmit, - formState: {isDirty}, - } = methods; - - const onSubmit: SubmitHandler = data => { - dispatch(updateColumnTitle({columnId: column.id, newTitle: data.title})); - onCloseEditMode(); - }; - - return ( - -
-
-
- -
- -
-
-
-
-
- ); -}; - -export default EditTitleForm; diff --git a/src/pages/Main/components/Calendar/Day/EditTitleForm/types.ts b/src/pages/Main/components/Calendar/Day/EditTitleForm/types.ts deleted file mode 100644 index 84ef099..0000000 --- a/src/pages/Main/components/Calendar/Day/EditTitleForm/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {TColumn} from '@/redux/columns/types'; - -export type TEditTitleFormProps = { - column: TColumn; - onCloseEditMode: () => void; -}; - -export type TFormValues = { - title: string; -}; diff --git a/src/redux/redux-provider.tsx b/src/providers/redux-provider.tsx similarity index 86% rename from src/redux/redux-provider.tsx rename to src/providers/redux-provider.tsx index 366772d..638fb79 100644 --- a/src/redux/redux-provider.tsx +++ b/src/providers/redux-provider.tsx @@ -1,7 +1,7 @@ import {PropsWithChildren} from 'react'; import {Provider} from 'react-redux'; -import store from './store'; +import store from '@/redux/store'; const ReduxProvider = ({children}: PropsWithChildren) => { return {children}; diff --git a/src/redux/columns/columnsSlice.ts b/src/redux/columns/columnsSlice.ts deleted file mode 100644 index 2bb8a7f..0000000 --- a/src/redux/columns/columnsSlice.ts +++ /dev/null @@ -1,109 +0,0 @@ -import {PayloadAction, createSlice} from '@reduxjs/toolkit'; -import {uid} from 'uid'; - -import {SliceNames} from '../types'; -import { - TAddTaskAction, - TColumn, - TColumnsState, - TDeleteColumnAction, - TDeleteTaskAction, - TEditTaskAction, - TReorderTasksAction, - TSetActiveColumnAction, - TSetActiveTaskAction, - TSetColumnTitleAction, - TSetColumnsAction, -} from './types'; - -const reducers = { - setColumns: (state: TColumnsState, action: PayloadAction) => { - state.columns = action.payload; - }, - setActiveColumn: (state: TColumnsState, action: PayloadAction) => { - state.activeColumn = action.payload; - }, - createNewColumn: (state: TColumnsState) => { - const newColumn: TColumn = { - id: uid(16), - title: `Column ${state.columns.length + 1}`, - }; - - state.columns.push(newColumn); - }, - deleteColumn: (state: TColumnsState, action: PayloadAction) => { - const updatedColumns = state.columns.filter(column => column.id !== action.payload); - - const updatedTasks = state.tasks.filter(task => task.columnId !== action.payload); - - state.tasks = updatedTasks; - state.columns = updatedColumns; - }, - updateColumnTitle: (state: TColumnsState, action: PayloadAction) => { - const {columnId, newTitle} = action.payload; - const updatedColumns = state.columns.map(column => { - if (column.id === columnId) { - return {...column, title: newTitle}; - } - return column; - }); - - state.columns = updatedColumns; - }, - addTask: (state: TColumnsState, action: PayloadAction) => { - state.tasks.push(action.payload); - }, - setActiveTask: (state: TColumnsState, action: PayloadAction) => { - state.activeTask = action.payload; - }, - deleteTask: (state: TColumnsState, action: PayloadAction) => { - const {taskId} = action.payload; - - const updatedTasks = state.tasks.filter(task => task.id !== taskId); - - state.tasks = updatedTasks; - }, - editTask: (state: TColumnsState, action: PayloadAction) => { - const {taskId, changedValues} = action.payload; - - const updatedTasks = state.tasks.map(task => { - if (task.id === taskId) { - return {...task, ...changedValues}; - } - - return task; - }); - - state.tasks = updatedTasks; - }, - reorderTasks: (state: TColumnsState, action: PayloadAction) => { - state.tasks = action.payload; - }, -}; - -const initialState: TColumnsState = { - columns: [], - tasks: [], - activeColumn: null, - activeTask: null, -}; - -const columnsSlice = createSlice({ - name: SliceNames.columnsSlice, - initialState, - reducers, -}); - -export const { - setColumns, - setActiveColumn, - createNewColumn, - deleteColumn, - updateColumnTitle, - addTask, - setActiveTask, - deleteTask, - editTask, - reorderTasks, -} = columnsSlice.actions; -export default columnsSlice; diff --git a/src/redux/columns/selectors.ts b/src/redux/columns/selectors.ts deleted file mode 100644 index 5015415..0000000 --- a/src/redux/columns/selectors.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {AppStoreState} from '../store'; -import {TId} from './types'; - -export const selectColumns = (state: AppStoreState) => state.columnsData.columns; -export const selectActiveColumn = (state: AppStoreState) => state.columnsData.activeColumn; - -export const selectAllTasks = (state: AppStoreState) => state.columnsData.tasks; -export const selectTasksByColumn = (columnId: TId) => (state: AppStoreState) => - state.columnsData.tasks.filter(task => task.columnId === columnId); -export const selectActiveTask = (state: AppStoreState) => state.columnsData.activeTask; diff --git a/src/redux/columns/types.ts b/src/redux/columns/types.ts deleted file mode 100644 index 4b4b759..0000000 --- a/src/redux/columns/types.ts +++ /dev/null @@ -1,42 +0,0 @@ -import {UniqueIdentifier} from '@dnd-kit/core'; -import {PayloadAction} from '@reduxjs/toolkit'; - -import {Nullable} from '@/services/types'; - -export type TId = UniqueIdentifier; - -export type TTask = { - id: TId; - title: string; - columnId: TId; -}; - -export type TColumn = { - id: TId; - title: string; -}; - -export type TColumnsState = { - columns: TColumn[]; - tasks: TTask[]; - activeColumn: Nullable; - activeTask: Nullable; -}; - -export type TSetColumnsAction = PayloadAction; -export type TSetActiveColumnAction = PayloadAction>; -export type TSetActiveTaskAction = PayloadAction>; -export type TDeleteColumnAction = PayloadAction; -export type TSetColumnTitleAction = PayloadAction<{ - columnId: TId; - newTitle: string; -}>; -export type TAddTaskAction = PayloadAction; -export type TDeleteTaskAction = PayloadAction<{ - taskId: TId; -}>; -export type TEditTaskAction = PayloadAction<{ - taskId: TId; - changedValues: Partial; -}>; -export type TReorderTasksAction = PayloadAction; diff --git a/src/redux/events/eventsSlice.ts b/src/redux/events/eventsSlice.ts new file mode 100644 index 0000000..358d399 --- /dev/null +++ b/src/redux/events/eventsSlice.ts @@ -0,0 +1,37 @@ +import {PayloadAction, createSlice} from '@reduxjs/toolkit'; + +import {StorageKeys} from '@/services/types'; +import {getLocalStoredValues} from '@/services/utils'; + +import {SliceNames} from '../types'; +import {TEvent, TEventsState} from './types'; + +const reducers = { + addEvent: (state: TEventsState, action: PayloadAction) => { + const {date} = action.payload; + const events = state.events || {}; + + const updatedEvents = { + ...events, + [date]: [...(events[date] || []), action.payload], + }; + + state.events = updatedEvents; + localStorage.setItem(StorageKeys.events, JSON.stringify(updatedEvents)); + }, +}; + +const initialEvents = getLocalStoredValues(StorageKeys.events, null); + +const initialState: TEventsState = { + events: initialEvents, +}; + +const eventsSlice = createSlice({ + name: SliceNames.eventsSlice, + initialState, + reducers, +}); + +export const {addEvent} = eventsSlice.actions; +export default eventsSlice; diff --git a/src/redux/events/selectors.ts b/src/redux/events/selectors.ts new file mode 100644 index 0000000..0b743a1 --- /dev/null +++ b/src/redux/events/selectors.ts @@ -0,0 +1,3 @@ +import {AppStoreState} from '../store'; + +export const selectEventsByDate = (date: string) => (state: AppStoreState) => state.eventsData.events?.[date]; diff --git a/src/redux/events/types.ts b/src/redux/events/types.ts new file mode 100644 index 0000000..e1f647a --- /dev/null +++ b/src/redux/events/types.ts @@ -0,0 +1,15 @@ +export type TId = string; + +export type TEvent = { + id: TId; + title: string; + description?: string; + date: string; + color: string; +}; + +export type TEventsState = { + events: { + [key: string]: TEvent[]; + } | null; +}; From a4e376e8c13658310d528221d8e0429bf30f36e5 Mon Sep 17 00:00:00 2001 From: Daniil Fomenko Date: Tue, 17 Jun 2025 21:56:37 +0300 Subject: [PATCH 04/11] renamed columnsSlice --- src/redux/store.ts | 4 ++-- src/redux/types.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/redux/store.ts b/src/redux/store.ts index af40d68..8705173 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -1,13 +1,13 @@ import {combineReducers, configureStore} from '@reduxjs/toolkit'; import {useDispatch} from 'react-redux'; -import counterSlice from './columns/columnsSlice'; import dateSlice from './date/dateSlice'; +import counterSlice from './events/eventsSlice'; import overflowSlice from './overflow/overflowSlice'; import {PreloadedState, SliceNames} from './types'; const combinedReducer = combineReducers({ - [SliceNames.columnsSlice]: counterSlice.reducer, + [SliceNames.eventsSlice]: counterSlice.reducer, [SliceNames.dateSlice]: dateSlice.reducer, [SliceNames.overflowSlice]: overflowSlice.reducer, }); diff --git a/src/redux/types.ts b/src/redux/types.ts index 955109a..18c40c4 100644 --- a/src/redux/types.ts +++ b/src/redux/types.ts @@ -3,7 +3,7 @@ export interface PreloadedState { } export enum SliceNames { - columnsSlice = 'columnsData', + eventsSlice = 'eventsData', dateSlice = 'dateData', overflowSlice = 'overflowData', } From 4eddad3dc4edc81cbd187200a6f03d9e23fbcaea Mon Sep 17 00:00:00 2001 From: Daniil Fomenko Date: Tue, 17 Jun 2025 21:58:06 +0300 Subject: [PATCH 05/11] added calendar provider --- src/context/calendar-context.ts | 3 + src/context/hooks.ts | 13 ++ src/main.tsx | 2 +- src/pages/Main/components/Calendar/index.tsx | 175 +------------------ src/pages/Main/index.tsx | 5 +- src/providers/calendar-provider.tsx | 11 ++ 6 files changed, 39 insertions(+), 170 deletions(-) create mode 100644 src/context/calendar-context.ts create mode 100644 src/context/hooks.ts create mode 100644 src/providers/calendar-provider.tsx diff --git a/src/context/calendar-context.ts b/src/context/calendar-context.ts new file mode 100644 index 0000000..3ae4497 --- /dev/null +++ b/src/context/calendar-context.ts @@ -0,0 +1,3 @@ +import {RefObject, createContext} from 'react'; + +export const CalendarContext = createContext | null>(null); diff --git a/src/context/hooks.ts b/src/context/hooks.ts new file mode 100644 index 0000000..596a95e --- /dev/null +++ b/src/context/hooks.ts @@ -0,0 +1,13 @@ +import {useContext} from 'react'; + +import {CalendarContext} from './calendar-context'; + +export const useCalendarContext = () => { + const context = useContext(CalendarContext); + + if (!context) { + throw new Error('useCalendarContext must be used within a CalendarProvider'); + } + + return context; +}; diff --git a/src/main.tsx b/src/main.tsx index 9cd4c44..c84bb57 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -3,7 +3,7 @@ import {createRoot} from 'react-dom/client'; import App from './App'; import './index.css'; import './moment.config'; -import ReduxProvider from './redux/redux-provider'; +import ReduxProvider from './providers/redux-provider'; createRoot(document.getElementById('root')!).render( diff --git a/src/pages/Main/components/Calendar/index.tsx b/src/pages/Main/components/Calendar/index.tsx index 0bc246c..c62559c 100644 --- a/src/pages/Main/components/Calendar/index.tsx +++ b/src/pages/Main/components/Calendar/index.tsx @@ -1,178 +1,17 @@ -import { - DndContext, - DragEndEvent, - DragOverEvent, - DragStartEvent, - PointerSensor, - pointerWithin, - useSensor, - useSensors, -} from '@dnd-kit/core'; -import {arrayMove} from '@dnd-kit/sortable'; -import {useCallback} from 'react'; -import {useDispatch, useSelector} from 'react-redux'; - -import {reorderTasks, setActiveColumn, setActiveTask, setColumns} from '@/redux/columns/columnsSlice'; -import {selectActiveColumn, selectActiveTask, selectAllTasks, selectColumns} from '@/redux/columns/selectors'; +import {useCalendarContext} from '@/context/hooks'; import DaysList from './DaysList'; import WeekDays from './WeekDays'; -import {CurrentDraggableType} from './types'; - -// TODO: add days from prev and next month -// TODO: add mobile view const Calendar = () => { - const dispatch = useDispatch(); - const columns = useSelector(selectColumns); - const activeColumn = useSelector(selectActiveColumn); - const activeTask = useSelector(selectActiveTask); - const tasks = useSelector(selectAllTasks); - - const sensors = useSensors( - useSensor(PointerSensor, { - activationConstraint: { - distance: 3, // 3px - }, - }), - ); - - const onClearActiveElements = useCallback(() => { - if (activeColumn) { - dispatch(setActiveColumn(null)); - } - - if (activeTask) { - dispatch(setActiveTask(null)); - } - }, [activeColumn, activeTask, dispatch]); - - const handleDragStart = (event: DragStartEvent) => { - const {current} = event.active.data; - - if (current?.type === CurrentDraggableType.Column) { - dispatch(setActiveColumn(current?.column)); - } - - if (current?.type === CurrentDraggableType.Task) { - dispatch(setActiveTask(current?.task)); - } - }; - - const initDragEvent = (event: DragEndEvent | DragOverEvent) => { - const {active, over} = event; - - const activeId = active.id; - const overId = over ? over.id : null; - - const activeElement = active.data.current; - const overElement = over?.data.current; - - const activeType = activeElement?.type; - const overType = overElement?.type; - - if (!over || activeId === overId) { - return; - } - - return { - activeId, - overId, - activeElement, - overElement, - activeType, - overType, - }; - }; - - const handleDragEnd = useCallback( - (event: DragEndEvent) => { - onClearActiveElements(); - - const dragEvent = initDragEvent(event); - if (!dragEvent) return; - - const {activeId, overId, activeElement, overElement, activeType, overType} = dragEvent; - - const isActiveATask = activeType === CurrentDraggableType.Task; - const isOverAColumn = overType === CurrentDraggableType.Column; - - // moving columns - const activeColumnIndex = columns.findIndex(column => column.id === activeId); - const overColumnIndex = columns.findIndex(column => column.id === overId); - - if (isActiveATask && isOverAColumn) { - if (activeElement?.task.columnId === overElement?.column.id) return; - } - - dispatch(setColumns(arrayMove(columns, activeColumnIndex, overColumnIndex))); - }, - [columns, dispatch, onClearActiveElements], - ); - - const handleDragOver = (event: DragOverEvent) => { - const dragEvent = initDragEvent(event); - if (!dragEvent) return; - - const {activeId, overId, activeType, overType} = dragEvent; - - const isActiveATask = activeType === CurrentDraggableType.Task; - const isOverATask = overType === CurrentDraggableType.Task; - const isOverAColumn = overType === CurrentDraggableType.Column; - - if (!isActiveATask) return; - - // dropping task over another task - if (isActiveATask && isOverATask) { - const activeIndex = tasks.findIndex(task => task.id === activeId); - const overIndex = tasks.findIndex(task => task.id === overId); - - const updatedTasks = tasks.map(task => { - if (task.id === activeTask?.id) { - return { - ...activeTask, - columnId: tasks[overIndex].columnId, - }; - } - - return task; - }); - - dispatch(reorderTasks(arrayMove(updatedTasks, activeIndex, overIndex))); - } - - // dropping task over another column - if (isActiveATask && isOverAColumn) { - const activeIndex = tasks.findIndex(task => task.id === activeId); - - const updatedTasks = tasks.map(task => { - if (task.id === activeTask?.id) { - return { - ...activeTask, - columnId: overId!, - }; - } - - return task; - }); - - dispatch(reorderTasks(arrayMove(updatedTasks, activeIndex, activeIndex))); - } - }; + const containerRef = useCalendarContext(); return ( -
- -
- - -
-
+
+
+ + +
); }; diff --git a/src/pages/Main/index.tsx b/src/pages/Main/index.tsx index 2cd1bae..78c8c15 100644 --- a/src/pages/Main/index.tsx +++ b/src/pages/Main/index.tsx @@ -1,4 +1,5 @@ import Container from '@/components/Container'; +import CalendarProvider from '@/providers/calendar-provider'; import Calendar from './components/Calendar'; import Header from './components/Header'; @@ -9,7 +10,9 @@ const MainPage = () => {
- + + +
); diff --git a/src/providers/calendar-provider.tsx b/src/providers/calendar-provider.tsx new file mode 100644 index 0000000..1540d62 --- /dev/null +++ b/src/providers/calendar-provider.tsx @@ -0,0 +1,11 @@ +import {PropsWithChildren, useRef} from 'react'; + +import {CalendarContext} from '@/context/calendar-context'; + +const CalendarProvider = ({children}: PropsWithChildren) => { + const containerRef = useRef(null); + + return {children}; +}; + +export default CalendarProvider; From 4108b8637674d75087c87be63ad0498b3dc7865f Mon Sep 17 00:00:00 2001 From: Daniil Fomenko Date: Tue, 17 Jun 2025 21:59:04 +0300 Subject: [PATCH 06/11] added day element with events list --- .../Day/DayEventsList/Event/index.tsx | 88 +++---------------- .../Calendar/Day/DayEventsList/Event/types.ts | 8 +- .../DayEventsList/HiddenEventsList/index.tsx | 25 ++++++ .../DayEventsList/HiddenEventsList/types.ts | 8 ++ .../Day/DayEventsList/RemainedItems/index.tsx | 21 +++++ .../Day/DayEventsList/RemainedItems/types.ts | 5 ++ .../Calendar/Day/DayEventsList/index.tsx | 81 ++++++++++++++--- .../Calendar/Day/DayEventsList/types.ts | 4 +- .../Main/components/Calendar/Day/index.tsx | 64 +++++++------- 9 files changed, 183 insertions(+), 121 deletions(-) create mode 100644 src/pages/Main/components/Calendar/Day/DayEventsList/HiddenEventsList/index.tsx create mode 100644 src/pages/Main/components/Calendar/Day/DayEventsList/HiddenEventsList/types.ts create mode 100644 src/pages/Main/components/Calendar/Day/DayEventsList/RemainedItems/index.tsx create mode 100644 src/pages/Main/components/Calendar/Day/DayEventsList/RemainedItems/types.ts diff --git a/src/pages/Main/components/Calendar/Day/DayEventsList/Event/index.tsx b/src/pages/Main/components/Calendar/Day/DayEventsList/Event/index.tsx index c94e251..7d933c3 100644 --- a/src/pages/Main/components/Calendar/Day/DayEventsList/Event/index.tsx +++ b/src/pages/Main/components/Calendar/Day/DayEventsList/Event/index.tsx @@ -1,80 +1,20 @@ -import {useSortable} from '@dnd-kit/sortable'; -import {CSS} from '@dnd-kit/utilities'; -import {useState} from 'react'; -import {useDispatch} from 'react-redux'; +import Tooltip from '@/components/Tooltip'; -import Button from '@/components/Button'; -import Modal from '@/components/Modal'; -import BoardItemForm from '@/components/forms/BoardItemForm'; -import DeleteIcon from '@/icons/DeleteIcon'; -import EditIcon from '@/icons/EditIcon'; -import {deleteTask} from '@/redux/columns/columnsSlice'; -import {useOpeningItem} from '@/services/hooks'; - -import {TTasksProps} from './types'; - -const Event = ({task}: TTasksProps) => { - const [isHover, setIsHover] = useState(false); - - const dispatch = useDispatch(); - const {ref, isOpen, handleClose: handleModalClose, handleOpen: handleModalOpen} = useOpeningItem(); - const {setNodeRef, attributes, listeners, transform, transition, isDragging} = useSortable({ - id: task.id, - data: {type: 'Task', task}, - disabled: isOpen, - }); - - const style = { - transition, - transform: CSS.Transform.toString(transform), - }; - - const isDraggingStyles = isDragging ? 'opacity-50 border-2 border-sky-500' : ''; - - const onDeleteTask = () => { - dispatch(deleteTask({taskId: task.id})); - }; +import {TEventProps} from './types'; +const Event = ({event, eventRef}: TEventProps) => { return ( -
setIsHover(true)} - onMouseLeave={() => setIsHover(false)}> - {!isDragging && ( - <> - {task.title} - {isHover && ( -
-
- )} - - )} - - {isOpen && ( - - {}} - handleModalClose={handleModalClose} - /> - - )} -
+ + } + triggerElementClassName="w-auto h-auto" + tooltipClassnames="whitespace-nowrap text-ellipsis overflow-hidden"> + {event.title} + ); }; diff --git a/src/pages/Main/components/Calendar/Day/DayEventsList/Event/types.ts b/src/pages/Main/components/Calendar/Day/DayEventsList/Event/types.ts index abef00b..d5b2399 100644 --- a/src/pages/Main/components/Calendar/Day/DayEventsList/Event/types.ts +++ b/src/pages/Main/components/Calendar/Day/DayEventsList/Event/types.ts @@ -1,3 +1,7 @@ -import {TTask} from '@/redux/columns/types'; +import {TEvent} from '@/redux/events/types'; -export type TTasksProps = {task: TTask}; +export type TEventProps = { + event: TEvent; + eventRef: (el: HTMLDivElement | null) => void; + eventIndex: number; +}; diff --git a/src/pages/Main/components/Calendar/Day/DayEventsList/HiddenEventsList/index.tsx b/src/pages/Main/components/Calendar/Day/DayEventsList/HiddenEventsList/index.tsx new file mode 100644 index 0000000..f622132 --- /dev/null +++ b/src/pages/Main/components/Calendar/Day/DayEventsList/HiddenEventsList/index.tsx @@ -0,0 +1,25 @@ +import Event from '../Event'; +import {THiddenEventsListProps} from './types'; + +const HiddenEventsList = ({events, eventsRefs, eventsContainerRef}: THiddenEventsListProps) => { + return ( +
+ {events.map((event, i) => ( + { + if (el && eventsRefs.current) { + eventsRefs.current[i] = el; + } + }} + eventIndex={i} + /> + ))} +
+ ); +}; + +export default HiddenEventsList; diff --git a/src/pages/Main/components/Calendar/Day/DayEventsList/HiddenEventsList/types.ts b/src/pages/Main/components/Calendar/Day/DayEventsList/HiddenEventsList/types.ts new file mode 100644 index 0000000..e88d32c --- /dev/null +++ b/src/pages/Main/components/Calendar/Day/DayEventsList/HiddenEventsList/types.ts @@ -0,0 +1,8 @@ +import {RefObject} from 'react'; + +import {TDayEventsListProps} from '../types'; + +export type THiddenEventsListProps = { + eventsRefs: RefObject; + eventsContainerRef: RefObject; +} & TDayEventsListProps; diff --git a/src/pages/Main/components/Calendar/Day/DayEventsList/RemainedItems/index.tsx b/src/pages/Main/components/Calendar/Day/DayEventsList/RemainedItems/index.tsx new file mode 100644 index 0000000..0885dfd --- /dev/null +++ b/src/pages/Main/components/Calendar/Day/DayEventsList/RemainedItems/index.tsx @@ -0,0 +1,21 @@ +import Tooltip from '@/components/Tooltip'; + +import {TRemainedItemsProps} from './types'; + +const RemainedItems = ({items}: TRemainedItemsProps) => { + return ( + +{items.length}} + triggerElementClassName="w-auto flex justify-end"> +
+ {items.map(item => ( +
+ {item.title} +
+ ))} +
+
+ ); +}; + +export default RemainedItems; diff --git a/src/pages/Main/components/Calendar/Day/DayEventsList/RemainedItems/types.ts b/src/pages/Main/components/Calendar/Day/DayEventsList/RemainedItems/types.ts new file mode 100644 index 0000000..5a63a0d --- /dev/null +++ b/src/pages/Main/components/Calendar/Day/DayEventsList/RemainedItems/types.ts @@ -0,0 +1,5 @@ +import {TEvent} from '@/redux/events/types'; + +export type TRemainedItemsProps = { + items: TEvent[]; +}; diff --git a/src/pages/Main/components/Calendar/Day/DayEventsList/index.tsx b/src/pages/Main/components/Calendar/Day/DayEventsList/index.tsx index 88bc4e7..ce9175c 100644 --- a/src/pages/Main/components/Calendar/Day/DayEventsList/index.tsx +++ b/src/pages/Main/components/Calendar/Day/DayEventsList/index.tsx @@ -1,21 +1,78 @@ -import {SortableContext} from '@dnd-kit/sortable'; -import {useMemo} from 'react'; +import {useCallback, useEffect, useRef, useState} from 'react'; -import Task from './Event'; -import {TColumnTasksListProps} from './types'; +import {TEvent} from '@/redux/events/types'; -const ColumnTasksList = ({tasks}: TColumnTasksListProps) => { - const tasksIds = useMemo(() => tasks.map(task => task.id), [tasks]); +import Event from './Event'; +import HiddenEventsList from './HiddenEventsList'; +import RemainedItems from './RemainedItems'; +import {TDayEventsListProps} from './types'; + +const DayEventsList = ({events}: TDayEventsListProps) => { + const [visibleEvents, setVisibleEvents] = useState([]); + const [hiddenEvents, setHiddenEvents] = useState([]); + + const eventsContainerRef = useRef(null); + const eventsRefs = useRef([]); + + const handleResize = useCallback(() => { + if (!eventsContainerRef.current) return; + + const eventsContainerSizes = eventsContainerRef.current.getBoundingClientRect(); + + const hiddenEvents: TEvent[] = []; + const visibleEvents: TEvent[] = []; + + for (let i = 0; i < events.length; i++) { + const eventRef = eventsRefs.current[i]; + const eventRefSizes = eventRef.getBoundingClientRect(); + + const isFits = + eventRefSizes.right < eventsContainerSizes.right && eventRefSizes.left < eventsContainerSizes.right; + + if (isFits) { + visibleEvents.push(events[i]); + } else { + hiddenEvents.push(events[i]); + } + } + + setVisibleEvents(visibleEvents); + setHiddenEvents(hiddenEvents); + }, [events]); + + useEffect(() => { + const observer = new ResizeObserver(() => { + requestAnimationFrame(() => handleResize()); + }); + + if (eventsContainerRef.current) { + observer.observe(eventsContainerRef.current); + } + + return () => observer.disconnect(); + }, [handleResize]); return ( -
- - {tasks.map(task => ( - +
+ +
+ {visibleEvents.map((event, i) => ( + { + if (el) { + eventsRefs.current[i] = el; + } + }} + eventIndex={i} + /> ))} - +
+ + {!!hiddenEvents.length && }
); }; -export default ColumnTasksList; +export default DayEventsList; diff --git a/src/pages/Main/components/Calendar/Day/DayEventsList/types.ts b/src/pages/Main/components/Calendar/Day/DayEventsList/types.ts index 44a3d05..9e213bd 100644 --- a/src/pages/Main/components/Calendar/Day/DayEventsList/types.ts +++ b/src/pages/Main/components/Calendar/Day/DayEventsList/types.ts @@ -1,3 +1,3 @@ -import {TTask} from '@/redux/columns/types'; +import {TEvent} from '@/redux/events/types'; -export type TColumnTasksListProps = {tasks: TTask[]}; +export type TDayEventsListProps = {events: TEvent[]}; diff --git a/src/pages/Main/components/Calendar/Day/index.tsx b/src/pages/Main/components/Calendar/Day/index.tsx index c712874..0f59afb 100644 --- a/src/pages/Main/components/Calendar/Day/index.tsx +++ b/src/pages/Main/components/Calendar/Day/index.tsx @@ -1,63 +1,65 @@ import {motion} from 'framer-motion'; import moment from 'moment'; -import {useCallback} from 'react'; +import {useState} from 'react'; import {useSelector} from 'react-redux'; -// import Button from '@/components/Button'; +import Button from '@/components/Button'; import Modal from '@/components/Modal'; -import BoardItemForm from '@/components/forms/BoardItemForm'; -// import AddIcon from '@/icons/AddIcon'; +import BoardEventForm from '@/components/forms/BoardEventForm'; +import AddIcon from '@/icons/AddIcon'; import {selectFullDate} from '@/redux/date/selectors'; +import {selectEventsByDate} from '@/redux/events/selectors'; +import {formatDate, getDate} from '@/services/dateUtils'; import {useOpeningItem} from '@/services/hooks'; -import {formatDate, getDate} from '@/services/utils'; +import DayEventsList from './DayEventsList'; import {TDayProps} from './types'; const Day = ({date}: TDayProps) => { - const {ref, isOpen, handleClose: handleModalClose /*handleOpen: handleModalOpen*/} = useOpeningItem(); + const [isHover, setIsHover] = useState(false); + const {ref, isOpen, handleClose: handleModalClose, handleOpen: handleModalOpen} = useOpeningItem(); - const curentDate = useSelector(selectFullDate); - const curentMonth = formatDate(moment(curentDate), 'M'); + const events = useSelector(selectEventsByDate(date)); + const fullDate = useSelector(selectFullDate); + const currentMonth = formatDate(moment(fullDate), 'M'); const dateMonth = formatDate(moment(date), 'M'); const currentDate = formatDate(getDate(new Date()), 'YYYY-MM-DD'); const day = formatDate(getDate(date), 'DD'); - const onAddNewTaskSubmit = useCallback( - (data: unknown) => { - handleModalClose(); - - // eslint-disable-next-line no-console - console.log(data); - }, - [handleModalClose], - ); - return ( setIsHover(true)} + onMouseLeave={() => setIsHover(false)} initial={{opacity: 0}} animate={{opacity: 1}} transition={{duration: 0.5, ease: 'easeOut'}} - className={`border border-secondaryBackgroundColor rounded-md p-1 md:p-3 ${dateMonth !== curentMonth ? 'bg-secondaryBackgroundColor' : ''} cursor-pointer hover:bg-secondaryBackgroundColorHover`}> -
- - - -
+ className={`flex flex-col relative justify-between border border-secondaryBackgroundColor rounded-md p-1 md:p-3 ${dateMonth !== currentMonth ? 'bg-secondaryBackgroundColor' : ''} cursor-pointer hover:bg-secondaryBackgroundColorHover`}> +
+
+ + + +
- {/* */} + {isHover && ( +
- {/*
-
*/} + {events?.length && } {isOpen && ( - From 7e5444fb67f9201c83ef57f1907f29ded68c2f22 Mon Sep 17 00:00:00 2001 From: Daniil Fomenko Date: Tue, 17 Jun 2025 21:59:46 +0300 Subject: [PATCH 07/11] added tooltip --- src/components/Tooltip/index.tsx | 73 ++++++++++++++++++++++++++++++++ src/components/Tooltip/types.ts | 7 +++ 2 files changed, 80 insertions(+) create mode 100644 src/components/Tooltip/index.tsx create mode 100644 src/components/Tooltip/types.ts diff --git a/src/components/Tooltip/index.tsx b/src/components/Tooltip/index.tsx new file mode 100644 index 0000000..af9652c --- /dev/null +++ b/src/components/Tooltip/index.tsx @@ -0,0 +1,73 @@ +import cn from 'classnames'; +import classNames from 'classnames'; +import {cloneElement, memo, useCallback, useEffect, useRef, useState} from 'react'; + +import {useCalendarContext} from '@/context/hooks'; + +import {TooltipProps} from './types'; + +// Tooltip component gets triggerElement as rendered element for hover +// Tooltip component gets children as tooltip content +const Tooltip = ({triggerElement, children, triggerElementClassName = '', tooltipClassnames = ''}: TooltipProps) => { + const [isFitsContainer, setIsFitsContainer] = useState(true); + const [isOpened, setIsOpened] = useState(false); + + const containerRef = useCalendarContext(); + const tooltipRef = useRef(null); + + const handleMouseEnter = () => setIsOpened(true); + const handleMouseLeave = () => setIsOpened(false); + + const onTooltipHover = useCallback(() => { + if (isOpened) { + const containerSizes = containerRef.current?.getBoundingClientRect(); + const tooltipSizes = tooltipRef.current?.getBoundingClientRect(); + + if (!containerSizes || !tooltipSizes) return; + + // 12 - right padding of the block + if (tooltipSizes.right > containerSizes.right - 12) { + setIsFitsContainer(false); + } + } + }, [containerRef, isOpened]); + + useEffect(() => { + onTooltipHover(); + }, [onTooltipHover]); + + // clone trigger element and add mouse events + const triggerWithHandlers = cloneElement(triggerElement, { + onMouseEnter: (e: MouseEvent) => { + triggerElement.props.onMouseEnter?.(e); + handleMouseEnter(); + }, + onMouseLeave: (e: MouseEvent) => { + triggerElement.props.onMouseLeave?.(e); + handleMouseLeave(); + }, + }); + + return ( +
+ {triggerWithHandlers} + + {isOpened && ( +
+ {children} +
+ )} +
+ ); +}; + +export default memo(Tooltip); diff --git a/src/components/Tooltip/types.ts b/src/components/Tooltip/types.ts new file mode 100644 index 0000000..d527471 --- /dev/null +++ b/src/components/Tooltip/types.ts @@ -0,0 +1,7 @@ +import {PropsWithChildren, ReactElement} from 'react'; + +export type TooltipProps = PropsWithChildren<{ + triggerElement: ReactElement; + triggerElementClassName?: string; + tooltipClassnames?: string; +}>; From f73229b3b7cefce3726ef03400b1cacf83f4663d Mon Sep 17 00:00:00 2001 From: Daniil Fomenko Date: Tue, 17 Jun 2025 22:03:10 +0300 Subject: [PATCH 08/11] changed new event form --- .../forms/NewEventByDateForm/form.ts | 14 +++----- .../forms/NewEventByDateForm/index.tsx | 34 +++++++++++++++---- .../forms/NewEventByDateForm/types.ts | 7 ++-- 3 files changed, 36 insertions(+), 19 deletions(-) diff --git a/src/components/forms/NewEventByDateForm/form.ts b/src/components/forms/NewEventByDateForm/form.ts index d50a0ec..078047b 100644 --- a/src/components/forms/NewEventByDateForm/form.ts +++ b/src/components/forms/NewEventByDateForm/form.ts @@ -1,17 +1,11 @@ import {object, string} from 'yup'; -export const defaultValues = { - eventName: '', - eventYear: '', - eventMonth: '', - eventDay: '', -}; - export const validation = object().shape({ eventName: string() .required('Name is required') .test('empty-check', 'Event name can not be empty string.', name => !!name.trim().length), - eventYear: string(), - eventMonth: string(), - eventDay: string(), + eventYear: string().required(), + eventMonth: string().required(), + eventDay: string().required(), + eventDescription: string(), }); diff --git a/src/components/forms/NewEventByDateForm/index.tsx b/src/components/forms/NewEventByDateForm/index.tsx index afeed9f..f329175 100644 --- a/src/components/forms/NewEventByDateForm/index.tsx +++ b/src/components/forms/NewEventByDateForm/index.tsx @@ -9,12 +9,16 @@ import InputControl from '@/components/formInputs/InputControl'; import CheckIcon from '@/icons/CheckIcon'; import CloseIcon from '@/icons/CloseIcon'; import {selectDay, selectMonth, selectYear} from '@/redux/date/selectors'; -import {getDays, getMonthsOptions, getYearsOptions} from '@/services/utils'; +import {addEvent} from '@/redux/events/eventsSlice'; +import {useAppDispatch} from '@/redux/store'; +import {createDate, getDays, getMonthsOptions, getYearsOptions} from '@/services/dateUtils'; +import {createEvent} from '@/services/utils'; import {validation} from './form'; import {TFormValues, TNewEventByDateFormProps} from './types'; const NewEventByDateForm = ({handleModalClose}: TNewEventByDateFormProps) => { + const dispatch = useAppDispatch(); const year = useSelector(selectYear); const month = useSelector(selectMonth); const day = useSelector(selectDay); @@ -26,6 +30,7 @@ const NewEventByDateForm = ({handleModalClose}: TNewEventByDateFormProps) => { eventYear: year || '', eventMonth: month || '', eventDay: day || '', + eventDescription: '', }, mode: 'onSubmit', shouldUnregister: false, @@ -48,12 +53,18 @@ const NewEventByDateForm = ({handleModalClose}: TNewEventByDateFormProps) => { [formYearValue, formMonthValue], ); - const onSubmit: SubmitHandler = newEvent => { + const onSubmit: SubmitHandler = eventData => { + const {eventName, eventYear, eventMonth, eventDay, eventDescription} = eventData; + handleModalClose(); - // TODO: trim eventName value - // eslint-disable-next-line no-console - console.log(newEvent); + const newEvent = createEvent({ + eventName, + date: createDate(+eventYear, eventMonth, +eventDay), + description: eventDescription, + }); + + dispatch(addEvent(newEvent)); }; return ( @@ -76,7 +87,18 @@ const NewEventByDateForm = ({handleModalClose}: TNewEventByDateFormProps) => {
- + + +