Merge branch 'feat/inda-383-daily-stat' into 'main'
[ProtonMail-WebClient.git] / applications / calendar / src / app / components / eventModal / CreateEventPopover.tsx
blobad8a2bb69020ca0ca9dc715a58a6bd51430c57e1
1 import type { CSSProperties, MouseEventHandler, Ref } from 'react';
2 import { useEffect, useRef, useState } from 'react';
4 import { c } from 'ttag';
6 import { Button } from '@proton/atoms';
7 import { PrimaryButton, useBusySlotsAvailable } from '@proton/components';
8 import { useMailSettings } from '@proton/mail/mailSettings/hooks';
9 import type { VIEWS } from '@proton/shared/lib/calendar/constants';
10 import type { WeekStartsOn } from '@proton/shared/lib/date-fns-utc/interface';
11 import type { Address } from '@proton/shared/lib/interfaces';
12 import type { EventModel } from '@proton/shared/lib/interfaces/calendar';
13 import debounce from '@proton/utils/debounce';
14 import throttle from '@proton/utils/throttle';
16 import { getCannotSaveEvent } from '../../helpers/event';
17 import { useRect } from '../../hooks/useRect';
18 import type { InviteActions } from '../../interfaces/Invite';
19 import { INVITE_ACTION_TYPES } from '../../interfaces/Invite';
20 import { busySlotsActions } from '../../store/busySlots/busySlotsSlice';
21 import { useCalendarDispatch } from '../../store/hooks';
22 import PopoverContainer from '../events/PopoverContainer';
23 import PopoverFooter from '../events/PopoverFooter';
24 import PopoverHeader from '../events/PopoverHeader';
25 import EventForm from './EventForm';
26 import validateEventModel from './eventForm/validateEventModel';
27 import { ACTION, useForm } from './hooks/useForm';
29 interface Props {
30     isSmallViewport: boolean;
31     displayWeekNumbers: boolean;
32     weekStartsOn: WeekStartsOn;
33     model: EventModel;
34     addresses: Address[];
35     onSave: (inviteActions: InviteActions) => Promise<void>;
36     onClose: () => void;
37     onEdit: (value: EventModel) => void;
38     style: CSSProperties;
39     popoverRef: Ref<HTMLDivElement>;
40     setModel: (value: EventModel) => void;
41     isCreateEvent: boolean;
42     isInvitation: boolean;
43     isDraggingDisabled?: boolean;
44     isDrawerApp?: boolean;
45     view: VIEWS;
48 const CreateEventPopover = ({
49     model,
50     setModel,
51     onSave,
52     onEdit,
53     onClose,
54     style,
55     popoverRef,
56     displayWeekNumbers,
57     weekStartsOn,
58     addresses,
59     isSmallViewport,
60     isCreateEvent,
61     isInvitation,
62     isDraggingDisabled = false,
63     isDrawerApp,
64     view,
65 }: Props) => {
66     const [mailSettings] = useMailSettings();
67     const dispatch = useCalendarDispatch();
68     const isBusySlotsAvailable = useBusySlotsAvailable();
69     const [participantError, setParticipantError] = useState(false);
70     const errors = { ...validateEventModel(model), participantError };
71     const cannotSave = getCannotSaveEvent({
72         isOwnedCalendar: model.calendar.isOwned,
73         isOrganizer: model.isOrganizer,
74         numberOfAttendees: model.attendees.length,
75         maxAttendees: mailSettings?.RecipientLimit,
76     });
77     const formRef = useRef<HTMLFormElement>(null);
78     const { isSubmitted, loadingAction, lastAction, handleSubmit } = useForm({
79         containerEl: formRef.current,
80         errors,
81         onSave,
82     });
83     const formRect = useRect(formRef.current);
85     const handleMore = () => {
86         onEdit(model);
87     };
88     // new events have no uid yet
89     const inviteActions = {
90         // the type will be more properly assessed in getSaveEventActions by getCorrectedSaveEventActions
91         type: model.isAttendee ? INVITE_ACTION_TYPES.NONE : INVITE_ACTION_TYPES.SEND_INVITATION,
92         selfAddress: model.selfAddress,
93     };
95     const [isDragging, setIsDragging] = useState(false);
96     const [offset, setOffset] = useState({ x: 0, y: 0 });
97     const [initialPosition, setInitialPosition] = useState({ clientX: 0, clientY: 0 });
98     const { clientX, clientY } = initialPosition;
100     const dragStyle: CSSProperties = {
101         transform: `translate3d(${offset.x}px, ${offset.y}px, 0)`,
102     };
103     const mergedStyle = isSmallViewport ? dragStyle : { ...style, ...dragStyle };
105     const handleMouseDown: MouseEventHandler<HTMLElement> = (event) => {
106         event.preventDefault();
107         setInitialPosition(event);
108         setIsDragging(true);
109     };
111     const handleStopDragging = () => {
112         setIsDragging(false);
113     };
115     const handleMouseMove = (event: MouseEvent) => {
116         const prevOffset = offset;
117         const cursorMoveXOffset = event.clientX - clientX;
118         const cursorMoveYOffset = event.clientY - clientY;
120         setOffset(({ x, y }) => ({
121             x:
122                 event.clientX >= 0 && event.clientX <= document.documentElement.clientWidth
123                     ? prevOffset.x + cursorMoveXOffset
124                     : x,
125             y:
126                 event.clientY >= 0 && event.clientY <= document.documentElement.clientHeight
127                     ? prevOffset.y + cursorMoveYOffset
128                     : y,
129         }));
130     };
132     useEffect(() => {
133         if (isBusySlotsAvailable) {
134             dispatch(busySlotsActions.setDisplay(true));
135         }
136     }, []);
138     useEffect(() => {
139         if (!formRect) {
140             return;
141         }
143         // When there's an existing offset and added content e.g. a participant group
144         // causes the popover to go off screen, we adjust the offset to stick to the top
145         if (formRect.top < 0 && offset.y && !isDragging) {
146             // Debouncing lets us have a slightly better flicker
147             debounce(() => setOffset((prevState) => ({ ...prevState, y: prevState.y - formRect.top })), 50)();
148         }
149     }, [formRect?.top]);
151     useEffect(() => {
152         if (isDraggingDisabled) {
153             return;
154         }
156         const throttledMouseMove = throttle(handleMouseMove, 20);
157         const debouncedStopDragging = debounce(handleStopDragging, 50);
159         if (isDragging) {
160             document.addEventListener('mousemove', throttledMouseMove);
161             document.addEventListener('mouseup', debouncedStopDragging);
162             document.addEventListener('mouseleave', debouncedStopDragging);
163         }
165         return () => {
166             document.removeEventListener('mousemove', throttledMouseMove);
167             document.removeEventListener('mouseup', debouncedStopDragging);
168             document.removeEventListener('mouseleave', debouncedStopDragging);
169         };
170     }, [isDraggingDisabled, isDragging]);
172     return (
173         <PopoverContainer style={mergedStyle} className="eventpopover" ref={popoverRef} onClose={onClose}>
174             <form
175                 onSubmit={(e) => {
176                     e.preventDefault();
177                     handleSubmit(inviteActions);
178                 }}
179                 className="form--icon-labels"
180                 ref={formRef}
181             >
182                 <PopoverHeader
183                     className={isDraggingDisabled ? '' : 'eventpopover-header--draggable'}
184                     onMouseDown={handleMouseDown}
185                     onClose={onClose}
186                     isDraggable={!isDraggingDisabled}
187                 />
188                 <EventForm
189                     displayWeekNumbers={displayWeekNumbers}
190                     addresses={addresses}
191                     weekStartsOn={weekStartsOn}
192                     isSubmitted={isSubmitted}
193                     errors={errors}
194                     model={model}
195                     setModel={setModel}
196                     isMinimal
197                     isCreateEvent={isCreateEvent}
198                     isInvitation={isInvitation}
199                     setParticipantError={setParticipantError}
200                     isSmallViewport={isSmallViewport}
201                     isDrawerApp={isDrawerApp}
202                     view={view}
203                 />
204                 <PopoverFooter className="justify-end flex-nowrap flex-column-reverse sm:flex-row gap-2">
205                     <Button
206                         disabled={loadingAction}
207                         data-testid="create-event-popover:more-event-options"
208                         className="w-full sm:w-auto"
209                         onClick={handleMore}
210                     >{c('Action').t`More options`}</Button>
211                     <PrimaryButton
212                         data-testid="create-event-popover:save"
213                         type="submit"
214                         className={isDrawerApp ? 'w-full' : undefined}
215                         loading={loadingAction && lastAction === ACTION.SUBMIT}
216                         disabled={loadingAction || cannotSave}
217                     >
218                         {c('Action').t`Save`}
219                     </PrimaryButton>
220                 </PopoverFooter>
221             </form>
222         </PopoverContainer>
223     );
226 export default CreateEventPopover;