zapupenec/diary_app

View on GitHub
src/components/note-form/index.tsx

Summary

Maintainability
A
3 hrs
Test Coverage
import {
  ChangeEventHandler,
  DetailedHTMLProps,
  FC,
  FormEventHandler,
  HTMLAttributes,
  MouseEventHandler,
  useEffect,
  useState,
} from 'react';

import styles from './note-form.module.css';
import { useResize } from 'hooks';
import {
  Button,
  Checkbox,
  Icon,
  ImgWithLoader,
  Input,
  SearchImage,
  Selector,
  Textarea,
} from 'components';
import { emojis } from 'constant';
import { clsx } from 'lib';
import { useNoteForm } from 'contexts/note-form';
import { useDiary } from 'contexts/diary';
import { useRouter } from 'contexts/router';
import { useModal } from 'contexts/modal';
import { useSearchImage } from 'contexts/search-image';
import { TImage, TNote } from 'types';

interface INoteForm extends DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement> {
  type: 'add' | 'edit';
}

export const NoteForm: FC<INoteForm> = ({ type = 'add' }) => {
  const { width } = useResize();
  const { addNote, editNote, getNoteById } = useDiary();
  const { formData, isValid, updateFormData, resetFormData, isValidFormData } = useNoteForm();
  const { extra, navigateTo } = useRouter();
  const { showModal } = useModal();
  const { fetchImages } = useSearchImage();
  const [prevImage, setPrevImage] = useState<TNote['image']>(null);

  useEffect(() => {
    if (type === 'edit' && extra) {
      const note = getNoteById(extra.noteId);
      if (note) {
        updateFormData(note);
        setPrevImage(note.image);
      }
    }
    fetchImages();
  }, []);

  const handleChange: ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement> = (e) => {
    if (e.target instanceof HTMLInputElement && e.target.type === 'checkbox') {
      updateFormData({
        image: e.target.checked ? prevImage : null,
      });
      return;
    }

    updateFormData({
      [e.target.id]: e.target.value,
    });
  };

  const handleChangeSelect = (value: string) => {
    updateFormData({
      emoji: value,
    });
  };

  const handleClickImage = (image: TImage | null) => {
    updateFormData({ image });
  };

  const handleSubmit: FormEventHandler<HTMLFormElement> = (e) => {
    e.preventDefault();
    if (isValidFormData()) {
      if (type === 'add') {
        addNote(formData);
      }

      if (type === 'edit' && extra) {
        editNote(extra.noteId, formData);
      }

      navigateTo('note-list');
      resetFormData();
    }
  };

  const handleCancel: MouseEventHandler<HTMLButtonElement> = (e) => {
    e.preventDefault();
    navigateTo('note-list');
    resetFormData();
  };

  const handleClickImagePlaceholder: MouseEventHandler<HTMLDivElement> = (e) => {
    showModal('search-image', { handleClickImage });
  };

  return (
    <div className={styles.container}>
      <div className={clsx(styles['form-wrapper'], type === 'edit' ? styles['type-edit'] : '')}>
        <form className={styles.form} onSubmit={handleSubmit} id="add-form">
          <Input
            autoFocus
            className={styles.title}
            placeholder="Название"
            id="title"
            onChange={handleChange}
            isValid={isValid.title}
            value={formData.title}
          />
          <Selector
            values={emojis}
            displayValues={emojis}
            placeholder={<Icon name="smile-mouth-open" />}
            value={formData.emoji}
            defaultValue=""
            isValid={isValid.emoji}
            onChangeValue={handleChangeSelect}
            aria-label="Выбор эмодзи"
          />
          <Input
            className={styles.date}
            placeholder="Дата"
            id="date"
            type="date"
            onFocus={(e) => (e.target.type = 'date')}
            onBlur={(e) => (e.target.type = e.target.value === '' ? 'text' : 'date')}
            max={new Date().toISOString().split('T')[0]}
            onChange={handleChange}
            value={formData.date}
            isValid={isValid.date}
          />
        </form>
        <Textarea
          className={styles.textarea}
          id="description"
          placeholder="Описание"
          form="add-form"
          onChange={handleChange}
          isValid={isValid.description}
          value={formData.description}
        />
        {type === 'edit' && (
          <Checkbox
            className={styles.checkbox}
            id="description"
            form="add-form"
            checked={prevImage?.url === formData.image?.url}
            onChange={handleChange}
            isValid={isValid.image}
          >
            Оставить прошлое изображение?
          </Checkbox>
        )}
        <div className={styles.controls}>
          <Button className={styles.btn} type="submit" form="add-form">
            <Icon name="pen" />
            {type === 'edit' ? 'Изменить' : 'Добавить'}
            <span> запись</span>
          </Button>
          <Button
            className={styles.btn}
            type="button"
            form="add-form"
            bgColor="var(--button-grey)"
            onClick={handleCancel}
          >
            <Icon name="cross" />
            Отменить<span> {type === 'edit' ? 'изменение' : 'добавление'}</span>
          </Button>
        </div>
        {width <= 1439 && (
          <div
            className={clsx(styles['image-placeholder'], isValid.image ? '' : styles.invalid)}
            tabIndex={0}
            role="button"
            onClick={handleClickImagePlaceholder}
            aria-label="Открыть поиск изображений"
          >
            {formData.image ? (
              <ImgWithLoader src={formData.image.url} alt={formData.image.description} />
            ) : (
              <Icon name="image-icon" />
            )}
          </div>
        )}
      </div>
      {width > 1439 && (
        <div className={clsx(styles.search, isValid.image ? '' : styles.invalid)}>
          <SearchImage onClickImage={handleClickImage} selectedImg={formData.image?.url} />
        </div>
      )}
    </div>
  );
};