@@ -1,9 +1,10 @@
|
||||
import { userData } from "@/features/User/userDataTypes";
|
||||
import { Box, Typography } from "@linagora/twake-mui";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { Dispatch, SetStateAction, useState } from "react";
|
||||
import { useI18n } from "twake-i18n";
|
||||
import { ContextualizedEvent } from "../EventsTypes";
|
||||
import { RSVPButton } from "./RSVPButton";
|
||||
import { PartStat } from "@/features/User/models/attendee";
|
||||
|
||||
interface AttendanceValidationProps {
|
||||
contextualizedEvent: ContextualizedEvent;
|
||||
@@ -20,6 +21,8 @@ export function AttendanceValidation({
|
||||
}: AttendanceValidationProps) {
|
||||
const { currentUserAttendee, isOwn } = contextualizedEvent;
|
||||
const { t } = useI18n();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [loadingValue, setLoadingValue] = useState<PartStat | null>(null);
|
||||
|
||||
// Check if we should show RSVP buttons
|
||||
const hasNoAttendeesOrOrganizer =
|
||||
@@ -30,11 +33,19 @@ export function AttendanceValidation({
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleLoadingChange = (loading: boolean, value?: PartStat) => {
|
||||
setIsLoading(loading);
|
||||
setLoadingValue(loading && value ? value : null);
|
||||
};
|
||||
|
||||
const commonButtonProps = {
|
||||
contextualizedEvent,
|
||||
user,
|
||||
setAfterChoiceFunc,
|
||||
setOpenEditModePopup,
|
||||
isLoading,
|
||||
onLoadingChange: handleLoadingChange,
|
||||
loadingValue,
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useAppDispatch } from "@/app/hooks";
|
||||
import { PartStat } from "@/features/User/models/attendee";
|
||||
import { userData } from "@/features/User/userDataTypes";
|
||||
import { Button } from "@linagora/twake-mui";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { Button, CircularProgress, Box, Theme } from "@linagora/twake-mui";
|
||||
import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react";
|
||||
import { useI18n } from "twake-i18n";
|
||||
import { ContextualizedEvent } from "../EventsTypes";
|
||||
import { handleRSVPClick } from "./handleRSVPClick";
|
||||
@@ -21,6 +21,9 @@ interface RSVPButtonProps {
|
||||
user: userData | undefined;
|
||||
setAfterChoiceFunc: Dispatch<SetStateAction<Function | undefined>>;
|
||||
setOpenEditModePopup: Dispatch<SetStateAction<string | null>>;
|
||||
isLoading: boolean;
|
||||
onLoadingChange: (loading: boolean, value?: PartStat) => void;
|
||||
loadingValue: PartStat | null;
|
||||
}
|
||||
|
||||
export function RSVPButton({
|
||||
@@ -29,35 +32,98 @@ export function RSVPButton({
|
||||
user,
|
||||
setAfterChoiceFunc,
|
||||
setOpenEditModePopup,
|
||||
isLoading,
|
||||
onLoadingChange,
|
||||
loadingValue,
|
||||
}: RSVPButtonProps) {
|
||||
const { t } = useI18n();
|
||||
const dispatch = useAppDispatch();
|
||||
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 (
|
||||
<Button
|
||||
variant={
|
||||
currentUserAttendee?.partstat === rsvpValue ? "contained" : "outlined"
|
||||
}
|
||||
color={
|
||||
currentUserAttendee?.partstat === rsvpValue
|
||||
? rsvpColor[rsvpValue]
|
||||
: "primary"
|
||||
}
|
||||
variant={shouldShowActive ? "contained" : "outlined"}
|
||||
color={buttonColor}
|
||||
size="medium"
|
||||
sx={{ borderRadius: "50px" }}
|
||||
onClick={() =>
|
||||
handleRSVPClick(
|
||||
rsvpValue,
|
||||
contextualizedEvent,
|
||||
user,
|
||||
setAfterChoiceFunc,
|
||||
setOpenEditModePopup,
|
||||
dispatch
|
||||
)
|
||||
}
|
||||
sx={{
|
||||
borderRadius: "50px",
|
||||
// Override MUI's default disabled styles to keep the color
|
||||
"&.Mui-disabled": shouldShowActive
|
||||
? {
|
||||
backgroundColor: (theme: Theme) =>
|
||||
theme.palette[buttonColor].main,
|
||||
color: (theme: Theme) => theme.palette[buttonColor].contrastText,
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,15 +11,31 @@ export async function handleRSVPClick(
|
||||
user: userData | undefined,
|
||||
setAfterChoiceFunc: Dispatch<SetStateAction<Function | undefined>>,
|
||||
setOpenEditModePopup: Dispatch<SetStateAction<string | null>>,
|
||||
dispatch: AppDispatch
|
||||
dispatch: AppDispatch,
|
||||
onLoadingChange?: (loading: boolean, value?: PartStat) => void
|
||||
) {
|
||||
const { isRecurring, calendar, event } = contextualizedEvent;
|
||||
|
||||
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 {
|
||||
await handleRSVP(dispatch, calendar, user, event, rsvp, type);
|
||||
} catch (error) {
|
||||
console.error("Error handling RSVP:", error);
|
||||
// Clear loading on error
|
||||
if (onLoadingChange) {
|
||||
onLoadingChange(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
setOpenEditModePopup("attendance");
|
||||
@@ -28,6 +44,9 @@ export async function handleRSVPClick(
|
||||
await handleRSVP(dispatch, calendar, user, event, rsvp);
|
||||
} catch (error) {
|
||||
console.error("Error handling RSVP:", error);
|
||||
if (onLoadingChange) {
|
||||
onLoadingChange(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -435,7 +435,6 @@ export default function EventPreviewModal({
|
||||
actions={
|
||||
<AttendanceValidation
|
||||
contextualizedEvent={contextualizedEvent}
|
||||
calendarList={calendarList}
|
||||
user={user}
|
||||
setAfterChoiceFunc={setAfterChoiceFunc}
|
||||
setOpenEditModePopup={setOpenEditModePopup}
|
||||
|
||||
Reference in New Issue
Block a user