Untitled
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