diff --git a/client/src/components/composite/Admin/AdminEventView/AdminAllEvents/AdminAllEvents.tsx b/client/src/components/composite/Admin/AdminEventView/AdminAllEvents/AdminAllEvents.tsx
index 124993ff..0df05b62 100644
--- a/client/src/components/composite/Admin/AdminEventView/AdminAllEvents/AdminAllEvents.tsx
+++ b/client/src/components/composite/Admin/AdminEventView/AdminAllEvents/AdminAllEvents.tsx
@@ -1,6 +1,4 @@
-import EventsCardPreview, {
- IEventsCardPreview
-} from "@/components/generic/Event/EventPreview/EventPreview"
+import EventsCardPreview from "@/components/generic/Event/EventPreview/EventPreview"
import { DateUtils } from "@/components/utils/DateUtils"
import { Event } from "@/models/Events"
import { useCallback, useMemo, useState } from "react"
@@ -127,7 +125,7 @@ const AdminAllEvents = ({
/**
* Detailed view of the event
*/
- const previewCurrentEvents: IEventsCardPreview[] =
+ const previewCurrentEvents =
eventList.upcomingAndCurrentEvents?.map((event) => {
return EventRenderingUtils.previewTransformer(
event,
@@ -137,7 +135,7 @@ const AdminAllEvents = ({
)
}) || []
- const previewPastEvents: IEventsCardPreview[] =
+ const previewPastEvents =
eventList.pastEvents?.map((event) => {
return EventRenderingUtils.previewTransformer(
event,
@@ -164,11 +162,11 @@ const AdminAllEvents = ({
)}
{previewCurrentEvents.map((event) => (
-
+
))}
{previewPastEvents.map((event) => (
-
+
))}
>
)}
diff --git a/client/src/components/composite/Admin/AdminEventView/AdminEventForm/AdminEventForm.tsx b/client/src/components/composite/Admin/AdminEventView/AdminEventForm/AdminEventForm.tsx
index 09338135..b49ef848 100644
--- a/client/src/components/composite/Admin/AdminEventView/AdminEventForm/AdminEventForm.tsx
+++ b/client/src/components/composite/Admin/AdminEventView/AdminEventForm/AdminEventForm.tsx
@@ -26,8 +26,18 @@ interface IAdminEventForm {
/**
* To be called after user submits the new data for the event
+ *
+ * (the big call to action button)
*/
handlePostEvent: (data: CreateEventBody) => void
+
+ /**
+ * Handler which is passed in if {@link isEditMode} is `true`,
+ *
+ * if the user confirms they want the event deleted this handler will be called
+ */
+ handleDeleteEvent?: () => void
+
/**
* If the panel should suggest that the event is being edited, instead of created
*
@@ -42,6 +52,18 @@ interface IAdminEventForm {
defaultData?: CreateEventBody["data"]
}
+const USER_TIMEZONE = Intl.DateTimeFormat().resolvedOptions().timeZone
+
+const TimezoneIndicator = () => (
+ <>
+ {USER_TIMEZONE && (
+
+ The timezone is: {USER_TIMEZONE}
+
+ )}
+ >
+)
+
export const AdminEventFormKeys = {
TITLE: "title",
DESCRIPTION: "description",
@@ -61,7 +83,8 @@ const AdminEventForm = ({
handlePostEvent,
generateImageLink,
isEditMode = false,
- defaultData
+ defaultData,
+ handleDeleteEvent
}: IAdminEventForm) => {
const [isSubmitting, setIsSubmitting] = useState(false)
@@ -102,7 +125,11 @@ const AdminEventForm = ({
Number.parseInt(data.get(AdminEventFormKeys.MAX_OCCUPANCY) as string) ||
undefined,
physical_end_date: physical_end_date
- ? Timestamp.fromDate(new Date(physical_end_date as string))
+ ? Timestamp.fromDate(
+ new Date(
+ (physical_end_date as string).replace(/-/g, "/").replace("T", " ")
+ )
+ )
: undefined
}
@@ -128,6 +155,20 @@ const AdminEventForm = ({
return (
{formTitle}
+ {isEditMode && (
+
+ )}
-
+
Event Dates
+
{
return undefined
},
+ eventPreviousData: {
+ id: "1",
+ title: "CACK",
+ location: "STRAIGHT ZHAO",
+ physical_start_date: earlierStartDate,
+ physical_end_date: earlierStartDate,
+ sign_up_start_date: earlierStartDate,
+ sign_up_end_date: earlierStartDate,
+ google_forms_link: "https://google.com",
+ description: "default data"
+ },
rawEvents: [
{
id: "1",
diff --git a/client/src/components/composite/Admin/AdminEventView/AdminEventView.tsx b/client/src/components/composite/Admin/AdminEventView/AdminEventView.tsx
index 4bbcf948..b4f0bc05 100644
--- a/client/src/components/composite/Admin/AdminEventView/AdminEventView.tsx
+++ b/client/src/components/composite/Admin/AdminEventView/AdminEventView.tsx
@@ -1,8 +1,9 @@
import Button from "@/components/generic/FigmaButtons/FigmaButton"
-import { CreateEventBody, Event } from "@/models/Events"
+import { CreateEventBody, EditEventBody, Event } from "@/models/Events"
import { useState } from "react"
import AdminEventForm from "./AdminEventForm/AdminEventForm"
import AdminAllEvents from "./AdminAllEvents/AdminAllEvents"
+import Loader from "@/components/generic/SuspenseComponent/Loader"
type EventViewModes = "view-all-events" | "creating-new-event" | "editing-event"
@@ -44,6 +45,22 @@ interface IAdminEventView {
* Function to fetch more events.
*/
fetchMoreEvents?: () => void
+
+ /**
+ * If passed in, will open the edit panel for the event with given data
+ */
+ eventPreviousData?: Event
+
+ /**
+ * Will be called when the admin is _editing_ a selected event
+ */
+ handleEditEvent?: (eventId: string, newData: EditEventBody) => void
+
+ /**
+ * Obtains the latest data for an event to edit, if `undefined` is passed
+ * in then it means that no event should be edited
+ */
+ fetchEventToEdit?: (eventId?: string) => void
}
const AdminEventViewContent = ({
@@ -54,16 +71,28 @@ const AdminEventViewContent = ({
rawEvents,
hasMoreEvents,
isLoading,
- fetchMoreEvents
- // TODO: extend with the event id to allow showing an edit view
+ fetchMoreEvents,
+ handleEditEvent,
+ eventPreviousData,
+ fetchEventToEdit
}: {
mode: EventViewModes
setMode: (mode: EventViewModes) => void
} & IAdminEventView) => {
+ /**
+ * Used to make the `PATCH` request for the event (need to specify path with `id`)
+ */
+ const [editedEventId, setEditedEventId] = useState()
+
switch (mode) {
case "view-all-events":
return (
{
+ setEditedEventId(id)
+ fetchEventToEdit?.(id)
+ setMode("editing-event")
+ }}
rawEvents={rawEvents}
hasMoreEvents={hasMoreEvents}
isLoading={isLoading}
@@ -83,7 +112,28 @@ const AdminEventViewContent = ({
/>
)
case "editing-event":
- return null
+ if (!editedEventId) {
+ setMode("view-all-events")
+ return
+ }
+
+ if (!eventPreviousData) {
+ return
+ }
+
+ return (
+ {
+ return await generateImageLink(image)
+ }}
+ defaultData={eventPreviousData}
+ handlePostEvent={async (data) => {
+ await handleEditEvent?.(editedEventId, data.data)
+ setMode("view-all-events")
+ }}
+ isEditMode
+ />
+ )
}
}
@@ -95,6 +145,7 @@ const buttonMessage = (mode: EventViewModes) => {
case "view-all-events":
return "Create Event"
case "creating-new-event":
+ return "Back to Events"
case "editing-event":
return "Back to Events"
}
@@ -110,7 +161,10 @@ const AdminEventView = ({
rawEvents = [],
hasMoreEvents,
isLoading,
- fetchMoreEvents
+ fetchMoreEvents,
+ eventPreviousData,
+ handleEditEvent,
+ fetchEventToEdit
}: IAdminEventView) => {
const [mode, setMode] = useState("view-all-events")
@@ -127,7 +181,10 @@ const AdminEventView = ({
setMode("creating-new-event")
break
case "creating-new-event":
+ setMode("view-all-events")
+ break
case "editing-event":
+ fetchEventToEdit?.()
setMode("view-all-events")
}
}}
@@ -136,14 +193,18 @@ const AdminEventView = ({
+ {/** TODO: pass in delete handler */}
diff --git a/client/src/components/composite/Admin/AdminEventView/WrappedAdminEventView.tsx b/client/src/components/composite/Admin/AdminEventView/WrappedAdminEventView.tsx
index 42997a96..0ac3409d 100644
--- a/client/src/components/composite/Admin/AdminEventView/WrappedAdminEventView.tsx
+++ b/client/src/components/composite/Admin/AdminEventView/WrappedAdminEventView.tsx
@@ -1,10 +1,16 @@
"use client"
-import { useCreateEventMutation } from "@/services/Admin/AdminMutations"
+import {
+ useCreateEventMutation,
+ useEditEventMutation
+} from "@/services/Admin/AdminMutations"
import AdminEventView from "./AdminEventView"
import StorageService from "@/services/Storage/StorageService"
import { useLatestEventsQuery } from "@/services/Event/EventQueries"
-import { useMemo } from "react"
+import { useMemo, useState } from "react"
+import { useGetEventQuery } from "@/services/Admin/AdminQueries"
+import { Event } from "@/models/Events"
+import Loader from "@/components/generic/SuspenseComponent/Loader"
const WrappedAdminEventView = () => {
const { mutateAsync: handleEventCreation } = useCreateEventMutation()
@@ -16,6 +22,15 @@ const WrappedAdminEventView = () => {
fetchNextPage,
isFetchingNextPage
} = useLatestEventsQuery()
+
+ const { mutateAsync: editEvent } = useEditEventMutation()
+
+ const [eventPreviousData, setEventPreviousData] = useState<
+ Event | undefined
+ >()
+
+ const { mutateAsync: fetchEventToBeEdited } = useGetEventQuery()
+
const rawEvents = useMemo(() => {
const flattenedEvents = data?.pages.flatMap((page) => {
return page.data || []
@@ -23,6 +38,10 @@ const WrappedAdminEventView = () => {
return flattenedEvents
}, [data])
+ if (!fetchEventToBeEdited) {
+ return
+ }
+
return (
<>
{
generateImageLink={async (image) =>
await StorageService.uploadEventImage(image)
}
+ fetchEventToEdit={async (id) => {
+ if (id) {
+ setEventPreviousData(await fetchEventToBeEdited(id))
+ } else {
+ /**
+ * If we go back to the main screen we
+ * don't have an event id thats being edited,
+ * so we have to consider the data stale
+ */
+ setEventPreviousData(undefined)
+ }
+ }}
+ handleEditEvent={async (eventId, newData) => {
+ await editEvent({ eventId, newData })
+ }}
+ eventPreviousData={eventPreviousData}
rawEvents={rawEvents || []}
hasMoreEvents={hasNextPage}
isLoading={isPending}
diff --git a/client/src/components/composite/EventsView/EventsView.tsx b/client/src/components/composite/EventsView/EventsView.tsx
index dc8c3ddc..e8755ae6 100644
--- a/client/src/components/composite/EventsView/EventsView.tsx
+++ b/client/src/components/composite/EventsView/EventsView.tsx
@@ -1,6 +1,4 @@
-import EventsCardPreview, {
- IEventsCardPreview
-} from "@/components/generic/Event/EventPreview/EventPreview"
+import EventsCardPreview from "@/components/generic/Event/EventPreview/EventPreview"
import EventDetailed from "@/components/generic/Event/EventDetailed/EventDetailed"
import { DateUtils } from "@/components/utils/DateUtils"
import { Event } from "@/models/Events"
@@ -195,7 +193,7 @@ const EventsPage = ({
)
}, [selectedEventObject, eventSelectionHandler])
- const previewCurrentEvents: IEventsCardPreview[] =
+ const previewCurrentEvents =
eventList.upcomingAndCurrentEvents?.map((event) => {
return EventRenderingUtils.previewTransformer(
event,
@@ -204,7 +202,7 @@ const EventsPage = ({
)
}) || []
- const previewPastEvents: IEventsCardPreview[] =
+ const previewPastEvents =
eventList.pastEvents?.map((event) => {
return EventRenderingUtils.previewTransformer(
event,
@@ -235,11 +233,11 @@ const EventsPage = ({
)}
{previewCurrentEvents.map((event) => (
-
+
))}
{previewPastEvents.map((event) => (
-
+
))}
>
)}
diff --git a/client/src/components/generic/Event/EventUtils.test.ts b/client/src/components/generic/Event/EventUtils.test.ts
index 5b3730e0..b7de6f4f 100644
--- a/client/src/components/generic/Event/EventUtils.test.ts
+++ b/client/src/components/generic/Event/EventUtils.test.ts
@@ -1,33 +1,15 @@
import { EventRenderingUtils } from "./EventUtils"
describe("dateTimeLocalPlaceHolder", () => {
- it("should return a formatted string in ISO 8601 format without milliseconds", () => {
- const date = new Date("2024-11-01T23:34:15.123Z")
+ it("should format the date correctly in ISO 8601 format without milliseconds", () => {
+ const date = new Date(2024, 10, 3, 14, 30) // November 3, 2024, 14:30
const result = EventRenderingUtils.dateTimeLocalPlaceHolder(date)
- expect(result).toBe("2024-11-01T23:34:15")
+ expect(result).toBe("2024-11-03T14:30")
})
- it("should handle dates without milliseconds correctly", () => {
- const date = new Date("2024-11-01T23:34:15Z")
+ it("should pad single digit month, day, hours, and minutes with leading zeros", () => {
+ const date = new Date(2024, 0, 5, 9, 7) // January 5, 2024, 09:07
const result = EventRenderingUtils.dateTimeLocalPlaceHolder(date)
- expect(result).toBe("2024-11-01T23:34:15")
- })
-
- it("should handle different time zones correctly", () => {
- const date = new Date("2024-11-01T23:34:15.123+09:00")
- const result = EventRenderingUtils.dateTimeLocalPlaceHolder(date)
- expect(result).toBe("2024-11-01T14:34:15")
- })
-
- it("should handle leap years correctly", () => {
- const date = new Date("2024-02-29T23:34:15.123Z")
- const result = EventRenderingUtils.dateTimeLocalPlaceHolder(date)
- expect(result).toBe("2024-02-29T23:34:15")
- })
-
- it("should handle dates before 1970 correctly", () => {
- const date = new Date("1969-12-31T23:34:15.123Z")
- const result = EventRenderingUtils.dateTimeLocalPlaceHolder(date)
- expect(result).toBe("1969-12-31T23:34:15")
+ expect(result).toBe("2024-01-05T09:07")
})
})
diff --git a/client/src/components/generic/Event/EventUtils.ts b/client/src/components/generic/Event/EventUtils.ts
index 05ff23a9..46e791f0 100644
--- a/client/src/components/generic/Event/EventUtils.ts
+++ b/client/src/components/generic/Event/EventUtils.ts
@@ -13,6 +13,15 @@ export const IMAGE_PLACEHOLDER_SRC =
* Static methods to format strings related to events
*/
export const EventMessages = {
+ /**
+ * Message to be displayed for deleting an event
+ *
+ * @param title the title of the event
+ * @returns a formatted, user-readable string asking for confirmation
+ */
+ adminDeleteEventConfirmation: (title: string) => {
+ return `Are you sure you want to delete the event ${title}? This can NOT be undone!`
+ },
/**
* Message to be displayed for confirming event creation or editing
*
@@ -85,6 +94,13 @@ export const EventDateComparisons = {
}
} as const
+/**
+ * Utility type to allow for a key to to be associated with a preview
+ *
+ * (generally using the firebase `uid`)
+ */
+export type EventCardPreviewWithKey = IEventsCardPreview & { key: string }
+
export const EventRenderingUtils = {
/**
* Generates a placeholder string for a local date and time input field
@@ -93,9 +109,13 @@ export const EventRenderingUtils = {
* @returns a formatted string in ISO 8601 format without milliseconds
*/
dateTimeLocalPlaceHolder: (date: Date) => {
- const isoString = date.toISOString()
- const placeholderString = isoString.substring(0, isoString.lastIndexOf("."))
- return placeholderString
+ const year = date.getFullYear()
+ const month = String(date.getMonth() + 1).padStart(2, "0")
+ const day = String(date.getDate()).padStart(2, "0")
+ const hours = String(date.getHours()).padStart(2, "0")
+ const minutes = String(date.getMinutes()).padStart(2, "0")
+
+ return `${year}-${month}-${day}T${hours}:${minutes}`
},
/**
* Utility function to convert a raw {@link Event} into {@link IEventsCardPreview}
@@ -109,7 +129,7 @@ export const EventRenderingUtils = {
eventSetter: (id?: string) => void,
buttonText?: string,
variant?: EventCardPreviewVariant
- ): IEventsCardPreview => {
+ ): EventCardPreviewWithKey => {
let eventStartDate
if (event.physical_start_date) {
@@ -131,6 +151,7 @@ export const EventRenderingUtils = {
)
return {
+ key: event.id || event.title,
date: eventStartDate
? EventMessages.eventDateRange(eventStartDate, eventEndDate)
: "",
diff --git a/client/src/models/Events.ts b/client/src/models/Events.ts
index 9e05a70b..508f2dfb 100644
--- a/client/src/models/Events.ts
+++ b/client/src/models/Events.ts
@@ -2,4 +2,6 @@ import { components } from "./__generated__/schema"
export type CreateEventBody = components["schemas"]["CreateEventBody"]
+export type EditEventBody = components["schemas"]["Partial_Event_"]
+
export type Event = components["schemas"]["Event"] & { id?: string } // Assume all responses will return an Id
diff --git a/client/src/services/Admin/AdminMutations.ts b/client/src/services/Admin/AdminMutations.ts
index ff402b5f..6905ac19 100644
--- a/client/src/services/Admin/AdminMutations.ts
+++ b/client/src/services/Admin/AdminMutations.ts
@@ -10,6 +10,7 @@ import {
} from "./AdminQueries"
import { CombinedUserData } from "@/models/User"
import { replaceUserInPage } from "./AdminUtils"
+import { ALL_EVENTS_QUERY_KEY } from "../Event/EventQueries"
export function usePromoteUserMutation() {
return useMutation({
@@ -175,7 +176,24 @@ export function useCreateEventMutation() {
return useMutation({
mutationKey: ["create-booking"],
retry: false,
- mutationFn: AdminService.createEvent
- // TODO: invalidate all events query (we need to refetch the events)
+ mutationFn: AdminService.createEvent,
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: [ALL_EVENTS_QUERY_KEY]
+ })
+ }
+ })
+}
+
+export function useEditEventMutation() {
+ return useMutation({
+ mutationKey: ["edit-event"],
+ retry: false,
+ mutationFn: AdminService.editEvent,
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: [ALL_EVENTS_QUERY_KEY]
+ })
+ }
})
}
diff --git a/client/src/services/Admin/AdminQueries.ts b/client/src/services/Admin/AdminQueries.ts
index 143c8f22..9e84d2c7 100644
--- a/client/src/services/Admin/AdminQueries.ts
+++ b/client/src/services/Admin/AdminQueries.ts
@@ -1,4 +1,4 @@
-import { useInfiniteQuery, useQuery } from "@tanstack/react-query"
+import { useInfiniteQuery, useMutation, useQuery } from "@tanstack/react-query"
import { Timestamp } from "firebase/firestore"
import AdminService from "./AdminService"
@@ -41,3 +41,14 @@ export function useBookingHistoryQuery() {
getNextPageParam: (lastPage) => lastPage.nextCursor
})
}
+
+export function useGetEventQuery() {
+ /**
+ * Need to use a mutation instead of query because
+ * we only want a manual trigger of the fetch
+ */
+ return useMutation({
+ mutationKey: ["single-event"],
+ mutationFn: AdminService.getEvent
+ })
+}
diff --git a/client/src/services/Admin/AdminService.ts b/client/src/services/Admin/AdminService.ts
index aa22c9c0..5f13aefb 100644
--- a/client/src/services/Admin/AdminService.ts
+++ b/client/src/services/Admin/AdminService.ts
@@ -2,7 +2,7 @@ import { Timestamp } from "firebase/firestore"
import { UserAdditionalInfo } from "@/models/User"
import fetchClient from "@/services/OpenApiFetchClient"
import { MEMBER_TABLE_MAX_DATA } from "@/utils/Constants"
-import { CreateEventBody } from "@/models/Events"
+import { CreateEventBody, EditEventBody } from "@/models/Events"
export type EditUsersBody = {
uid: string
@@ -202,6 +202,41 @@ const AdminService = {
if (!response.ok) {
throw new Error(`Failed to create the event ${data.title}`)
}
+ },
+ editEvent: async function ({
+ eventId,
+ newData
+ }: {
+ eventId: string
+ newData: EditEventBody
+ }) {
+ const { response } = await fetchClient.PATCH("/admin/events/{id}", {
+ params: {
+ path: {
+ id: eventId
+ }
+ },
+ body: { ...newData }
+ })
+
+ if (!response.ok) {
+ throw new Error(
+ `Failed to edit the event ${newData.title} with id ${eventId}`
+ )
+ }
+ },
+ getEvent: async function (eventId: string) {
+ const { response, data } = await fetchClient.GET("/admin/events/{id}", {
+ params: {
+ path: { id: eventId }
+ }
+ })
+
+ if (!response.ok) {
+ throw new Error(`Failed to fetch data for event with id ${eventId}`)
+ }
+
+ return data?.data
}
} as const
diff --git a/client/src/services/Event/EventQueries.ts b/client/src/services/Event/EventQueries.ts
index 850d9d1f..41ed22be 100644
--- a/client/src/services/Event/EventQueries.ts
+++ b/client/src/services/Event/EventQueries.ts
@@ -1,13 +1,15 @@
import { useInfiniteQuery } from "@tanstack/react-query"
import EventService from "./EventService"
+export const ALL_EVENTS_QUERY_KEY = "fetch-all-events" as const
+
/**
* A paginated query to fetch events (using a wrapper around the {@link EventService})
*/
export function useLatestEventsQuery() {
return useInfiniteQuery({
queryFn: EventService.getAllEvents,
- queryKey: ["fetch-all-events"],
+ queryKey: [ALL_EVENTS_QUERY_KEY],
staleTime: 0, // always poll
initialPageParam: undefined,
getNextPageParam: (lastPage) => lastPage.nextCursor
diff --git a/server/src/middleware/tests/EventController.test.ts b/server/src/middleware/tests/EventController.test.ts
index 9f8d64cb..e516311e 100644
--- a/server/src/middleware/tests/EventController.test.ts
+++ b/server/src/middleware/tests/EventController.test.ts
@@ -34,7 +34,7 @@ const event3: Event = {
title: "Another Event",
location: "Krispy Kreme",
physical_start_date: laterStartDate,
- sign_up_start_date: earlierStartDate,
+ sign_up_start_date: laterStartDate,
sign_up_end_date: earlierStartDate,
google_forms_link: "https://random.com/event3"
}
diff --git a/server/src/service-layer/controllers/EventController.ts b/server/src/service-layer/controllers/EventController.ts
index e868333e..c3456b6e 100644
--- a/server/src/service-layer/controllers/EventController.ts
+++ b/server/src/service-layer/controllers/EventController.ts
@@ -28,7 +28,7 @@ export class EventController extends Controller {
const currentTime = Timestamp.now()
res.events.forEach((event) => {
- const eventStartTime = event.physical_start_date
+ const eventStartTime = event.sign_up_start_date
const timeDifference =
eventStartTime.toMillis() - currentTime.toMillis()