Untitled

 avatar
unknown
plain_text
a month ago
24 kB
4
Indexable
import React, { useState, useRef, useEffect } from 'react';
import styled, { css } from 'styled-components/macro';
import { GridDropZone, GridItem } from 'react-grid-dnd';
import { useDropZoneContext } from 'contexts/DropZoneContext';

import { PostItGroupAndCards, Step } from 'types';
import {
  BodySmall,
  ButtonRound,
  Icon,
  Subtitle2,
  Tooltip,
} from 'components/shared';

import { colors } from 'constants/colors';
import { Transition, TransitionGroup } from 'react-transition-group';
import { TransitionStatus } from 'react-transition-group/Transition';
import { device } from 'utils/breakpoints';
import { AddNoteButton } from './AddNoteButton';
import { PostItCard } from 'components/PostItCard';
import useDesktop from 'hooks/useDesktop';
import useMobile from 'hooks/useMobile';
import { uid } from 'uid';
import { useDropzone } from 'react-dropzone';
import {
  PostItCard as Card,
  useRequestFileUploadMutation,
  PostItCardType,
  CompetitiveLandscapeRatingFragment,
  PostItGroupFragment,
  Step as PageStep,
  SubStep,
  Role,
} from 'data/graphql/generated';
import { uploadFile } from 'utils/uploadFile';
import { DropZoneWrapper } from './DropZoneWrapper';
import { onDropHandler } from './onDropHandler';
import { usePostItCardsPayload } from 'hooks/usePostItCards';
import { SliderRating } from './SliderRating';
import { useAuthContext } from 'contexts/AuthContext';
import { postItCardWidths } from 'constants/index';
import { LoadingComponent } from 'components/Loading';

const RatingLoading = styled(LoadingComponent)`
  margin-top: 0;
  margin-bottom: 0;
  height: 40px;
  width: 338.641px;
`;

const RatingWrapper = styled.div`
  @media (max-width: 652px) {
    width: 100%;
  }
`;

export const GroupWrapper = styled.div<{
  largePadding: boolean;
  step: Step;
}>`
  border: 1px solid #e9e9ea;
  background: #ffffff;
  @media ${device.tabletMax} {
    border: none;
    border-radius: 0px;
  }
  margin-bottom: 15px;
  border-radius: 5px;
  position: relative;
  transition: padding 0.3s;
  transition-delay: 0.3s;

  .GridItem .postitcard {
    transition: transform 0.1s;
    cursor: grab;
  }
  .GridItem .postitcard.show-typing {
    cursor: not-allowed;
  }

  .GridItem.dragging .postitcard {
    border-radius: 5px;
    transform: rotate(5deg);
    filter: drop-shadow(0px 1px 5px rgba(0, 0, 0, 0.05)),
      drop-shadow(0px 10px 20px rgba(0, 0, 0, 0.1));
    cursor: grabbing;
  }
`;

const GroupHeader = styled.div<{ centeredTitle: boolean }>`
  margin-bottom: 15px;
  min-height: 41px;
  position: relative;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 15px 15px 0px 15px;
  gap: 15px;
  flex-wrap: ${({ centeredTitle }) => (centeredTitle ? 'nowrap' : 'wrap')};

  @media ${device.tabletMin} {
    justify-content: flex-start;
  }
`;

interface mountStateProps {
  mountState: TransitionStatus;
}

export const GroupDelete = styled.div`
  z-index: 0;
  display: flex;
  justify-content: flex-end;
  padding: 0 15px 15px 15px;

  @media ${device.desktopMin} {
    padding: 0;
  }
`;

const PlusButtonWrapper = styled.div`
  width: 40px;
  height: 40px;
  align-self: flex-start;

  margin-left: 15px;
`;

const GroupTitle = styled.textarea<{ preset: boolean }>`
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  -ms-overflow-style: none;
  scrollbar-width: none;
  ::-webkit-scrollbar {
    display: none;
  }
  text-align: center;
  overflow: visible;
  @media ${device.tabletMax} {
    text-align: left;
  }
  resize: none;
  border: none;
  outline: none;
  font-size: 18px;
  border: 0.5px solid ${colors.white};
  color: ${colors.greyDark};
  width: 100%;
  display: block;
  pointer-events: ${({ preset }) => (preset ? 'none' : 'all')};
  &:focus {
    border-color: ${colors.blue};
  }
  &:disabled {
    background: ${colors.white};
    opacity: 1;

    // Safari
    -webkit-opacity: 1;
    -webkit-text-fill-color: ${colors.greyDark};
  }
`;

const NoteAdd = styled.div`
  height: 40px;
  position: relative;
  z-index: 1;
`;

const GroupContent = styled.div<{ empty: boolean }>`
  display: block;
  background: transparent;
  padding: 0px 15px 15px 15px;
  position: relative;
`;

const GroupCards = styled.div`
  position: relative;
  display: block;
  transition: 0.3s;

  @media ${device.tabletMax} {
    margin: 0 auto;
  }
`;

const EmptyWrapper = styled.div<mountStateProps>`
  padding: 37px 0px 38px 0px;
  text-align: center;
  position: absolute;
  top: 0px;
  left: 0px;
  right: 0px;
  pointer-events: none;
  z-index: 0;
  border-radius: 5px;

  ${({ mountState }) => {
    if (mountState === 'entered') {
      return css`
        opacity: 1;
        background-color: ${colors.yellow20};
      `;
    }
    if (
      mountState === 'exiting' ||
      mountState === 'exited' ||
      mountState === 'entering'
    )
      return css`
        opacity: 0;
        background-color: transparent;
      `;
  }};
  transition: 0.3s;
`;

const EmptyState: React.FC<mountStateProps & { text: string }> = ({
  mountState,
  text,
}) => {
  return (
    <EmptyWrapper mountState={mountState}>
      <Icon
        name="GenericEmptyState"
        size={115}
        height={115}
        color="initial"
        style={{ margin: '0 auto' }}
      />
      <Subtitle2 color={colors.greyDark}>{text}</Subtitle2>
    </EmptyWrapper>
  );
};

const GroupTitleSub = styled.div`
  overflow: hidden;
  margin: 0;
  text-align: left;
`;

const GroupTitleText = styled(Subtitle2)`
  color: ${colors.black70};
`;

const GroupSubTitleText = styled(BodySmall)`
  color: ${colors.black70};
`;

const HiddenText = styled.p`
  width: 100%;
  margin: 0;
  visibility: hidden;
  font-size: 18px;
  border: 0.5px solid ${colors.white};
  color: ${colors.greyDark};
`;

const GroupTitleWrapper = styled.div<{ centeredTitle: boolean, disableUpdate: boolean }>`
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  width: calc(80% - 120px);
  margin: 0 auto;
  max-width: 100%;
  mouse-events: ${({ disableUpdate }) => (disableUpdate ? 'none' : 'all')};
  pointer-events: ${({ disableUpdate }) => (disableUpdate ? 'none' : 'all')};

  textarea {
    text-align: center;
  }

  overflow: hidden;

  ${({ centeredTitle }) => {
    if (!centeredTitle) {
      return css`
        position: relative;
        left: 0;
        transform: translateX(0%);

        width: 100%;
        flex: 1;

        textarea {
          text-align: left;
        }

        overflow: hidden;
        margin-right: auto;
      `;
    }
  }}

  @media ${device.tabletMax} {
    width: 100%;
    position: static;
    left: 0;
    transform: translateX(0);
  }
  @media ${device.mobile} {
    min-width: auto;
    margin-left: 0px;
  }
`;
interface GroupTitleProps {
  preset: boolean;
  value: string;
  onChange(e: React.ChangeEvent<HTMLTextAreaElement>): void;
  onBlur(event: React.FocusEvent<HTMLTextAreaElement>): void;
  rows: number;
  newGroup: [
    number | undefined,
    React.Dispatch<React.SetStateAction<number | undefined>>
  ];
  disableUpdate: boolean;
}

const GroupTitleInput: React.FC<GroupTitleProps> = ({
  value,
  onChange,
  onBlur,
  preset,
  newGroup,
  disableUpdate,
}) => {
  const inputRef = useRef<HTMLTextAreaElement>(null);
  const [newGroupId, setNewGroupId] = newGroup;

  return (
    <div>
      <GroupTitle
        preset={preset}
        ref={inputRef}
        value={value}
        onChange={onChange}
        disabled={preset || disableUpdate}
        autoFocus={typeof newGroupId === 'number'}
        onBlur={onBlur}
        onFocus={(e) => {
          setNewGroupId(undefined);
          e.target.select();
        }}
        onKeyDown={(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
          // capture Enter and blur input
          if (e.key === 'Enter') {
            e.preventDefault();
            inputRef?.current?.blur();
          }
        }}
      />
      {/* HiddenText is here to make sure the wrapper div grows to the right size when the window size
       changes as the GroupTitle textarea doesn't seem to change the height of the wrapper div */}
      <HiddenText>{value}</HiddenText>
    </div>
  );
};

const GroupTitleAndSub: React.FC<{ title: string; subtitle: string }> = ({
  title,
  subtitle,
}) => {
  return (
    <GroupTitleSub>
      <GroupTitleText data-cy="post-it-group-title">{title}</GroupTitleText>
      <GroupSubTitleText data-cy="post-it-group-sub-title">
        {subtitle}
      </GroupSubTitleText>
    </GroupTitleSub>
  );
};

export interface SliderUpsertFnInput {
  postItGroup: number;
  score: number;
  region?: string;
  user?: number;
}
export type SliderUpsertFn = (input: SliderUpsertFnInput) => Promise<void>;

interface Props {
  group: PostItGroupAndCards;
  allGroupsCount: number;
  step: Step;
  subStep?: SubStep;
  columns: number;
  userId: number;
  addCard: usePostItCardsPayload['createCard'];
  removeCard: usePostItCardsPayload['removeCard'];
  updateCard: usePostItCardsPayload['updateCard'];
  updateGroup(group: PostItGroupFragment): void;
  removeGroup(groupId: number): void;
  dragDisabled: boolean;
  setDragDisabled(dragDisabled: boolean): any;
  userRole: Role | null;
  slider?: {
    enable?: boolean;
    isLoading?: boolean;
    rating?: CompetitiveLandscapeRatingFragment | null;
    upsert?: SliderUpsertFn;
  };
  newGroup: [
    number | undefined,
    React.Dispatch<React.SetStateAction<number | undefined>>
  ];
  copyEnabled?: boolean;
  copyAction?: (title: string) => void;
  deleteGroupHide: boolean;
  updateGroupDisabled: boolean;
}

export const PostItGroupView: React.FC<Props> = ({
  group,
  allGroupsCount,
  step,
  subStep,
  userId,
  columns,
  addCard,
  removeCard,
  updateCard,
  updateGroup,
  removeGroup,
  dragDisabled,
  setDragDisabled,
  userRole,
  newGroup,
  deleteGroupHide,
  updateGroupDisabled,
  slider = {
    enable: false,
    isLoading: false,
    rating: null,
    upsert: undefined,
  },
  copyEnabled = false,
  copyAction = () => { },
}) => {
  const [{ user }] = useAuthContext();
  const { id, cards, preset, subtitle } = group;
  const [title, setTitle] = useState<string>(group.title);
  const animId = useRef<any>();
  const mouseUp = useRef<boolean>(true);
  const [uploading, setUploading] = useState(-1);
  const [errMsg, setErrMsg] = useState({
    id: -1,
    message: '',
    cardHasImage: false,
  });

  const [cardImageToEdit, setCardImageToEdit] = useState<Omit<
    Card,
    'collaboration'
  > | null>(null);
  const [isDragOver, setIsDragOver] = useState(false);
  const [loadingPostId, setIsLoadingPostId] = useState<number>(0);
  const [sliderRatingValue, setSliderRatingValue] = useState(3);

  const groupContainer = useRef<HTMLDivElement>(null!);

  const [requestFileUpload] = useRequestFileUploadMutation();

  const { isDragActive, draggedFiles } = useDropZoneContext();

  const enableImages = [
    PageStep.Strategicquestion,
    PageStep.Positioning,
  ].includes(step);

  const inputRef = useRef<HTMLInputElement>(null!);

  const { getRootProps, getInputProps, open } = useDropzone({
    disabled: !enableImages,
    noClick: true,
    noKeyboard: true,
    accept: 'image/jpeg, image/png',
    maxFiles: 1,
    maxSize: 20971520,

    onDragEnter: () => {
      setIsDragOver(true);
    },
    onDragLeave: () => {
      setIsDragOver(false);
    },
    onDrop: async (acceptedFiles, fileRejections) => {
      await onDropHandler({
        setIsDragOver,
        setErrMsg,
        errMsg,
        editCardImage: cardImageToEdit,
        group,
        addCard: () =>
          addCard({
            postItGroup: id,
            pos: cards[0] ? cards[0].pos + 1000 : 1000,
            title: '',
            type: PostItCardType.Image,
          }),
        fileRejections,
        setUploading,
        acceptedFiles,
        requestFileUpload,
        uploadFile,
        setCardImageToEdit,
        updateCard,
        removeCard,
      });

      const hiddenInput = inputRef.current;
      if (!hiddenInput) return;
      hiddenInput.value = '';
    },
  });

  const hasCleanedUpBlankImages = useRef(false);

  //Delete empty image cards on mount
  useEffect(() => {
    async function cleanUpBlankImages() {
      for (const card of cards) {
        if (card?.type === 'image' && !card?.image) {
          try {
            await removeCard(card.id);
          } catch (error) {
            console.error(error);
          }
        }
      }
    }

    if (cards && !hasCleanedUpBlankImages.current) {
      cleanUpBlankImages();
      hasCleanedUpBlankImages.current = true;
    }
  }, [cards, removeCard]);

  useEffect(() => {
    setTitle(group.title);
  }, [group.title]);

  useEffect(() => {
    if (!slider.isLoading) {
      setIsLoadingPostId(0);
    }
  }, [slider.isLoading, setIsLoadingPostId]);

  useEffect(() => {
    setSliderRatingValue(slider.rating?.score || 3);
  }, [slider.rating?.score]);

  const tooltipUID = uid();

  const mouseMoveHandler = (e: MouseEvent) => {
    if (animId.current) {
      window.cancelAnimationFrame(animId.current);
    }
    function scrollPage() {
      const dx = e.clientX;
      const dy = e.clientY;
      const windowWidth = window.innerWidth;
      const windowHeight = window.innerHeight;
      const scrollRight = windowWidth - Math.abs(dx) < 50;
      const scrollLeft = windowWidth - Math.abs(dx) > windowWidth - 50;
      const scrollUp = windowHeight - Math.abs(dy) > windowHeight - 50;
      const scrollDown = windowHeight - Math.abs(dy) < 50;
      switch (true) {
        case scrollRight:
          window.scrollBy(10, 0);
          break;
        case scrollLeft:
          window.scrollBy(-10, -10);
          break;
        case scrollUp:
          window.scrollBy(0, -10);
          break;
        case scrollDown:
          window.scrollBy(0, 10);
          break;
      }
      if (!mouseUp.current) {
        animId.current = window.requestAnimationFrame(scrollPage);
      }
    }
    animId.current = window.requestAnimationFrame(scrollPage);
  };

  const scrollPageOnDragHandler = function (e: any) {
    const app = document.querySelector('.app') as HTMLElement;
    if (app) {
      mouseUp.current = false;
      document.addEventListener('mousemove', mouseMoveHandler);

      document.addEventListener(
        'mouseup',
        () => {
          mouseUp.current = true;
          window.cancelAnimationFrame(animId.current);
          document.removeEventListener('mousemove', mouseMoveHandler);
        },
        { once: true }
      );
    }
  };
  const isDesktop = useDesktop();
  const isMobile = useMobile();

  const tooltipMessage =
    (subStep &&
      [SubStep.MedicalObjectives, SubStep.TheWho].includes(subStep)) ||
      [
        PageStep.DistinctiveCapabilities,
        PageStep.CriticalMetrics,
        PageStep.MedicalStrategy,
      ].includes(step)
      ? allGroupsCount === 1
        ? 'Cannot delete the last group'
        : cards.length !== 0
          ? 'Group must be empty to delete'
          : ''
      : preset
        ? 'Cannot delete a preset group'
        : cards.length !== 0
          ? 'Group must be empty to delete'
          : 'Delete group';

  const preventDeleteLastGroup =
    ((subStep &&
      [SubStep.MedicalObjectives, SubStep.TheWho].includes(subStep)) ||
      [
        PageStep.DistinctiveCapabilities,
        PageStep.CriticalMetrics,
        PageStep.MedicalStrategy,
      ].includes(step)) &&
    allGroupsCount === 1;

  const preventDelete = preventDeleteLastGroup || preset || cards.length !== 0;

  const groupIsEmpty = cards.length === 0;
  const rowHeight = groupIsEmpty
    ? 220
    : [
      PageStep.Momentsthatmatter,
      PageStep.Competitivelandscape,
      PageStep.CriticalMetrics,
      PageStep.MedicalStrategy,
      PageStep.Keyinsights,
    ].includes(step as PageStep) ||
      (PageStep.Positioning === step &&
        subStep === SubStep.CompetitorPositioning)
      ? 250
      : 230;

  const draggingImageFiles =
    isDragActive &&
    //dragging accepted files
    draggedFiles.some((file) =>
      ['jpeg', 'png'].some((type) => {
        return file.type.includes(type);
      })
    );

  const postItWidth =
    (isMobile ? postItCardWidths.min : postItCardWidths.max) +
    postItCardWidths.gutter;

  function handleDelete() {
    if (preventDelete) return;

    removeGroup(id);
  }

  return (
    <GroupWrapper
      ref={groupContainer}
      {...getRootProps()}
      step={step}
      largePadding={cards.length === 0}
      className="postit-group cypress-postit-group"
    >
      <input ref={inputRef} {...getInputProps()} />
      <GroupHeader centeredTitle={!slider.enable}>
        {!isMobile ? (
          <NoteAdd>
            <AddNoteButton
              group={group}
              step={step}
              subStep={subStep}
              addCard={(e) => {
                if (e.type === 'image') {
                  open();
                } else addCard(e);
              }}
              cardArgs={{
                postItGroup: id,
                pos: cards[0] ? cards[0].pos + 1000 : 1000,
                title: '',
              }}
            />
          </NoteAdd>
        ) : null}
        {subtitle ? (
          <GroupTitleAndSub title={title} subtitle={subtitle} />
        ) : (
          <GroupTitleWrapper centeredTitle={false} disableUpdate={updateGroupDisabled}>
            <GroupTitleInput
              disableUpdate={updateGroupDisabled}
              rows={1}
              preset={preset}
              value={title}
              onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
                if (preset) {
                  return;
                }
                setTitle(e.target.value);
              }}
              onBlur={(e) => {
                if (preset) {
                  return;
                }
                if (!e.target.value) {
                  setTitle('Untitled Theme');
                }
                updateGroup({
                  ...group,
                  title: title.length === 0 ? 'Untitled Theme' : title,
                });
              }}
              newGroup={newGroup}
            />
          </GroupTitleWrapper>
        )}

        {!isMobile ? (
          isDesktop && !slider.enable && !deleteGroupHide ? (
            <GroupDelete>
              <ButtonRound
                level="secondary"
                iconName="Trash"
                size="small"
                disabled={preventDelete}
                tooltip={tooltipMessage}
                onClick={handleDelete}
              />
            </GroupDelete>
          ) : null
        ) : (
          <PlusButtonWrapper>
            <AddNoteButton
              mobile
              step={step}
              subStep={subStep}
              addCard={addCard}
              cardArgs={{
                postItGroup: id,
                pos: cards[0] ? cards[0].pos + 1000 : 1000,
                title: '',
              }}
              group={group}
            />
          </PlusButtonWrapper>
        )}

        {!!slider.enable && user && (
          <RatingWrapper>
            <RatingLoading isLoading={!!slider.isLoading && loadingPostId === id}>
              <SliderRating
                user={user}
                value={sliderRatingValue}
                onChange={(val) => {
                  setSliderRatingValue(+val);
                }}
                onMouseUp={(val) => {
                  const region =
                    user.role === 'CONTRIBUTOR'
                      ? user.country || undefined
                      : undefined;

                  setIsLoadingPostId(id)
                  slider.upsert?.({
                    score: +val,
                    postItGroup: id,
                    region,
                    user: user.id,
                  });
                }}
                min={1}
                max={5}
                hideCover={!!slider.rating}
                onCoverClick={() => {
                  const region =
                    user.role === 'CONTRIBUTOR'
                      ? user.country || undefined
                      : undefined;
                  if (!slider.rating)
                    slider.upsert?.({
                      score: 3,
                      postItGroup: id,
                      region,
                      user: userId,
                    });
                }}
              />
            </RatingLoading>
          </RatingWrapper>
        )}
      </GroupHeader>

      <GroupContent empty={groupIsEmpty} className="group-content">
        <DropZoneWrapper
          isDragActive={enableImages && draggingImageFiles}
          isDragOver={isDragOver}
        />

        {!!columns && (
          <GroupCards
            style={{
              maxWidth: groupIsEmpty
                ? 'unset'
                : columns * postItWidth -
                //The space on the right for the last post it card needs to be subtracted from the overall width
                postItCardWidths.gutter,
            }}
          >
            <TransitionGroup component={null}>
              {groupIsEmpty ? (
                <Transition timeout={300} component={null}>
                  {(state) => (
                    <EmptyState
                      mountState={state}
                      text="Notes will appear here"
                    />
                  )}
                </Transition>
              ) : null}
            </TransitionGroup>
            <div
              style={{
                zIndex: 300,
              }}
            >
              <GridDropZone
                id={String(id)}
                boxesPerRow={columns}
                boxAmount={cards.length}
                disableDrag={dragDisabled}
                rowHeight={rowHeight}
                style={{
                  display: 'block',
                  minHeight: rowHeight,
                  background: 'transparent',
                  transition:
                    'height 0.5s ease, padding 0.1s linear, margin 0.1s linear',
                  paddingTop: groupIsEmpty ? 0 : 20,
                  marginTop: groupIsEmpty ? 0 : -20,

                  //This removes the space on the right for the last card on the row
                  marginRight: '-10px',
                }}
              >
                {cards.map((c) => {
                  return (
                    <GridItem
                      key={c.id}
                      style={{
                        display: 'flex',
                        alignItems: 'center',
                        justifyContent: 'flex-start',
                        width: postItWidth,
                      }}
                    >
                      <PostItCard
                        tooltipUID={tooltipUID}
                        onMouseDown={(e) => {
                          scrollPageOnDragHandler(e);
                        }}
                        card={c}
                        groupId={id}
                        onFocus={() => setDragDisabled(true)}
                        onBlur={() => setDragDisabled(false)}
                        userId={userId}
                        userRole={userRole}
                        removeCard={removeCard}
                        updateCard={updateCard}
                        dragDisabled={dragDisabled}
                        uploading={uploading === c.id}
                        errMsg={errMsg}
                        setErrMsg={setErrMsg}
                        openFileDialog={open}
                        setEditImage={(card) => setCardImageToEdit(card)}
                        copyEnabled={copyEnabled}
                        copyAction={copyAction}
                      />
                    </GridItem>
                  );
                })}
              </GridDropZone>
            </div>
          </GroupCards>
        )}
        <Tooltip id={tooltipUID} effect="float" capitalize />
      </GroupContent>
      {!isDesktop && !deleteGroupHide ? (
        <GroupDelete>
          <ButtonRound
            level="secondary"
            iconName="Trash"
            size="small"
            disabled={preventDelete}
            tooltip={tooltipMessage}
            onClick={handleDelete}
          />
        </GroupDelete>
      ) : null}
    </GroupWrapper>
  );
};
Leave a Comment