-
{
const [formErrors, setFormErrors] = useState({});
const { showSnackbar } = useSnackbar();
- const handleEventUpdate = (
- eventID,
- values,
- startTimeOriginal,
- durationOriginal
- ) => async () => {
- const errors = validateEventForm(values, projectToEdit);
- if (!errors) {
- let theUpdatedEvent = {};
+ const handleEventUpdate =
+ (eventID, values, startTimeOriginal, durationOriginal) => async () => {
+ const errors = validateEventForm(values, projectToEdit);
+ if (!errors) {
+ let theUpdatedEvent = {};
- if (values.name) {
- theUpdatedEvent = {
- ...theUpdatedEvent,
- name: values.name,
- };
- }
+ if (values.name) {
+ theUpdatedEvent = {
+ ...theUpdatedEvent,
+ name: values.name,
+ };
+ }
+
+ if (values.eventType) {
+ theUpdatedEvent = {
+ ...theUpdatedEvent,
+ eventType: values.eventType,
+ };
+ }
- if (values.eventType) {
theUpdatedEvent = {
...theUpdatedEvent,
- eventType: values.eventType,
+ description: values.description,
};
- }
- theUpdatedEvent = {
- ...theUpdatedEvent,
- description: values.description,
- };
+ if (values.videoConferenceLink) {
+ theUpdatedEvent = {
+ ...theUpdatedEvent,
+ videoConferenceLink: values.videoConferenceLink,
+ };
+ }
- if (values.videoConferenceLink) {
+ // Set updated date to today and add it to the object
+ const updatedDate = new Date().toISOString();
theUpdatedEvent = {
...theUpdatedEvent,
- videoConferenceLink: values.videoConferenceLink,
+ updatedDate,
};
- }
- // Set updated date to today and add it to the object
- const updatedDate = new Date().toISOString();
- theUpdatedEvent = {
- ...theUpdatedEvent,
- updatedDate,
- };
+ // Find next occurance of Day in the future
+ // Assign new start time and end time
+ const date = findNextOccuranceOfDay(values.day);
+ const startTimeDate = timeConvertFromForm(date, values.startTime);
+ const endTime = addDurationToTime(startTimeDate, values.duration);
- // Find next occurance of Day in the future
- // Assign new start time and end time
- const date = findNextOccuranceOfDay(values.day);
- const startTimeDate = timeConvertFromForm(date, values.startTime);
- const endTime = addDurationToTime(startTimeDate, values.duration);
+ // Revert timestamps to GMT
+ const startDateTimeGMT = new Date(startTimeDate).toISOString();
+ const endTimeGMT = new Date(endTime).toISOString();
- // Revert timestamps to GMT
- const startDateTimeGMT = new Date(startTimeDate).toISOString();
- const endTimeGMT = new Date(endTime).toISOString();
-
- theUpdatedEvent = {
- ...theUpdatedEvent,
- date: startDateTimeGMT,
- startTime: startDateTimeGMT,
- endTime: endTimeGMT,
- duration: values.duration
- };
+ theUpdatedEvent = {
+ ...theUpdatedEvent,
+ date: startDateTimeGMT,
+ startTime: startDateTimeGMT,
+ endTime: endTimeGMT,
+ duration: values.duration,
+ };
- updateRecurringEvent(theUpdatedEvent, eventID);
- showSnackbar("Recurring event updated", 'info')
- setSelectedEvent(null);
- }
- setFormErrors(errors);
- };
+ updateRecurringEvent(theUpdatedEvent, eventID);
+ showSnackbar('Recurring event updated', 'info');
+ setSelectedEvent(null);
+ }
+ setFormErrors(errors);
+ };
const handleEventDelete = (eventID) => async () => {
deleteRecurringEvent(eventID);
setSelectedEvent(null);
- showSnackbar("Recurring event deleted", 'info');
+ showSnackbar('Recurring event deleted', 'info');
};
return (
-
{selectedEvent && (
);
};
-export default EditMeetingTimes;
\ No newline at end of file
+export default EditMeetingTimes;
diff --git a/client/src/components/manageProjects/editProject.jsx b/client/src/components/manageProjects/editProject.jsx
index 784e08cae..786f46453 100644
--- a/client/src/components/manageProjects/editProject.jsx
+++ b/client/src/components/manageProjects/editProject.jsx
@@ -1,4 +1,4 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useCallback } from 'react';
import EditMeetingTimes from './editMeetingTimes';
import CreateNewEvent from './createNewEvent';
import readableEvent from './utilities/readableEvent';
@@ -6,13 +6,99 @@ import ProjectForm from '../ProjectForm';
import { simpleInputs, additionalInputsForEdit } from '../data';
import TitledBox from '../parts/boxes/TitledBox';
+import { styled } from '@mui/material/styles';
+import CloseIcon from '@mui/icons-material/Close';
import EditIcon from '../../svg/Icon_Edit.svg?react';
import PlusIcon from '../../svg/PlusIcon.svg?react';
-import { Typography, Box } from '@mui/material';
+import {
+ Typography,
+ Box,
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ IconButton,
+ Button,
+ List,
+ ListItem,
+ ListItemButton,
+ ListItemText,
+} from '@mui/material';
-// Need to hold user state to check which type of user they are and conditionally render editing fields in this component
-// for user level block access to all except for the ones checked
+// --- Styled Components: Centralized & Reusable UI Elements ---
+// Leverages MUI's `styled` utility for cleaner, component-specific styles
+// enhancing maintainability and adherence to design principles
+
+// StyledListItem: Base style for each event row
+// Padding is applied to the clickable `ListItemButton` for full-width hover effect
+const StyledListItem = styled(ListItem)(({ theme }) => ({
+ display: 'flex',
+ flexDirection: 'column',
+ borderBottom: `1px solid ${theme.palette.grey[200] || '#ecebed'}`,
+ padding: 0,
+}));
+
+// StyledListItemButton: The clickable area for each event row
+// Contains core text and hover styling for consistent UX
+const StyledListItemButton = styled(ListItemButton)(({ theme }) => ({
+ padding: '8px 0',
+ fontFamily:
+ "'aliseoregular', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
+ WebkitFontSmoothing: 'antialiased',
+ MozOsxFontSmoothing: 'grayscale',
+ textAlign: 'left',
+ width: '100%',
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'center',
+ '&:hover': {
+ backgroundColor: '#f2f2f2',
+ },
+}));
+
+// DetailsText: Consistent typography for secondary event details
+const DetailsText = styled(Typography)(({ theme }) => ({
+ fontFamily: 'Arial, Helvetica, sans-serif',
+ fontStyle: 'normal',
+ fontWeight: 'normal',
+ fontSize: '14px',
+ lineHeight: '24px',
+ whiteSpace: 'nowrap',
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ display: 'block',
+}));
+
+
+
+
+// DescriptionText: Specific typography for the event description line
+const DescriptionText = styled(Typography)(({ theme }) => ({
+ fontFamily: 'Arial, Helvetica, sans-serif',
+ fontStyle: 'normal',
+ fontWeight: 'normal',
+ fontSize: '14px',
+ lineHeight: '24px',
+ color: '#5c5c5c',
+ height: '24px',
+ whiteSpace: 'nowrap',
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ display: 'block',
+}));
+
+/**
+ * EditProject: A component for managing and editing project details
+ * @param {Object} projectToEdit - The project to be edited
+ * @param {Array} recurringEvents - All recurring events associated with the current project
+ * @param {Function} createNewRecurringEvent - Function to create a new recurring event
+ * @param {Function} deleteRecurringEvent - Function to delete a recurring event
+ * @param {Function} updateRecurringEvent - Function to update a recurring event
+ * @param {Array} regularEvents - All regular events associated with the current project
+ * @param {Function} updateRegularEvent - Function to update a regular event
+ * @returns {ReactElement} - A component with the project form, recurring events, and regular
+ * events sections
+ */
const EditProject = ({
projectToEdit,
recurringEvents,
@@ -31,26 +117,80 @@ const EditProject = ({
slackUrl: projectToEdit.slackUrl,
googleDriveUrl: projectToEdit.googleDriveUrl,
hflaWebsiteUrl: projectToEdit.hflaWebsiteUrl,
- // this feature is commented out as per the PR #1577
- // partners: projectToEdit.partners,
- // managedByUsers: projectToEdit.managedByUsers,
- // projectStatus: projectToEdit.projectStatus,
- // comment out as per PR #1584
- // googleDriveId: projectToEdit.googleDriveId,
- // createdDate: new Date(projectToEdit.createdDate)
+ // Note: 'partners', 'managedByUsers', 'projectStatus', and 'googleDriveId' fields
+ // are commented out as per recent PRs (#1577, #1584) to streamline project data
});
// eslint-disable-next-line no-unused-vars
const [rEvents, setREvents] = useState([]);
const [regularEventsState, setRegularEventsState] = useState([]);
- const [selectedEvent, setSelectedEvent] = useState();
- const [isCreateNew, setIsCreateNew] = useState();
+ const [selectedEvent, setSelectedEvent] = useState(null);
+ const [isCreateNew, setIsCreateNew] = useState(false);
- // States for alerts
+ // State for displaying event-related alerts (e.g., success messages)
const [eventAlert, setEventAlert] = useState(null);
- // test
+ // `buttonKey`: Manages the key for the "Add New Event" button
+ // Incrementing this key forces the button to re-mount, fixing a stuck ripple
+ // effect when the CreateNewEvent modal is closed via the Escape key
+ const [buttonKey, setButtonKey] = useState(0);
+
+ // --- Ripple Effect Fix for List Items (Scalable Solution) ---
+ // These states prevent stuck ripple/hover effects on ListItemButtons
+ // when associated modals close via the Escape key, leveraging React's `key` prop
+ // for targeted component re-mounting.
+
+ // `lastOpenedEventId`: Tracks the ID of the specific event whose modal was last opened
+ const [lastOpenedEventId, setLastOpenedEventId] = useState(null);
+
+ // `forceRemountEventId`: Signals which specific ListItemButton needs to be re-mounted
+ // Changing its key value forces a full component reset for that item only
+ const [forceRemountEventId, setForceRemountEventId] = useState(null);
+ // --- End Ripple Effect Fix States ---
+
+ // `handleSelectEvent`: Opens the Edit Meeting Times modal.
+ // It captures the clicked event's ID to track which list item should be reset
+ // if the modal closes via the Escape key.
+ const handleSelectEvent = useCallback((event) => {
+ setSelectedEvent(event);
+ // Determine the correct unique ID for the selected event
+ const idToTrack = event._id || event.event_id;
+ setLastOpenedEventId(idToTrack);
+ setForceRemountEventId(null); // Clear any pending re-mounts for other items
+ }, []);
+
+ // `handleCloseEditMeetingModal`: Closes the Edit Meeting Times modal.
+ // If the modal was closed by the Escape key, it flags the corresponding
+ // `ListItemButton` for re-mounting to clear any stuck visual states.
+ const handleCloseEditMeetingModal = useCallback(
+ (event, reason) => {
+ setSelectedEvent(null);
+ if (reason === 'escapeKeyDown' && lastOpenedEventId) {
+ setForceRemountEventId(lastOpenedEventId);
+ }
+ setLastOpenedEventId(null);
+ },
+ [lastOpenedEventId]
+ );
+
+ // `handleOpenCreateNewModal`: Sets the state to open the Create New Event modal
+ const handleOpenCreateNewModal = useCallback(() => {
+ setIsCreateNew(true);
+ }, []);
+
+ // `handleCloseCreateNewModal`: Closes the Create New Event modal.
+ // If closed by Escape key, it triggers a `buttonKey` change for the "Add New Event"
+ // button, forcing its re-mount to clear any stuck ripple effect.
+ const handleCloseCreateNewModal = useCallback((event, reason) => {
+ setIsCreateNew(false);
+ if (reason === 'escapeKeyDown') {
+ setButtonKey((prevKey) => prevKey + 1);
+ }
+ }, []);
+
+ // Populates `regularEventsState` by filtering and mapping regular events
+ // associated with the current project, sorting them by most recent first.
useEffect(() => {
if (regularEvents) {
setRegularEventsState(
@@ -63,7 +203,8 @@ const EditProject = ({
}
}, [projectToEdit, regularEvents, setRegularEventsState]);
- // Get project recurring events when component loads
+ // Populates `rEvents` (recurring events) by filtering and mapping them
+ // for the current project, sorting by day of the week.
useEffect(() => {
if (recurringEvents) {
setREvents(
@@ -78,26 +219,103 @@ const EditProject = ({
return (
-
-
-
-
-
-
+ {/* Dialog for editing recurring meeting times */}
+
+
+ {/* Dialog for creating new events */}
+
+ {/* Main project form for editing project details */}
+ {/* Section for displaying and managing recurring events */}
}
sx={{
- display: 'flex',
- '&:hover': { color: 'red', cursor: 'pointer' },
+ fontSize: '14px',
+ fontWeight: '600',
+ color: 'black',
+ textTransform: 'none',
+ '&:hover': {
+ color: 'error.main',
+ },
+ maxWidth: '138px',
+ width: '138px',
+ justifyContent: 'center',
}}
- onClick={() => setIsCreateNew(true)}
>
-
-
- Add New Event
-
-
+ Add New Event
+
}
>
-
+
+
);
};
-function RegularEvent({ event, updateRegularEvent }) {
+/**
+ * RegularEvent: Displays a single regular event item within a list.
+ * It's responsible for rendering event details and handling selection to open an edit modal.
+ * @param {Object} event - The regular event object to display. Includes details like name, time, type, and check-in status.
+ * @param {Function} updateRegularEvent - A function to call when a regular event needs to be updated. (Note: Not directly used in the provided JSX, but passed as a prop.)
+ * @param {Function} handleSelectEvent - A callback function to invoke when the event item is clicked, typically to select it and open a corresponding edit modal.
+ * @returns {ReactElement} - A list item component representing a single regular event, with clickable functionality.
+ */
+const RegularEvent = ({ event, updateRegularEvent, handleSelectEvent }) => {
return (
-