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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/components/common/EventForm/form.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import {object, string} from 'yup';
import {mixed, object, string} from 'yup';

import {CalendarsNames} from '@/services/types';

export const validation = object().shape({
eventName: string()
eventTitle: string()
.required('Name is required')
.test('empty-check', 'Event name can not be empty string.', name => !!name.trim().length),
eventYear: string().required(),
eventMonth: string().required(),
eventDay: string().required(),
eventDescription: string(),
eventCalendar: mixed<CalendarsNames>().oneOf(Object.values(CalendarsNames)).required('Calendar is required'),
});
82 changes: 54 additions & 28 deletions src/components/common/EventForm/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {yupResolver} from '@hookform/resolvers/yup';
import cn from 'classnames';
import classNames from 'classnames';
import moment from 'moment';
import {useMemo} from 'react';
import {useCallback, useMemo} from 'react';
import {FormProvider, SubmitHandler, useForm} from 'react-hook-form';
import {useSelector} from 'react-redux';

Expand All @@ -14,9 +14,12 @@ import CloseIcon from '@/components/ui/icons/CloseIcon';
import EditIcon from '@/components/ui/icons/EditIcon';
import {selectDay, selectMonth, selectYear} from '@/redux/date/selectors';
import {addEvent, editEvent} from '@/redux/events/eventsSlice';
import {selectSelectedCalendar} from '@/redux/myCalendars/selectors';
import {useAppDispatch} from '@/redux/store';
import {defaultCalendars} from '@/services/constants';
import {createDate, formatDate, getDays, getMonthsOptions, getYearsOptions} from '@/services/dateUtils';
import {createEventObj, editEventObj} from '@/services/eventUtils';
import {CalendarsNames} from '@/services/types';

import {validation} from './form';
import {FormActionType, TEventFormProps, TFormValues} from './types';
Expand All @@ -26,22 +29,16 @@ const EventForm = ({actionType = FormActionType.create, formTitle, event, date,
const year = useSelector(selectYear);
const month = useSelector(selectMonth);
const day = useSelector(selectDay);

const defaultValues = event
? {
eventName: event.title,
eventDescription: event.description,
eventYear: formatDate(moment(event.date), 'YYYY'),
eventMonth: formatDate(moment(event.date), 'MMMM'),
eventDay: formatDate(moment(event.date), 'DD'),
}
: {
eventName: '',
eventYear: formatDate(moment(date), 'YYYY') || year || '',
eventMonth: formatDate(moment(date), 'MMMM') || month || '',
eventDay: formatDate(moment(date), 'DD') || day || '',
eventDescription: '',
};
const selectedCalendar = useSelector(selectSelectedCalendar);

const defaultValues = {
eventTitle: event?.title ?? '',
eventDescription: event?.description ?? '',
eventYear: formatDate(moment(event?.date || date), 'YYYY') || year || '',
eventMonth: formatDate(moment(event?.date || date), 'MMMM') || month || '',
eventDay: formatDate(moment(event?.date || date), 'DD') || day || '',
eventCalendar: event?.eventCalendar ?? CalendarsNames.personal,
};

const methods = useForm<TFormValues>({
resolver: yupResolver(validation),
Expand All @@ -66,32 +63,37 @@ const EventForm = ({actionType = FormActionType.create, formTitle, event, date,
() => getDays(formYearValue || '', formMonthValue || ''),
[formYearValue, formMonthValue],
);
const calendarOptions = Object.values(CalendarsNames);

const handleCreateEvent = (eventData: TFormValues) => {
const {eventName, eventYear, eventMonth, eventDay, eventDescription} = eventData;
const {eventTitle, eventYear, eventMonth, eventDay, eventDescription, eventCalendar} = eventData;

const newEvent = createEventObj({
eventName,
title: eventTitle,
date: createDate(+eventYear, eventMonth, +eventDay),
description: eventDescription,
eventCalendar,
isDisabled: !selectedCalendar ? false : selectedCalendar !== eventCalendar,
});

dispatch(addEvent(newEvent));
};

const handleEditEvent = (eventData: TFormValues) => {
const {eventName, eventYear, eventMonth, eventDay, eventDescription} = eventData;
const {eventTitle, eventYear, eventMonth, eventDay, eventDescription, eventCalendar} = eventData;

const editedEvent = editEventObj({
event,
updatedEvent: {
eventName,
title: eventTitle,
date: createDate(+eventYear, eventMonth, +eventDay),
description: eventDescription,
eventCalendar,
isDisabled: !selectedCalendar ? event?.isDisabled || false : selectedCalendar !== eventCalendar,
},
});

dispatch(editEvent(editedEvent));
dispatch(editEvent({oldEvent: event!, newEvent: editedEvent}));
};

const onSubmit: SubmitHandler<TFormValues> = eventData => {
Expand All @@ -104,6 +106,17 @@ const EventForm = ({actionType = FormActionType.create, formTitle, event, date,
handleModalClose();
};

const customEventCalendarOption = useCallback((option: string) => {
const currentCalendar = defaultCalendars.find(c => c.name === option);

return (
<div className="w-full flex items-center gap-2">
<div className="w-2 h-2 rounded-full" style={{backgroundColor: currentCalendar?.itemColor}} />
<span>{option}</span>
</div>
);
}, []);

return (
<div>
<Button
Expand All @@ -117,27 +130,40 @@ const EventForm = ({actionType = FormActionType.create, formTitle, event, date,
<FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex flex-col items-center justify-between gap-[20px] w-auto px-[30px] py-[20px]">
{!date && (
<div className="flex gap-2 relative self-stretch max-md:flex-col">
{!date ? (
<div className="grid grid-cols-2 gap-2 relative self-stretch max-md:grid-cols-1">
<DropdownControl control={control} options={monthsOptions} name="eventMonth" />
<DropdownControl control={control} options={yearsOptions} name="eventYear" />
<DropdownControl control={control} options={daysOptions} name="eventDay" />
<DropdownControl
control={control}
options={calendarOptions}
name="eventCalendar"
customOption={customEventCalendarOption}
/>
</div>
) : (
<DropdownControl
control={control}
options={calendarOptions}
name="eventCalendar"
customOption={customEventCalendarOption}
/>
)}

<div className="w-full flex flex-col gap-2">
<InputControl autoFocus control={control} name="eventName" placeholder="Enter required name" />
<InputControl autoFocus control={control} name="eventTitle" placeholder="Enter required name" />
<TextareaControl control={control} name="eventDescription" placeholder="Enter optional description" />
</div>

<Button
className={cn(
className={classNames(
{
'border-sky-500 text-sky-500': isDirty,
},
'text-sm p-2 max-md:w-full',
)}
disabled={!isDirty || !!errors.eventName}
disabled={!isDirty || !!errors.eventTitle}
text={actionType === FormActionType.edit ? 'Edit' : 'Create'}
endIcon={actionType === FormActionType.edit ? <EditIcon size="size-4" /> : <CheckIcon size="size-4" />}
type="submit"
Expand Down
4 changes: 3 additions & 1 deletion src/components/common/EventForm/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {TEvent} from '@/redux/events/types';
import {CalendarsNames} from '@/services/types';

export enum FormActionType {
edit = 'edit',
Expand All @@ -14,9 +15,10 @@ export type TEventFormProps = {
};

export type TFormValues = {
eventName: string;
eventTitle: string;
eventYear: string;
eventMonth: string;
eventDay: string;
eventCalendar: CalendarsNames;
eventDescription?: string;
};
51 changes: 33 additions & 18 deletions src/components/common/EventsList/EventsListItem/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,48 +9,63 @@ import DeleteIcon from '@/components/ui/icons/DeleteIcon';
import EditIcon from '@/components/ui/icons/EditIcon';
import {useOpeningItem} from '@/hooks/useOpeningItem';
import {deleteEvent} from '@/redux/events/eventsSlice';
import {TEvent} from '@/redux/events/types';
import {useAppDispatch} from '@/redux/store';
import {getCalendarColor} from '@/services/calendars';
import {cx} from '@/services/utils';

const EventsListItem = ({event, showItemControls}: {event: TEvent; showItemControls: boolean}) => {
import {IEventsListItemProps} from './types';

const EventsListItem = ({event, showItemControls, isDisabled}: IEventsListItemProps) => {
const dispatch = useAppDispatch();
const {ref, isOpen, handleClose, handleOpen} = useOpeningItem();

const calendarColor = getCalendarColor(event.eventCalendar);

const handleDeleteEvent = useCallback(() => {
dispatch(deleteEvent(event.id));
}, [dispatch, event.id]);
dispatch(deleteEvent(event));
}, [dispatch, event]);

return (
<>
<div
className="relative flex items-center gap-6 max-md:gap-4 rounded-lg px-4 py-3"
style={{background: `rgb(from ${event.color} r g b / 0.15)`, borderLeft: `2px solid ${event.color}`}}>
className={cx(
'relative flex items-center gap-6 max-md:gap-4 rounded-lg px-4 py-3 transition-opacity overflow-auto',
{
'opacity-10 pointer-events-none cursor-not-allowed select-none': isDisabled,
},
)}
style={{background: `rgb(from ${calendarColor} r g b / 0.3)`, borderLeft: `2px solid ${calendarColor}`}}>
<div className="flex-1">
<h4 className="text-lg font-semibold">{event.title}</h4>
<h4 className="text-lg font-semibold break-all">{event.title}</h4>

{event.description && <p className="text-base text-gray-400 line-clamp-3">{event.description}</p>}

{/* TODO: add correct calendar */}
<div
className="w-fit text-xs/[1] rounded-[20px] px-1.5 py-1 mt-1.5 font-medium"
style={{backgroundColor: 'rgba(74, 108, 247, 0.3)', color: 'rgb(74, 108, 247)'}}>
Work
</div>
{event.eventCalendar && (
<div
className="w-fit text-xs/[1] rounded-[20px] px-1.5 py-1 mt-1.5 font-medium"
style={{background: `rgb(from ${calendarColor} r g b / 0.3)`, color: calendarColor}}>
{event.eventCalendar}
</div>
)}
</div>

{showItemControls && (
<div className="flex gap-2">
<Button onClick={handleOpen} endIcon={<EditIcon size="size-4" />} className="w-fit" />
<div className="flex gap-4">
<Button
onClick={handleOpen}
endIcon={<EditIcon size="size-4" />}
className="w-fit p-0 text-white hover:text-sky-500"
variant="transparent"
/>
<Button
variant="red-bordered"
variant="transparent"
onClick={handleDeleteEvent}
endIcon={<DeleteIcon size="size-4" />}
className="w-fit"
className="w-fit p-0 text-white hover:text-red-400"
/>
</div>
)}

{/* TODO: add d&d */}
<BarsIcon className="size-4 text-gray-400 cursor-pointer" />
</div>

Expand Down
7 changes: 7 additions & 0 deletions src/components/common/EventsList/EventsListItem/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {TEvent} from '@/redux/events/types';

export interface IEventsListItemProps {
event: TEvent;
showItemControls: boolean;
isDisabled?: boolean;
}
10 changes: 8 additions & 2 deletions src/components/common/EventsList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,18 @@ const EventsList = ({
<div
ref={eventsContainerRef}
className={classNames('flex-1 overflow-y-auto scroll-smooth', {'pr-2.5': hasScroll})}>
{/* <div className="flex-1 min-h-screen overflow-y-auto scroll-smooth"> */}
{!events?.length ? (
<p>no events</p>
) : (
<div className="flex flex-col gap-4">
{events?.map(event => <EventsListItem key={event.id} event={event} showItemControls={showItemControls} />)}
{events?.map(event => (
<EventsListItem
key={event.id}
event={event}
showItemControls={showItemControls}
isDisabled={event.isDisabled}
/>
))}
</div>
)}
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/components/common/Modal/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import cn from 'classnames';
import classNames from 'classnames';
import {createPortal} from 'react-dom';

import {TModalProps} from './types';
Expand All @@ -19,7 +19,7 @@ const Modal = ({refItem, className, children, onClose}: TModalProps) => {
{createPortal(
<div
onClick={handleClose}
className={cn(
className={classNames(
'fixed top-0 left-0 w-screen h-screen flex items-center justify-center bg-overlay bg-opacity-80 z-[1000]',
className,
)}>
Expand Down
3 changes: 2 additions & 1 deletion src/components/common/formInputs/DropdownControl.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {memo} from 'react';
import {ReactNode, memo} from 'react';
import {Control, useController} from 'react-hook-form';

import Dropdown from '../inputs/Dropdown';
Expand All @@ -9,6 +9,7 @@ type DropdownControlProps = {
control: Control<any>;
options: string[];
selectedOption?: string;
customOption?: (option: string) => ReactNode;
};

const DropdownControl = ({name, control, selectedOption = '', ...restProps}: DropdownControlProps) => {
Expand Down
25 changes: 17 additions & 8 deletions src/components/common/inputs/Dropdown/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const Dropdown = ({
options,
placeholder = 'Choose an option',
className = '',
customOption,
}: TDropdownProps) => {
const [currentValue, setCurrentValue] = useState(selectedOption);

Expand Down Expand Up @@ -53,10 +54,16 @@ const Dropdown = ({
return (
<div className={cx('w-full relative', className)}>
<Button
className={cx('py-2 w-full h-full text-left border-secondary-background-color z-30', {
'border-sky-500 text-sky-500': isOpen,
})}
text={currentValue ? currentValue : placeholder}
className={cx(
'py-2 w-full h-full text-left border-secondary-background-color z-30',
{
'border-sky-500 text-sky-500': isOpen,
},
{
'flex-row-reverse [&>div]:w-auto': customOption,
},
)}
text={customOption ? '' : currentValue ? currentValue : placeholder}
endIcon={
<div
className={cx('transition-transform', {
Expand All @@ -66,8 +73,9 @@ const Dropdown = ({
</div>
}
onClick={handleToggle}
type="button"
/>
type="button">
{customOption ? customOption(currentValue || '') : null}
</Button>

{isOpen && (
<>
Expand All @@ -92,8 +100,9 @@ const Dropdown = ({
'transition-all flex items-center justify-between gap-1 cursor-pointer text-white select-none relative py-2 px-3 hover:bg-secondary-background-color',
)}
onClick={() => handleChange(option)}>
<span className="font-normal block truncate">{option}</span>
{currentValue === option && <CheckIcon size="size-3" />}
{customOption ? customOption(option) : <span className="font-normal block truncate">{option}</span>}

{currentValue === option && <CheckIcon className="absolute right-2 top-1/2 -translate-y-1/2 size-3" />}
</li>
))}
</ul>
Expand Down
Loading
Loading