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
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"@hookform/error-message": "^2.0.1",
"@hookform/resolvers": "^3.10.0",
"@reduxjs/toolkit": "^2.5.0",
"classnames": "^2.5.1",
"moment": "^2.30.1",
"motion": "^12.7.4",
"react": "^18.3.1",
Expand Down
10 changes: 6 additions & 4 deletions src/components/Button/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {twMerge} from 'tailwind-merge';

import {TButtonProps} from './types';
import {buttonVariants} from './variants';

Expand All @@ -6,15 +8,15 @@ const Button = ({
startIcon = null,
endIcon = null,
text = '',
className,
className = '',
children,
...restProps
}: TButtonProps) => {
return (
<button
className={`${buttonVariants[variant]} w-auto p-3 flex justify-center items-center gap-2 cursor-pointer border rounded-lg transition duration-500 ease-in-out disabled:hover:border-secondaryBackgroundColor disabled:text-secondaryBackgroundColor disabled:hover:text-secondaryBackgroundColor disabled:cursor-auto ${
className ?? ''
}`}
className={twMerge(
`${buttonVariants[variant]} w-auto p-3 flex justify-center items-center gap-2 cursor-pointer border rounded-lg transition duration-500 ease-in-out disabled:hover:border-secondaryBackgroundColor disabled:text-secondaryBackgroundColor disabled:hover:text-secondaryBackgroundColor disabled:cursor-auto ${className}`,
)}
{...restProps}>
<div className={`flex items-center justify-center w-full ${(startIcon || endIcon) && text ? 'gap-2' : ''}`}>
{startIcon ? <span className="font-normal block truncate">{startIcon}</span> : null}
Expand Down
73 changes: 73 additions & 0 deletions src/components/Tooltip/index.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>(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 (
<div className={classNames('relative', triggerElementClassName)}>
{triggerWithHandlers}

{isOpened && (
<div
ref={tooltipRef}
className={cn(
`max-w-[100px] max-h-[200px] overflow-auto absolute bottom-[calc(100%+6px)] transition-all duration-200 ease-in-out px-3 py-1 rounded-md border bg-black text-sm z-10 opacity-100`,
{
'right-[1px]': !isFitsContainer,
'left-0': isFitsContainer,
},
tooltipClassnames,
)}>
{children}
</div>
)}
</div>
);
};

export default memo(Tooltip);
7 changes: 7 additions & 0 deletions src/components/Tooltip/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {PropsWithChildren, ReactElement} from 'react';

export type TooltipProps = PropsWithChildren<{
triggerElement: ReactElement;
triggerElementClassName?: string;
tooltipClassnames?: string;
}>;
53 changes: 53 additions & 0 deletions src/components/formInputs/TextareaControl.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {ErrorMessage} from '@hookform/error-message';
import {HTMLProps, memo} from 'react';
import {Control, get, useController, useFormContext} from 'react-hook-form';

import Textarea from '../inputs/Textarea';

type TextareaControlProps = {
name: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
control: Control<any>;
defaultValue?: string;
};

const TextareaControl = ({
name,
control,
defaultValue = '',
...restProps
}: HTMLProps<HTMLTextAreaElement> & TextareaControlProps) => {
const {
formState: {errors},
} = useFormContext();

const {field} = useController({
name,
control,
defaultValue,
});

const errorProps = {
error: Boolean(get(errors, name)),
helperText: <ErrorMessage errors={errors} name={name} render={({message}) => message} />,
};

return (
<>
<Textarea
field={field}
onChange={field.onChange}
value={field.value}
aria-invalid={errorProps.error}
{...restProps}
/>
{errorProps.error && (
<p role="alert" className="text-rose-500 text-xs pl-1">
{errorProps.helperText}
</p>
)}
</>
);
};

export default memo(TextareaControl);
Original file line number Diff line number Diff line change
@@ -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(),
});
98 changes: 98 additions & 0 deletions src/components/forms/BoardEventForm/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
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 TextareaControl from '@/components/formInputs/TextareaControl';
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 (
<div className="modal relative bg-mainBackgroundColor border border-sky-500 rounded-lg md:min-w-96 sm:min-w-56">
<Button
startIcon={<CloseIcon size="size-5" />}
className="absolute right-2 top-2 p-1 text-sm cursor-pointer hover:text-sky-500"
onClick={handleModalClose}
/>

<h3 className="text-lg font-bold p-3 text-center">{formTitle}</h3>

<FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex flex-col items-center justify-between gap-[20px] w-auto px-[30px] py-[20px]">
<div className="flex flex-col gap-3 w-full">
<InputControl
autoFocus
control={control}
name="eventName"
placeholder="Enter required name"
/>
<TextareaControl
control={control}
name="eventDescription"
placeholder="Enter optional description"
/>
</div>

<Button
className={`${isDirty && !errors.eventName && 'border-sky-500 text-sky-500'} text-sm p-2 `}
disabled={!isDirty || !!errors.eventName}
text={actionType === 'edit' ? 'Edit' : 'Add'}
endIcon={actionType === 'edit' ? <EditIcon size="size-5" /> : <CheckIcon size="size-5" />}
type="submit"
/>
</div>
</form>
</FormProvider>
</div>
);
};

export default BoardEventForm;
Original file line number Diff line number Diff line change
@@ -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<TFormFields>;
date: string;
handleModalClose: () => void;
};
57 changes: 0 additions & 57 deletions src/components/forms/BoardItemForm/index.tsx

This file was deleted.

Loading