sparkletown/sparkle

View on GitHub
src/pages/Account/Venue/VenueMapEdition/Container.tsx

Summary

Maintainability
F
3 days
Test Coverage
import React, {
  CSSProperties,
  useCallback,
  useEffect,
  useMemo,
  useState,
} from "react";
import { useDrop } from "react-dnd";
import ReactResizeDetector from "react-resize-detector";
import update from "immutability-helper";

import { DEFAULT_MAP_ICON_URL } from "settings";

import { Dimensions } from "types/utility";

import { CustomDragLayer } from "./CustomDragLayer";
import { DraggableSubvenue } from "./DraggableSubvenue";
import { DragItem } from "./interfaces";
import { ItemTypes } from "./ItemTypes";
import { snapToGrid as doSnapToGrid } from "./snapToGrid";

const styles: React.CSSProperties = {
  width: "100%",
  position: "relative",
};
export interface SubVenueIconMap {
  [key: string]: {
    title?: string;
    top: number;
    left: number;
    url?: string;
    width: number;
    height: number;
    roomIndex?: number;
    isEnabled?: boolean;
  };
}

interface CoordinatesBoundary {
  width: number;
  height: number;
}

interface PropsType {
  snapToGrid?: boolean;
  iconsMap: SubVenueIconMap;
  backgroundImage: string;
  iconImageStyle?: CSSProperties; // This is not being used ATM
  draggableIconImageStyle?: CSSProperties; // This is not being used ATM
  onChange?: (val: SubVenueIconMap) => void;
  otherIcons: SubVenueIconMap;
  onOtherIconClick?: (key: string) => void;
  coordinatesBoundary: CoordinatesBoundary;
  interactive: boolean;
  resizable: boolean;
  onResize?: (rawVal: Dimensions, percentageVal: Dimensions) => void;
  otherIconsStyle?: CSSProperties;
  rounded?: boolean;
  backgroundImageStyle?: CSSProperties;
  containerStyle?: CSSProperties;
  lockAspectRatio?: boolean;
  isSaving?: boolean;
}

export const Container: React.FC<PropsType> = (props) => {
  const {
    snapToGrid,
    iconsMap,
    backgroundImage,
    iconImageStyle,
    onChange,
    otherIcons,
    onOtherIconClick,
    coordinatesBoundary,
    interactive,
    resizable,
    rounded,
    otherIconsStyle,
    backgroundImageStyle,
    containerStyle,
    lockAspectRatio,
    isSaving,
  } = props;
  const [boxes, setBoxes] = useState<SubVenueIconMap>(iconsMap);
  const [imageDims, setImageDims] = useState<Dimensions>();
  const [dragBoxId, setDragBoxId] = useState<number>(0);

  const setDragItemId = useCallback((id: number) => {
    setDragBoxId(id);
  }, []);

  // trigger the parent callback on boxes change (as a result of movement)
  useEffect(() => {
    if (!imageDims) return;

    const convertDisplayedCoordToIntrinsic = (
      val: number,
      dimension: keyof typeof imageDims,
      coordinateBoundary: number
    ) => (coordinateBoundary * val) / imageDims[dimension];

    //need to return the unscaled values
    const unscaledBoxes = Object.keys(boxes).reduce(
      (acc, val) => ({
        ...acc,
        [val]: {
          ...boxes[val],
          // resizable expects a percentage (for rooms), whereas non-resizable expects pixels
          width: resizable
            ? (coordinatesBoundary.width * boxes[val].width) / imageDims.width
            : boxes[val].width,
          height: resizable
            ? (coordinatesBoundary.height * boxes[val].height) /
              imageDims.height
            : boxes[val].height,
          top: convertDisplayedCoordToIntrinsic(
            boxes[val].top,
            "height",
            coordinatesBoundary.height
          ),
          left: convertDisplayedCoordToIntrinsic(
            boxes[val].left,
            "width",
            coordinatesBoundary.width
          ),
        },
      }),
      {}
    );

    onChange && onChange(unscaledBoxes);
  }, [
    boxes,
    onChange,
    imageDims,
    resizable,
    coordinatesBoundary.width,
    coordinatesBoundary.height,
    iconsMap,
  ]);

  useMemo(() => {
    if (!imageDims) return;

    const copy = Object.keys(iconsMap).reduce(
      (acc, val) => ({
        ...acc,
        [val]: {
          ...iconsMap[val],
          width: resizable
            ? (imageDims.width * iconsMap[val].width) /
              coordinatesBoundary.width
            : iconsMap[val].width,
          height: resizable
            ? (imageDims.height * iconsMap[val].height) /
              coordinatesBoundary.height
            : iconsMap[val].height,
          top:
            (imageDims.height * iconsMap[val].top) / coordinatesBoundary.height,
          left:
            (imageDims.width * iconsMap[val].left) / coordinatesBoundary.width,
        },
      }),
      {}
    );

    setBoxes(copy);
  }, [
    coordinatesBoundary.height,
    coordinatesBoundary.width,
    iconsMap,
    imageDims,
    resizable,
  ]);

  const moveBox = useCallback(
    (id: string, left: number, top: number) => {
      setBoxes(
        update(boxes, {
          [id]: {
            $merge: { left, top },
          },
        })
      );
    },
    [boxes]
  );
  const resizeBox = useCallback(
    (id: string) => (dimensions: Dimensions) => {
      const { width, height } = dimensions;
      setBoxes(
        update(boxes, {
          [id]: {
            $merge: { width, height },
          },
        })
      );
    },
    [boxes]
  );

  const [, drop] = useDrop({
    accept: ItemTypes.SUBVENUE_ICON,
    drop: (item: DragItem, monitor) => {
      if (!interactive) return;
      const delta = monitor.getDifferenceFromInitialOffset() as {
        x: number;
        y: number;
      };

      let left = Math.round(item.left + delta.x);
      let top = Math.round(item.top + delta.y);
      if (snapToGrid) {
        [left, top] = doSnapToGrid(left, top);
      }

      moveBox(item.id, left, top);
    },
  });

  return (
    <>
      <div ref={drop} style={{ ...styles, ...containerStyle }}>
        <ReactResizeDetector
          handleWidth
          handleHeight
          onResize={(width, height) => setImageDims({ width, height })}
        />
        <img
          alt="draggable background "
          style={{
            width: "100%",
            ...backgroundImageStyle,
          }}
          src={backgroundImage}
        />
        <div
          style={{ position: "absolute", top: 0, left: 0, bottom: 0, right: 0 }}
        >
          {useMemo(
            () =>
              Object.keys(otherIcons).map((key, index) => (
                <img
                  key={`${otherIcons[key].top}-${otherIcons[key].left}-${otherIcons[key].url}-${index}`}
                  src={otherIcons[key].url || DEFAULT_MAP_ICON_URL}
                  style={{
                    position: "absolute",
                    top: `${
                      (100 * otherIcons[key].top) / coordinatesBoundary.height
                    }%`,
                    left: `${
                      (100 * otherIcons[key].left) / coordinatesBoundary.width
                    }%`,
                    width: resizable
                      ? `${otherIcons[key].width}%`
                      : otherIcons[key].width, //resizable dimensions are in percentages
                    height: resizable
                      ? `${otherIcons[key].height}%`
                      : otherIcons[key].width,
                    borderRadius: rounded ? "50%" : "none",
                    ...otherIconsStyle,
                  }}
                  alt={`${otherIcons[key].url} map icon`}
                  onClick={() => onOtherIconClick && onOtherIconClick(key)}
                />
              )),
            [
              otherIcons,
              coordinatesBoundary.height,
              coordinatesBoundary.width,
              resizable,
              rounded,
              otherIconsStyle,
              onOtherIconClick,
            ]
          )}
        </div>
        {Object.keys(boxes).map((key) => (
          <DraggableSubvenue
            isResizable={resizable}
            key={key}
            id={key}
            imageStyle={iconImageStyle}
            rounded={!!rounded}
            {...boxes[key]}
            onChangeSize={resizeBox(key)}
            lockAspectRatio={lockAspectRatio}
            onDragStart={setDragItemId}
            isSaving={isSaving}
          />
        ))}
      </div>
      {imageDims && interactive && (
        <CustomDragLayer
          snapToGrid={!!snapToGrid}
          rounded={!!rounded}
          iconSize={boxes[Object.keys(boxes)[dragBoxId]]}
        />
      )}
    </>
  );
};