From 37108255211599e9ce56dd158c2a482cdfb2cffc Mon Sep 17 00:00:00 2001 From: Pratik Tomar Date: Wed, 22 Oct 2025 05:24:55 +0530 Subject: [PATCH 1/3] Feature/Issue-622 | Feature/issue-548 | Mods and Authors can edit event now --- .../contexts/ModalContext/useProvideModal.jsx | 4 + client/src/features/form/FormCreateEvent.jsx | 22 +- client/src/features/form/FormError.jsx | 26 ++ .../src/features/form/FormScheduleEvent.jsx | 22 +- client/src/features/modal/EditEventModal.jsx | 287 ++++++++++++++++++ client/src/features/modal/EventModal.jsx | 23 +- client/src/pages/CalendarPage.jsx | 3 +- client/src/services/dataService.js | 3 + client/src/utilities/calendar.js | 3 +- server/controllers/events.js | 26 ++ server/routes/events.js | 8 + 11 files changed, 379 insertions(+), 48 deletions(-) create mode 100644 client/src/features/form/FormError.jsx create mode 100644 client/src/features/modal/EditEventModal.jsx diff --git a/client/src/contexts/ModalContext/useProvideModal.jsx b/client/src/contexts/ModalContext/useProvideModal.jsx index ff09b9a8..1a67e828 100644 --- a/client/src/contexts/ModalContext/useProvideModal.jsx +++ b/client/src/contexts/ModalContext/useProvideModal.jsx @@ -5,6 +5,7 @@ import { useState } from "react"; const useProvideModal = () => { const [isOpen, setIsOpen] = useState(false); const [activeEvent, setActiveEvent] = useState(null); + const [isEditing, setIsEditing] = useState(false); const handleOpen = () => { setIsOpen(true); @@ -12,6 +13,7 @@ const useProvideModal = () => { const handleClose = () => { setIsOpen(false); + setIsEditing(false); }; const handleToggle = () => { @@ -25,6 +27,8 @@ const useProvideModal = () => { handleToggle, activeEvent, setActiveEvent, + isEditing, + setIsEditing, }; }; diff --git a/client/src/features/form/FormCreateEvent.jsx b/client/src/features/form/FormCreateEvent.jsx index ce0ac0a8..86332e46 100644 --- a/client/src/features/form/FormCreateEvent.jsx +++ b/client/src/features/form/FormCreateEvent.jsx @@ -1,5 +1,6 @@ import { useFormContext } from "../../contexts/FormContext"; import { useAuthContext } from "../../contexts/AuthContext"; +import FormError from "../form/FormError"; export default function FormCreateEvent() { const auth = useAuthContext(); @@ -19,26 +20,7 @@ export default function FormCreateEvent() { return (
{formCreateEventErrors.map((error, index) => { - return ( -
-
- - - - {error} -
-
- ); + return ; })}
diff --git a/client/src/features/form/FormError.jsx b/client/src/features/form/FormError.jsx new file mode 100644 index 00000000..54aca3a7 --- /dev/null +++ b/client/src/features/form/FormError.jsx @@ -0,0 +1,26 @@ +import React from "react"; + +const FormError = ({ error }) => { + return ( +
+
+ + + + {error} +
+
+ ); +}; + +export default FormError; diff --git a/client/src/features/form/FormScheduleEvent.jsx b/client/src/features/form/FormScheduleEvent.jsx index 405103cb..17840531 100644 --- a/client/src/features/form/FormScheduleEvent.jsx +++ b/client/src/features/form/FormScheduleEvent.jsx @@ -1,4 +1,5 @@ import { useFormContext } from "../../contexts/FormContext"; +import FormError from "../form/FormError"; import FormRecurringDates from "./FormRecurringDates"; import { format } from "date-fns"; @@ -25,26 +26,7 @@ export default function FormScheduleEvent() { return (
{formScheduleEventErrors.map((error, index) => { - return ( -
-
- - - - {error} -
-
- ); + return ; })}
diff --git a/client/src/features/modal/EditEventModal.jsx b/client/src/features/modal/EditEventModal.jsx new file mode 100644 index 00000000..ca83eedd --- /dev/null +++ b/client/src/features/modal/EditEventModal.jsx @@ -0,0 +1,287 @@ +import { format } from "date-fns"; +import { useEffect } from "react"; +import { useEventsContext } from "../../contexts/EventsContext"; +import { useFormContext } from "../../contexts/FormContext"; +import { useModalContext } from "../../contexts/ModalContext"; +import dataService from "../../services/dataService"; +import FormError from "../form/FormError"; + +const EditEventModal = () => { + const modal = useModalContext(); + const { setEvents } = useEventsContext(); + const event = modal.activeEvent; + + const { + formData, + setFormData, + formCreateEventErrors, + setFormCreateEventErrors, + formScheduleEventErrors, + setFormScheduleEventErrors, + } = useFormContext(); + + const resetFormData = () => { + setFormData({ + title: "", + description: "", + initialDate: "", + finalDate: "", + location: "", + startTime: "00:00", + endTime: "00:00", + recurring: { + rate: "noRecurr", + days: [], + }, + }); + setFormCreateEventErrors([]); + setFormScheduleEventErrors([]); + }; + + const handleChange = (e) => { + const { name, value } = e.target; + setFormData((prevFormData) => ({ ...prevFormData, [name]: value })); + }; + + const validateInput = () => { + const errors = []; + if (!formData.title) { + errors.push(`Error: Title field can't be empty`); + } + if (formData.description?.length > 280) { + errors.push( + `Error: Description must be less than 280 characters. Current character count: ${formData.description?.length}.` + ); + } + + if (!formData.description) { + errors.push(`Error: description field can't be empty`); + } + + if (!formData.location) { + errors.push(`Error: Location field can't be empty`); + } + + setFormCreateEventErrors(errors); + + return errors.length === 0; + }; + + const scheduleEventErrors = (startAt, endAt) => { + const errors = []; + + if (startAt > endAt) { + errors.push(`Error: Start date must be before end date`); + } + + setFormScheduleEventErrors(errors); + + return errors.length === 0; + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + + if (!validateInput()) return; + + const startAt = new Date( + `${formData.initialDate}T${formData.startTime}` + ).toISOString(); + const endAt = new Date( + `${formData.finalDate}T${formData.endTime}` + ).toISOString(); + + if (!scheduleEventErrors(startAt, endAt)) { + return; + } + + const payload = { + title: formData.title.trim(), + description: formData.description.trim(), + location: formData.location.trim(), + startAt, + endAt, + }; + + const res = await dataService.updateEvent(event._id, payload); + + setEvents((prev) => + prev.map((el) => (el._id === event._id ? res.data.event : el)) + ); + resetFormData(); + modal.handleClose(); + }; + + useEffect(() => { + // Prefilling the values on clicking of edit button + if (event) { + setFormData({ + title: event.title || "", + description: event.description || "", + location: event.location || "", + initialDate: format(new Date(event.startAt), "yyyy-MM-dd") || "", + finalDate: format(new Date(event.endAt), "yyyy-MM-dd") || "", + startTime: format(new Date(event.startAt), "HH:mm") || "00:00", + endTime: format(new Date(event.endAt), "HH:mm") || "00:00", + }); + } + + if (!modal.isOpen) { + resetFormData(); + modal.setActiveEvent(null); + setFormCreateEventErrors([]); + setFormScheduleEventErrors([]); + } + }, [event, modal.isOpen]); + + return ( +
+
+
+ {formCreateEventErrors.map((error, index) => { + return ; + })} + {formScheduleEventErrors.map((error, index) => { + return ; + })} +
+ + {/* TITLE FIELD */} +
+
+ Title +
+
+ +
+
+ + {/* DESCRIPTION FIELD */} +
+
+ Description +
+
+ +
+
+ + {/* LOCATION FIELD */} +
+
+ Location +
+
+ +
+
+ +
+ {/* START DATE FIELD */} +
+
+ Start Date +
+
+ +
+
+ + {/* START TIME FIELD */} +
+
+ Start Time +
+
+ +
+
+ + {/* END DATE FIELD */} +
+
+ End Date +
+
+ +
+
+ + {/* END TIME FIELD */} +
+
+ End Time +
+
+ +
+
+
+ + +
+
+ ); +}; + +export default EditEventModal; diff --git a/client/src/features/modal/EventModal.jsx b/client/src/features/modal/EventModal.jsx index 9e3737f2..323b7313 100644 --- a/client/src/features/modal/EventModal.jsx +++ b/client/src/features/modal/EventModal.jsx @@ -13,21 +13,21 @@ const EventModal = () => { const { setEvents } = useEventsContext(); const modal = useModalContext(); const { user } = useAuthContext(); - const canDelete = + const isAuthorizedUser = // if user is author of event, or user is moderator, they can delete an event (user && user?._id === modal.activeEvent.user?._id) || user?.isModerator; return (
- {canDelete && ( + {isAuthorizedUser && ( )} - {canDelete && modal.activeEvent.groupId && ( + {isAuthorizedUser && modal.activeEvent.groupId && ( )} + {isAuthorizedUser && ( + + )}

{" "} diff --git a/client/src/pages/CalendarPage.jsx b/client/src/pages/CalendarPage.jsx index 9c70ce75..059574d5 100644 --- a/client/src/pages/CalendarPage.jsx +++ b/client/src/pages/CalendarPage.jsx @@ -10,6 +10,7 @@ import EventModal from "../features/modal/EventModal"; import { useModalContext } from "../contexts/ModalContext"; import RejectionModal from "../features/modal/RejectionModal"; import { useRef } from "react"; +import EditEventModal from "../features/modal/EditEventModal"; function CalendarPage() { const auth = useAuthContext(); @@ -43,7 +44,7 @@ function CalendarPage() {
- + {modal.isEditing ? : } {auth?.user ? ( diff --git a/client/src/services/dataService.js b/client/src/services/dataService.js index 3e0b3851..402df81d 100644 --- a/client/src/services/dataService.js +++ b/client/src/services/dataService.js @@ -21,6 +21,9 @@ class DataService { deleteAllEvents(groupId) { return URL.delete(`/events/deleteAllEvents/${groupId}`); } + updateEvent(id, msg) { + return URL.patch(`/events/${id}`, msg); + } getCurrentUser() { return URL.get("/getDisplayName"); } diff --git a/client/src/utilities/calendar.js b/client/src/utilities/calendar.js index d5a93dea..5f82e656 100644 --- a/client/src/utilities/calendar.js +++ b/client/src/utilities/calendar.js @@ -1,10 +1,11 @@ -import { parseISO, format } from "date-fns"; +import { format, parseISO } from "date-fns"; export const getMatchMonthAndYear = (monthToMatch, yearToMatch, events) => { if (!events.length) return []; const allMatchedEvents = events.filter((event) => { const isoDate = parseISO(event.startAt); + if (isNaN(isoDate)) return false; const monthInString = format(isoDate, "LLLL"); // December const year = isoDate.getFullYear(); return monthToMatch === monthInString && year === yearToMatch; diff --git a/server/controllers/events.js b/server/controllers/events.js index bb4d3ad7..e9ac25c7 100644 --- a/server/controllers/events.js +++ b/server/controllers/events.js @@ -55,6 +55,32 @@ module.exports = { res.json(event); }, + updateEvent: async (req, res) => { + const { id } = req.params; + + let event; + + if (!req.user.isModerator) { + // Allow update only if user is the author + event = await Event.findOneAndUpdate( + { _id: id, user: req.user._id }, + req.body, + { new: true } + ); + } else { + // Moderators can edit any event + event = await Event.findOneAndUpdate({ _id: id }, req.body, { + new: true, + }); + } + + if (!event) { + throw httpError(404, "Event not found or unauthorized to edit"); + } + + res.status(200).json({ message: "Event updated!", event }); + }, + deleteEvent: async (req, res) => { const { id } = req.params; diff --git a/server/routes/events.js b/server/routes/events.js index 6dd43d0e..8fa09936 100644 --- a/server/routes/events.js +++ b/server/routes/events.js @@ -26,6 +26,14 @@ router.delete( eventsController.deleteEvent ); +// Since we want to update the event so it best to use PATCH to improve UX by partial update +router.patch( + "/:id", + auth.ensureAuth, + validateObjectId, + eventsController.updateEvent +); + router.delete( "/deleteAllEvents/:groupId", auth.ensureAuth, From 4e0b7b20d3acb608b2a11bf54c31176d70ebde91 Mon Sep 17 00:00:00 2001 From: Pratik Tomar Date: Fri, 24 Oct 2025 01:03:11 +0530 Subject: [PATCH 2/3] Feature/Issue-622 | Feature/Issue -548 | Moderators and Authors can edit event now | Fixed a bug where author was showing as Deleted | Populated the user in PATCH Request --- server/controllers/events.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/controllers/events.js b/server/controllers/events.js index e9ac25c7..0b60ed9e 100644 --- a/server/controllers/events.js +++ b/server/controllers/events.js @@ -66,12 +66,12 @@ module.exports = { { _id: id, user: req.user._id }, req.body, { new: true } - ); + ).populate("user", "displayName"); } else { // Moderators can edit any event event = await Event.findOneAndUpdate({ _id: id }, req.body, { new: true, - }); + }).populate("user", "displayName"); } if (!event) { From a9f1f252479ae1d02d65daa2164c81179a7248eb Mon Sep 17 00:00:00 2001 From: Pratik Tomar Date: Thu, 6 Nov 2025 02:23:44 +0530 Subject: [PATCH 3/3] Feature/Issue-622 | Feature/issue-548 | Mods and Authors can edit event | Fixed linting in Contributing.md --- .github/CONTRIBUTING.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index f4a0038b..da9e7ada 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -78,14 +78,13 @@ Together's progress and milestones are split into separate, distinct issues. You > 💡 Issues tagged with `Good 100Devs First Try` are beginner-level issues that are great for fellow 100Devs. - Currently we have the way to discover issues using Issue Tab. + ### Issues Tab ![Screenshot of Issues tab on GitHub with example issues representing various stages of a project](assets/contributing-issues-tab.jpg) The [Issues tab](https://github.com/Together-100Devs/Together/issues) contains all of the issues that are currently in progress, planned to be worked on, or need further review. - ## Editing code and submitting a pull request Use the following process to make changes after an issue has been assigned to you. @@ -128,8 +127,8 @@ Now that you have a personal fork of the project on GitHub, you will be able to Now that you have the copy, you will need access to the feature branch related to your issue to create a local working branch to write your code. -1. Set upstream to track the remote repository containing the original repo. (Not just your fork.) - +1. Set upstream to track the remote repository containing the original repo. (Not just your fork.) + 2. Use this command to fetch the list of remote branches. `git fetch upstream`