tutorbookapp/tutorbook

View on GitHub
components/calendar/meetings/item.tsx

Summary

Maintainability
C
1 day
Test Coverage
import { useCallback, useMemo } from 'react';
import cn from 'classnames';

import { Meeting } from 'lib/model/meeting';
import { Position } from 'lib/model/position';
import { TCallback } from 'lib/model/callback';
import { useClickContext } from 'lib/hooks/click-outside';

import { DialogPage, useCalendarState } from '../state';
import { MouseEventHackData, MouseEventHackTarget } from '../hack-types';
import { getHeight, getPosition } from '../utils';

import MeetingContent from './content';
import styles from './item.module.scss';

export interface MeetingItemProps {
  now: Date;
  meeting: Meeting;
  leftPercent: number;
  widthPercent: number;
  setEventData: TCallback<MouseEventHackData>;
  setEventTarget: TCallback<MouseEventHackTarget>;
}

export default function MeetingItem({
  now,
  meeting,
  leftPercent,
  widthPercent,
  setEventData,
  setEventTarget,
}: MeetingItemProps): JSX.Element {
  const top = useMemo(() => getPosition(meeting.time.from).y, [
    meeting.time.from,
  ]);
  const left = useMemo(() => {
    if (leftPercent === 0) return '-1px';
    return `calc(${leftPercent * 100}% + 1px)`;
  }, [leftPercent]);
  const width = useMemo(() => {
    if (leftPercent === 0) return `calc(${widthPercent * 100}% + 1px)`;
    return `calc(${widthPercent * 100}% - 1px)`;
  }, [leftPercent, widthPercent]);
  const height = useMemo(() => getHeight(meeting.time), [meeting.time]);

  const { updateEl, removeEl } = useClickContext();
  const ref = useCallback(
    (node: HTMLElement | null) => {
      if (!node) return removeEl(`meeting-item-${meeting.id}`);
      return updateEl(`meeting-item-${meeting.id}`, node);
    },
    [updateEl, removeEl, meeting.id]
  );

  const {
    editing,
    setEditing,
    setEditingLeftPercent,
    setEditingWidthPercent,
    rnd,
    setRnd,
    dialog,
    setDialog,
    setDialogPage,
  } = useCalendarState();

  return (
    <div
      style={{ top, left, width, height }}
      className={cn(styles.meeting, {
        [styles.elevated]: !rnd && dialog && editing.id === meeting.id,
        [styles.editing]: rnd && editing.id === meeting.id,
        [styles.past]: meeting.time.to <= now,
      })}
      onClick={(evt) => evt.stopPropagation()}
      onMouseDown={(evt) => {
        evt.stopPropagation();

        // Decide what to do after mousedown:
        // - If mousemove more than 10px, then edit with RND (this is a drag).
        // - If mouseup, then view (this is a click).
        let mouseMovement = 0;
        let lastPosition: Position;
        const edit = (e: MouseEvent) => {
          e.stopPropagation();
          if (lastPosition)
            mouseMovement += Math.sqrt(
              Math.pow(lastPosition.x - e.clientX, 2) +
                Math.pow(lastPosition.y - e.clientY, 2)
            );
          lastPosition = { x: e.clientX, y: e.clientY };
          if (mouseMovement > 10) {
            removeListeners();
            setEditing(meeting);
            setEditingLeftPercent(leftPercent);
            setEditingWidthPercent(widthPercent);
            setEventTarget('middle');
            setEventData({
              screenX: e.screenX,
              screenY: e.screenY,
              clientX: e.clientX,
              clientY: e.clientY,
              button: e.button,
              buttons: e.buttons,
            });
            setRnd(true);
          }
        };
        const view = (e: MouseEvent) => {
          e.stopPropagation();
          removeListeners();
          setRnd(false);
          setEditing(meeting);
          setEditingLeftPercent(leftPercent);
          setEditingWidthPercent(widthPercent);
          setDialogPage(DialogPage.Display);
          setDialog(true);
        };
        const removeListeners = () => {
          document.removeEventListener('mousemove', edit, { capture: true });
          document.removeEventListener('mouseup', view, { capture: true });
        };

        document.addEventListener('mousemove', edit, { capture: true });
        document.addEventListener('mouseup', view, { capture: true });
      }}
    >
      <MeetingContent ref={ref} meeting={meeting} height={height} />
      <span>
        <div
          className={styles.bottom}
          onMouseDown={(evt) => {
            evt.stopPropagation();
            setEditing(meeting);
            setEditingLeftPercent(leftPercent);
            setEditingWidthPercent(widthPercent);
            setEventTarget('bottom');
            setEventData({
              screenX: evt.screenX,
              screenY: evt.screenY,
              clientX: evt.clientX,
              clientY: evt.clientY,
              button: evt.button,
              buttons: evt.buttons,
            });
            setRnd(true);
          }}
        />
        <div
          className={styles.top}
          onMouseDown={(evt) => {
            evt.stopPropagation();
            setEditing(meeting);
            setEditingLeftPercent(leftPercent);
            setEditingWidthPercent(widthPercent);
            setEventTarget('top');
            setEventData({
              screenX: evt.screenX,
              screenY: evt.screenY,
              clientX: evt.clientX,
              clientY: evt.clientY,
              button: evt.button,
              buttons: evt.buttons,
            });
            setRnd(true);
          }}
        />
      </span>
    </div>
  );
}