@@ -1,9 +1,10 @@
|
|||||||
import { userData } from "@/features/User/userDataTypes";
|
import { userData } from "@/features/User/userDataTypes";
|
||||||
import { Box, Typography } from "@linagora/twake-mui";
|
import { Box, Typography } from "@linagora/twake-mui";
|
||||||
import { Dispatch, SetStateAction } from "react";
|
import { Dispatch, SetStateAction, useState } from "react";
|
||||||
import { useI18n } from "twake-i18n";
|
import { useI18n } from "twake-i18n";
|
||||||
import { ContextualizedEvent } from "../EventsTypes";
|
import { ContextualizedEvent } from "../EventsTypes";
|
||||||
import { RSVPButton } from "./RSVPButton";
|
import { RSVPButton } from "./RSVPButton";
|
||||||
|
import { PartStat } from "@/features/User/models/attendee";
|
||||||
|
|
||||||
interface AttendanceValidationProps {
|
interface AttendanceValidationProps {
|
||||||
contextualizedEvent: ContextualizedEvent;
|
contextualizedEvent: ContextualizedEvent;
|
||||||
@@ -20,6 +21,8 @@ export function AttendanceValidation({
|
|||||||
}: AttendanceValidationProps) {
|
}: AttendanceValidationProps) {
|
||||||
const { currentUserAttendee, isOwn } = contextualizedEvent;
|
const { currentUserAttendee, isOwn } = contextualizedEvent;
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [loadingValue, setLoadingValue] = useState<PartStat | null>(null);
|
||||||
|
|
||||||
// Check if we should show RSVP buttons
|
// Check if we should show RSVP buttons
|
||||||
const hasNoAttendeesOrOrganizer =
|
const hasNoAttendeesOrOrganizer =
|
||||||
@@ -30,11 +33,19 @@ export function AttendanceValidation({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleLoadingChange = (loading: boolean, value?: PartStat) => {
|
||||||
|
setIsLoading(loading);
|
||||||
|
setLoadingValue(loading && value ? value : null);
|
||||||
|
};
|
||||||
|
|
||||||
const commonButtonProps = {
|
const commonButtonProps = {
|
||||||
contextualizedEvent,
|
contextualizedEvent,
|
||||||
user,
|
user,
|
||||||
setAfterChoiceFunc,
|
setAfterChoiceFunc,
|
||||||
setOpenEditModePopup,
|
setOpenEditModePopup,
|
||||||
|
isLoading,
|
||||||
|
onLoadingChange: handleLoadingChange,
|
||||||
|
loadingValue,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useAppDispatch } from "@/app/hooks";
|
import { useAppDispatch } from "@/app/hooks";
|
||||||
import { PartStat } from "@/features/User/models/attendee";
|
import { PartStat } from "@/features/User/models/attendee";
|
||||||
import { userData } from "@/features/User/userDataTypes";
|
import { userData } from "@/features/User/userDataTypes";
|
||||||
import { Button } from "@linagora/twake-mui";
|
import { Button, CircularProgress, Box, Theme } from "@linagora/twake-mui";
|
||||||
import { Dispatch, SetStateAction } from "react";
|
import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react";
|
||||||
import { useI18n } from "twake-i18n";
|
import { useI18n } from "twake-i18n";
|
||||||
import { ContextualizedEvent } from "../EventsTypes";
|
import { ContextualizedEvent } from "../EventsTypes";
|
||||||
import { handleRSVPClick } from "./handleRSVPClick";
|
import { handleRSVPClick } from "./handleRSVPClick";
|
||||||
@@ -21,6 +21,9 @@ interface RSVPButtonProps {
|
|||||||
user: userData | undefined;
|
user: userData | undefined;
|
||||||
setAfterChoiceFunc: Dispatch<SetStateAction<Function | undefined>>;
|
setAfterChoiceFunc: Dispatch<SetStateAction<Function | undefined>>;
|
||||||
setOpenEditModePopup: Dispatch<SetStateAction<string | null>>;
|
setOpenEditModePopup: Dispatch<SetStateAction<string | null>>;
|
||||||
|
isLoading: boolean;
|
||||||
|
onLoadingChange: (loading: boolean, value?: PartStat) => void;
|
||||||
|
loadingValue: PartStat | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RSVPButton({
|
export function RSVPButton({
|
||||||
@@ -29,35 +32,98 @@ export function RSVPButton({
|
|||||||
user,
|
user,
|
||||||
setAfterChoiceFunc,
|
setAfterChoiceFunc,
|
||||||
setOpenEditModePopup,
|
setOpenEditModePopup,
|
||||||
|
isLoading,
|
||||||
|
onLoadingChange,
|
||||||
|
loadingValue,
|
||||||
}: RSVPButtonProps) {
|
}: RSVPButtonProps) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { currentUserAttendee } = contextualizedEvent;
|
const { currentUserAttendee } = contextualizedEvent;
|
||||||
|
const showLoading = isLoading && loadingValue === rsvpValue;
|
||||||
|
const previousPartstatRef = useRef<PartStat | undefined>(
|
||||||
|
currentUserAttendee?.partstat
|
||||||
|
);
|
||||||
|
|
||||||
|
// Detect when attendee status changes (via WebSocket) and clear loading
|
||||||
|
useEffect(() => {
|
||||||
|
const currentPartstat = currentUserAttendee?.partstat;
|
||||||
|
|
||||||
|
if (
|
||||||
|
isLoading &&
|
||||||
|
previousPartstatRef.current !== undefined &&
|
||||||
|
currentPartstat !== previousPartstatRef.current
|
||||||
|
) {
|
||||||
|
onLoadingChange(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
previousPartstatRef.current = currentPartstat;
|
||||||
|
}, [currentUserAttendee?.partstat, isLoading, rsvpValue, onLoadingChange]);
|
||||||
|
|
||||||
|
const handleClick = async () => {
|
||||||
|
// Store current partstat before making changes
|
||||||
|
previousPartstatRef.current = currentUserAttendee?.partstat;
|
||||||
|
if (previousPartstatRef.current === rsvpValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For recurring events, don't set loading yet - wait for modal choice
|
||||||
|
if (!contextualizedEvent.isRecurring) {
|
||||||
|
onLoadingChange(true, rsvpValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await handleRSVPClick(
|
||||||
|
rsvpValue,
|
||||||
|
contextualizedEvent,
|
||||||
|
user,
|
||||||
|
setAfterChoiceFunc,
|
||||||
|
setOpenEditModePopup,
|
||||||
|
dispatch,
|
||||||
|
onLoadingChange
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`[RSVPButton ${rsvpValue}] Error in handleRSVPClick:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
// Clear loading on error
|
||||||
|
onLoadingChange(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isCurrentlyActive = currentUserAttendee?.partstat === rsvpValue;
|
||||||
|
|
||||||
|
// Show as active (colored) if:
|
||||||
|
// 1. This button is currently loading, OR
|
||||||
|
// 2. This is the active status AND nothing is loading
|
||||||
|
const shouldShowActive = showLoading || (isCurrentlyActive && !isLoading);
|
||||||
|
|
||||||
|
const buttonColor = shouldShowActive ? rsvpColor[rsvpValue] : "primary";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
variant={
|
variant={shouldShowActive ? "contained" : "outlined"}
|
||||||
currentUserAttendee?.partstat === rsvpValue ? "contained" : "outlined"
|
color={buttonColor}
|
||||||
}
|
|
||||||
color={
|
|
||||||
currentUserAttendee?.partstat === rsvpValue
|
|
||||||
? rsvpColor[rsvpValue]
|
|
||||||
: "primary"
|
|
||||||
}
|
|
||||||
size="medium"
|
size="medium"
|
||||||
sx={{ borderRadius: "50px" }}
|
sx={{
|
||||||
onClick={() =>
|
borderRadius: "50px",
|
||||||
handleRSVPClick(
|
// Override MUI's default disabled styles to keep the color
|
||||||
rsvpValue,
|
"&.Mui-disabled": shouldShowActive
|
||||||
contextualizedEvent,
|
? {
|
||||||
user,
|
backgroundColor: (theme: Theme) =>
|
||||||
setAfterChoiceFunc,
|
theme.palette[buttonColor].main,
|
||||||
setOpenEditModePopup,
|
color: (theme: Theme) => theme.palette[buttonColor].contrastText,
|
||||||
dispatch
|
borderColor: (theme: Theme) => theme.palette[buttonColor].main,
|
||||||
)
|
}
|
||||||
}
|
: {},
|
||||||
|
}}
|
||||||
|
onClick={handleClick}
|
||||||
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
{t(`eventPreview.${rsvpValue}`)}
|
<Box display="flex" alignItems="center" gap={1}>
|
||||||
|
{showLoading && <CircularProgress size={20} color="inherit" />}
|
||||||
|
{t(`eventPreview.${rsvpValue}`)}
|
||||||
|
</Box>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,15 +11,31 @@ export async function handleRSVPClick(
|
|||||||
user: userData | undefined,
|
user: userData | undefined,
|
||||||
setAfterChoiceFunc: Dispatch<SetStateAction<Function | undefined>>,
|
setAfterChoiceFunc: Dispatch<SetStateAction<Function | undefined>>,
|
||||||
setOpenEditModePopup: Dispatch<SetStateAction<string | null>>,
|
setOpenEditModePopup: Dispatch<SetStateAction<string | null>>,
|
||||||
dispatch: AppDispatch
|
dispatch: AppDispatch,
|
||||||
|
onLoadingChange?: (loading: boolean, value?: PartStat) => void
|
||||||
) {
|
) {
|
||||||
const { isRecurring, calendar, event } = contextualizedEvent;
|
const { isRecurring, calendar, event } = contextualizedEvent;
|
||||||
|
|
||||||
if (isRecurring) {
|
if (isRecurring) {
|
||||||
setAfterChoiceFunc(() => async (type: string) => {
|
setAfterChoiceFunc(() => async (type: string | null) => {
|
||||||
|
// If user cancelled the modal, don't proceed
|
||||||
|
if (type === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now start loading since user made a choice
|
||||||
|
if (onLoadingChange) {
|
||||||
|
onLoadingChange(true, rsvp);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await handleRSVP(dispatch, calendar, user, event, rsvp, type);
|
await handleRSVP(dispatch, calendar, user, event, rsvp, type);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error handling RSVP:", error);
|
console.error("Error handling RSVP:", error);
|
||||||
|
// Clear loading on error
|
||||||
|
if (onLoadingChange) {
|
||||||
|
onLoadingChange(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
setOpenEditModePopup("attendance");
|
setOpenEditModePopup("attendance");
|
||||||
@@ -28,6 +44,9 @@ export async function handleRSVPClick(
|
|||||||
await handleRSVP(dispatch, calendar, user, event, rsvp);
|
await handleRSVP(dispatch, calendar, user, event, rsvp);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error handling RSVP:", error);
|
console.error("Error handling RSVP:", error);
|
||||||
|
if (onLoadingChange) {
|
||||||
|
onLoadingChange(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -435,7 +435,6 @@ export default function EventPreviewModal({
|
|||||||
actions={
|
actions={
|
||||||
<AttendanceValidation
|
<AttendanceValidation
|
||||||
contextualizedEvent={contextualizedEvent}
|
contextualizedEvent={contextualizedEvent}
|
||||||
calendarList={calendarList}
|
|
||||||
user={user}
|
user={user}
|
||||||
setAfterChoiceFunc={setAfterChoiceFunc}
|
setAfterChoiceFunc={setAfterChoiceFunc}
|
||||||
setOpenEditModePopup={setOpenEditModePopup}
|
setOpenEditModePopup={setOpenEditModePopup}
|
||||||
|
|||||||
Reference in New Issue
Block a user