From 7537b7d3ca089262bf403f61f6ea117a018a25ea Mon Sep 17 00:00:00 2001 From: Adam Abundis Date: Tue, 17 Jun 2025 15:29:53 -0700 Subject: [PATCH 1/2] refactor(project-edit): Standardize MUI & fix modal ripples (#1703) --- .../manageProjects/createNewEvent.jsx | 11 - .../manageProjects/editMeetingTimes.jsx | 116 ++-- .../components/manageProjects/editProject.jsx | 528 ++++++++++++++---- client/src/sass/ManageProjects.scss | 200 ------- 4 files changed, 483 insertions(+), 372 deletions(-) diff --git a/client/src/components/manageProjects/createNewEvent.jsx b/client/src/components/manageProjects/createNewEvent.jsx index f320b129a..33ea60881 100644 --- a/client/src/components/manageProjects/createNewEvent.jsx +++ b/client/src/components/manageProjects/createNewEvent.jsx @@ -84,17 +84,6 @@ const CreateNewEvent = ({ }; return (
- { 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 f6d3feaa2..e5baa0f76 100644 --- a/client/src/components/manageProjects/editProject.jsx +++ b/client/src/components/manageProjects/editProject.jsx @@ -1,18 +1,100 @@ -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'; import ProjectForm from '../ProjectForm'; import { simpleInputs, additionalInputsForEdit } from '../data'; import TitledBox from '../parts/boxes/TitledBox'; - +import { styled } from '@mui/material/styles'; import { ReactComponent as EditIcon } from '../../svg/Icon_Edit.svg'; import { ReactComponent as PlusIcon } from '../../svg/PlusIcon.svg'; +import CloseIcon from '@mui/icons-material/Close'; + +import { + Typography, + Box, + Dialog, + DialogTitle, + DialogContent, + IconButton, + Button, + List, + ListItem, + ListItemButton, + ListItemText, +} from '@mui/material'; + +// --- 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, +})); -import { Typography, Box } from '@mui/material'; +// 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', + }, +})); -// 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 +// 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 +113,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 +199,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 +215,129 @@ const EditProject = ({ return ( -
- -
-
- -
+ {/* Dialog for editing recurring meeting times */} + + + + Edit Meeting Times + + + + + + + + + + + {/* Dialog for creating new events */} + + + + Create New Event + + + + + + + + + + {/* Main project form for editing project details */} - - } 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 + } > -
-

{eventAlert}

-
    - {rEvents.map((event) => ( - // eslint-disable-next-line no-underscore-dangle -
  • - -
  • - ))} -
-
+ + + {eventAlert} + + + {rEvents.map((event) => { + // Determine the correct unique ID for the current recurring event + const currentEventId = event._id || event.event_id; + + return ( + // eslint-disable-next-line no-underscore-dangle + + handleSelectEvent(event)} + > + + + + {' '} + {`${event.dayOfTheWeek}, ${event.startTime} - ${event.endTime}; ${event.eventType}`} + + + {' '} + {`${event.description}`} + + + + {' '} + {' '} + + + + ); + })} + + + {/* Section for manually editing check-ins for regular (non-recurring) events */} -
-

{eventAlert}

-
    - {regularEventsState.map((event, index) => ( - - // eslint-dis able-next-line no-underscore-dangle - + + + {eventAlert} + + + {regularEventsState.map((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 ( -
  • - -
  • - ) -} - - + + handleSelectEvent(event)}> + + + + {`${event.dayOfTheWeek}, ${event.startTime} - ${event.endTime}; ${event.eventType}`} + + + {`${new Date(event.raw.startTime).toLocaleDateString()}`} + + + Is this event available for check in now?:{' '} + {`${event.checkInReady ? 'Yes' : 'No'}`} + + + + + ); +}; export default EditProject; diff --git a/client/src/sass/ManageProjects.scss b/client/src/sass/ManageProjects.scss index 94480193b..d184bfd54 100644 --- a/client/src/sass/ManageProjects.scss +++ b/client/src/sass/ManageProjects.scss @@ -26,198 +26,6 @@ } } -.project-list { - display: flex; - flex-direction: column; - cursor: pointer; -} - -.project-list-item li { - margin-bottom: 5px; - border: 2px black solid; - border-radius: 5px; - padding: 0.3em; - &:hover { - background-color: lightgrey; - } -} - -.project-list-button { - font-family: 'aliseoregular', -apple-system, BlinkMacSystemFont, 'Segoe UI', - 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', - 'Helvetica Neue', sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - padding: 0; - text-align: left; -} - -.project-list-heading { - margin: 20px 0; - font-size: 18px; -} - -.project-sub-heading { - margin: 20px 0 20px 0; -} - -.project-warning-text { - color: #fa114f; - font-size: smaller; - margin-bottom: 1rem; -} - -div.editable-field { - font-weight: normal; - font-size: 18px; - max-width: none; - width: 400px; -} - -.editable-field { - font-family: Arial, Helvetica, sans-serif; - font-size: 14px; -} - -.editable-field-div { - margin-bottom: 20px; -} - -.section-name { - font-size: 16px; -} - -.section-content { - font-size: 14px; - text-transform: none; - font-family: Arial, Helvetica, sans-serif; -} - -.project-edit-title { - margin-bottom: 4px; - display: flex; - flex-direction: row; -} - -.project-edit-button { - flex: 2; - padding-left: 8px; - color: #fa114f; - cursor: pointer; - font-family: Arial, Helvetica, sans-serif; - text-align: left; -} - -.button-edit { - background-color: black; - width: 150px; - color: white; - font-size: 20px; - padding: 10px; - border-radius: 5px; - margin: 10px 0px; -} - -.display-events { - margin-top: 15px; - margin-bottom: 10px; -} - -.event-list { - margin-bottom: 40px; - h3 { - width: 190px; - padding: 7px 18px 10px; - font-family: Arial, Helvetica, sans-serif; - font-style: normal; - font-weight: bold; - font-size: 18px; - line-height: 24px; - color: #000000; - background-color: #f2f2f2; - } - - ul { - padding-top: 4px; - } - - li { - display: flex; - padding: 8px 0; - flex-direction: column; - border-bottom: 1px solid #ecebed; - } - - li:hover { - background-color: #f2f2f2; - } - - button { - font-family: 'aliseoregular', -apple-system, BlinkMacSystemFont, 'Segoe UI', - 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', - 'Helvetica Neue', sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - padding: 0; - text-align: left; - } - - &-details, - &-description { - font-family: Arial, Helvetica, sans-serif; - font-style: normal; - font-weight: normal; - font-size: 14px; - line-height: 24px; - } - - &-description { - color: #5c5c5c; - height: 24px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } -} - -.edit-meeting-modal { - width: 92%; - display: none; - background-color: white; - opacity: 1; - &.active { - position: absolute; - height: 100%; - width: 100%; - padding-left: 8px; - padding-right: 50px; - top: 24px; - display: block; - background-color: rgba(255, 255, 255, 0.763); - z-index: 3; - } -} - -.meeting-cancel-button { - background-color: transparent; - border: none; - width: 98%; - font-family: Helvetica, sans-serif; - font-style: normal; - font-weight: bold; - font-size: 22px; - line-height: 24px; - color: #000000; - text-align: right; -} - -.event-alert { - position: fixed; - bottom: 20px; - right: 20px; - background-color: white; -} - .event-form-box { min-width: 100%; min-height: 540px; @@ -284,11 +92,3 @@ div.editable-field { .create-form-button:active { background-color: #969595; } - -.event-list-details { - display: flex; -} - -.edit-icon { - margin-left: 22px; -} From 59fe214abf1194fbd84d050559c51398e8bbd14d Mon Sep 17 00:00:00 2001 From: Adam Abundis Date: Tue, 17 Jun 2025 17:54:00 -0700 Subject: [PATCH 2/2] style(modal): Remove redundant titles from dialog headers --- .../components/manageProjects/editProject.jsx | 32 ++----------------- 1 file changed, 3 insertions(+), 29 deletions(-) diff --git a/client/src/components/manageProjects/editProject.jsx b/client/src/components/manageProjects/editProject.jsx index e5baa0f76..53bf6275d 100644 --- a/client/src/components/manageProjects/editProject.jsx +++ b/client/src/components/manageProjects/editProject.jsx @@ -227,24 +227,11 @@ const EditProject = ({ m: 0, p: 2, display: 'flex', - justifyContent: 'space-between', + justifyContent: 'flex-end', alignItems: 'center', minHeight: '64px', }} > - - Edit Meeting Times - - - Create New Event -