diff --git a/client/src/components/composite/Admin/AdminEventView/AdminAllEvents/AdminAllEvents.story.tsx b/client/src/components/composite/Admin/AdminEventView/AdminAllEvents/AdminAllEvents.story.tsx new file mode 100644 index 00000000..da4290cb --- /dev/null +++ b/client/src/components/composite/Admin/AdminEventView/AdminAllEvents/AdminAllEvents.story.tsx @@ -0,0 +1,59 @@ +import type { Meta, StoryObj } from "@storybook/react" +import AdminAllEvents from "./AdminAllEvents" +import { Timestamp } from "firebase/firestore" + +const meta: Meta = { + component: AdminAllEvents +} + +export default meta +type Story = StoryObj + +const earlierStartDate = Timestamp.fromDate(new Date(2023, 1, 1)) +const startDate = Timestamp.fromDate(new Date(2024, 1, 1)) + +export const DefaultEventsPage: Story = { + args: { + rawEvents: [ + { + id: "1", + title: "UASC New event 1", + location: "UASC", + physical_start_date: earlierStartDate, + sign_up_start_date: earlierStartDate, + sign_up_end_date: earlierStartDate, + google_forms_link: "https://google.com", + description: + "Lorem ipsum dolor sit amet consectetur adipisicing elit. Reprehenderit adipisci repellat perferendis. Quia ipsum laborum est, veniam accusamus voluptas praesentium, odio perspiciatis blanditiis sequi dignissimos unde. Natus delectus nihil cum." + }, + { + id: "2", + title: "UASC New event 2", + location: "UASC", + physical_start_date: earlierStartDate, + sign_up_start_date: startDate, + sign_up_end_date: earlierStartDate, + description: + "Lorem ipsum dolor sit amet consectetur adipisicing elit. Reprehenderit adipisci repellat perferendis. Quia ipsum laborum est, veniam accusamus voluptas praesentium, odio perspiciatis blanditiis sequi dignissimos unde. Natus delectus nihil cum." + }, + { + id: "3", + title: "UASC New Event 3", + location: "UASC", + physical_start_date: earlierStartDate, + sign_up_start_date: startDate, + sign_up_end_date: earlierStartDate, + description: + "Lorem ipsum dolor sit amet consectetur adipisicing elit. Reprehenderit adipisci repellat perferendis. Quia ipsum laborum est, veniam accusamus voluptas praesentium, odio perspiciatis blanditiis sequi dignissimos unde. Natus delectus nihil cum." + } + ] + }, + tags: ["autodocs"] +} + +export const EmptyEventsPage: Story = { + args: { + rawEvents: [] + }, + tags: ["autodocs"] +} diff --git a/client/src/components/composite/Admin/AdminEventView/AdminAllEvents/AdminAllEvents.tsx b/client/src/components/composite/Admin/AdminEventView/AdminAllEvents/AdminAllEvents.tsx new file mode 100644 index 00000000..124993ff --- /dev/null +++ b/client/src/components/composite/Admin/AdminEventView/AdminAllEvents/AdminAllEvents.tsx @@ -0,0 +1,190 @@ +import EventsCardPreview, { + IEventsCardPreview +} from "@/components/generic/Event/EventPreview/EventPreview" +import { DateUtils } from "@/components/utils/DateUtils" +import { Event } from "@/models/Events" +import { useCallback, useMemo, useState } from "react" +import { + EventDateComparisons, + EventRenderingUtils +} from "@/components/generic/Event/EventUtils" +import Button from "@/components/generic/FigmaButtons/FigmaButton" +import Loader from "@/components/generic/SuspenseComponent/Loader" + +/** + * Interface representing the properties of the Events Page. + */ +interface IAdminAllEvents { + /** + * A list of _all_ {@link Event}s which should either be mocked + * or fetched from the backend. **NO** pre-processing should be + * performed on this list as it will be further mutated in the + * {@link EventsPage} component. + */ + rawEvents?: Event[] + + /** + * Indicates whether the events are currently being loaded. + */ + isLoading?: boolean + + /** + * Indicates whether there are more events to be fetched. + */ + hasMoreEvents?: boolean + + /** + * Function to fetch more events. + */ + fetchMoreEvents?: () => void + + /** + * The ID of the preselected event. + */ + preselectedEventId?: string + + /** + * Callback function to handle changes to the selected event ID. + * @param id - The new selected event ID. + */ + onSelectedEventIdChange?: (id?: string) => void +} + +/** + * Helper type to split the raw events into upcoming and past ones, + * this is important as they need to be sorted differently + */ +interface EventList { + upcomingAndCurrentEvents: Event[] + pastEvents: Event[] +} + +/** + * Used to handle all _presentation_ logic conerning the evnts + * + * - **Do not make any network requests in this component, the data should + * be fetched seperately and passed in as {@link rawEvents}** + * - String operations are ideally done in {@link EventMessages} + * - Complex date comparisons should also be abstracted away into {@link EventDateComparisons} + */ +const AdminAllEvents = ({ + rawEvents = [], + hasMoreEvents, + isLoading, + fetchMoreEvents, + preselectedEventId, + onSelectedEventIdChange +}: IAdminAllEvents) => { + const [selectedEventId, setSelectedEventId] = useState( + preselectedEventId + ) + + const eventSelectionHandler = useCallback( + (id?: string) => { + setSelectedEventId(id) + onSelectedEventIdChange?.(id) + }, + [setSelectedEventId, onSelectedEventIdChange] + ) + + /** + * Partitions of the array that allow us to individually process the ongoing events + */ + const eventList = useMemo(() => { + return rawEvents.reduce( + (buf: EventList, event) => { + const { physical_start_date, physical_end_date } = event + if ( + EventDateComparisons.isPastEvent( + new Date(DateUtils.timestampMilliseconds(physical_start_date)), + physical_end_date && + new Date(DateUtils.timestampMilliseconds(physical_end_date)) + ) + ) { + buf.pastEvents.push(event) + } else { + buf.upcomingAndCurrentEvents.push(event) + } + + /** + * Start dates ascending for upcoming and current events + */ + buf.upcomingAndCurrentEvents.sort( + ( + { physical_start_date: startDate1 }, + { physical_start_date: startDate2 } + ) => + DateUtils.timestampMilliseconds(startDate1) - + DateUtils.timestampMilliseconds(startDate2) + ) + + return buf + }, + { upcomingAndCurrentEvents: [], pastEvents: [] } + ) + }, [rawEvents]) + + /** + * Detailed view of the event + */ + const previewCurrentEvents: IEventsCardPreview[] = + eventList.upcomingAndCurrentEvents?.map((event) => { + return EventRenderingUtils.previewTransformer( + event, + eventSelectionHandler, + "edit event", + "admin" + ) + }) || [] + + const previewPastEvents: IEventsCardPreview[] = + eventList.pastEvents?.map((event) => { + return EventRenderingUtils.previewTransformer( + event, + eventSelectionHandler, + "edit event", + "admin" + ) + }) || [] + + return ( + <> +
+ {selectedEventId ? null : ( + <> + {isLoading ? ( + + ) : ( +
+ {rawEvents.length > 0 ? ( + <>Upcoming Events + ) : ( + <>No events found! + )} +
+ )} + {previewCurrentEvents.map((event) => ( + + ))} + + {previewPastEvents.map((event) => ( + + ))} + + )} + + {hasMoreEvents && !selectedEventId && ( + + )} +
+ + ) +} + +export default AdminAllEvents diff --git a/client/src/components/composite/Admin/AdminEventView/AdminEventView.story.tsx b/client/src/components/composite/Admin/AdminEventView/AdminEventView.story.tsx index 97e7b5e6..c33a5270 100644 --- a/client/src/components/composite/Admin/AdminEventView/AdminEventView.story.tsx +++ b/client/src/components/composite/Admin/AdminEventView/AdminEventView.story.tsx @@ -1,6 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react" import AdminEventView from "./AdminEventView" +import { Timestamp } from "firebase/firestore" const meta: Meta = { component: AdminEventView @@ -9,11 +10,57 @@ const meta: Meta = { export default meta type Story = StoryObj +const earlierStartDate = Timestamp.fromDate(new Date(2023, 1, 1)) +const startDate = Timestamp.fromDate(new Date(2024, 1, 1)) + export const DefaultAdminEventView: Story = { args: { handlePostEvent: () => {}, generateImageLink: async () => { return undefined - } + }, + rawEvents: [] + } +} + +export const AdminEventViewWithEvents: Story = { + args: { + handlePostEvent: () => {}, + generateImageLink: async () => { + return undefined + }, + rawEvents: [ + { + id: "1", + title: "UASC New event 1", + location: "UASC", + physical_start_date: earlierStartDate, + sign_up_start_date: earlierStartDate, + sign_up_end_date: earlierStartDate, + google_forms_link: "https://google.com", + description: + "Lorem ipsum dolor sit amet consectetur adipisicing elit. Reprehenderit adipisci repellat perferendis. Quia ipsum laborum est, veniam accusamus voluptas praesentium, odio perspiciatis blanditiis sequi dignissimos unde. Natus delectus nihil cum." + }, + { + id: "2", + title: "UASC New event 2", + location: "UASC", + physical_start_date: earlierStartDate, + sign_up_start_date: startDate, + sign_up_end_date: earlierStartDate, + description: + "Lorem ipsum dolor sit amet consectetur adipisicing elit. Reprehenderit adipisci repellat perferendis. Quia ipsum laborum est, veniam accusamus voluptas praesentium, odio perspiciatis blanditiis sequi dignissimos unde. Natus delectus nihil cum." + }, + { + id: "3", + title: "UASC New Event 3", + location: "UASC", + physical_start_date: earlierStartDate, + sign_up_start_date: startDate, + sign_up_end_date: earlierStartDate, + description: + "Lorem ipsum dolor sit amet consectetur adipisicing elit. Reprehenderit adipisci repellat perferendis. Quia ipsum laborum est, veniam accusamus voluptas praesentium, odio perspiciatis blanditiis sequi dignissimos unde. Natus delectus nihil cum." + } + ] } } diff --git a/client/src/components/composite/Admin/AdminEventView/AdminEventView.tsx b/client/src/components/composite/Admin/AdminEventView/AdminEventView.tsx index 6b79fbcf..4bbcf948 100644 --- a/client/src/components/composite/Admin/AdminEventView/AdminEventView.tsx +++ b/client/src/components/composite/Admin/AdminEventView/AdminEventView.tsx @@ -1,7 +1,8 @@ import Button from "@/components/generic/FigmaButtons/FigmaButton" -import { CreateEventBody } from "@/models/Events" +import { CreateEventBody, Event } from "@/models/Events" import { useState } from "react" import AdminEventForm from "./AdminEventForm/AdminEventForm" +import AdminAllEvents from "./AdminAllEvents/AdminAllEvents" type EventViewModes = "view-all-events" | "creating-new-event" | "editing-event" @@ -20,13 +21,40 @@ interface IAdminEventView { * @param data - The data of the event to be posted. */ handlePostEvent: (data: CreateEventBody) => void + + /** + * A list of _all_ {@link Event}s which should either be mocked + * or fetched from the backend. **NO** pre-processing should be + * performed on this list as it will be further mutated in the + * {@link AdminEventViewContent} component. + */ + rawEvents?: Event[] + + /** + * Indicates whether the events are currently being loaded. + */ + isLoading?: boolean + + /** + * Indicates whether there are more events to be fetched. + */ + hasMoreEvents?: boolean + + /** + * Function to fetch more events. + */ + fetchMoreEvents?: () => void } const AdminEventViewContent = ({ mode, setMode, handlePostEvent, - generateImageLink + generateImageLink, + rawEvents, + hasMoreEvents, + isLoading, + fetchMoreEvents // TODO: extend with the event id to allow showing an edit view }: { mode: EventViewModes @@ -34,7 +62,14 @@ const AdminEventViewContent = ({ } & IAdminEventView) => { switch (mode) { case "view-all-events": - return null + return ( + + ) case "creating-new-event": return ( { */ const AdminEventView = ({ handlePostEvent, - generateImageLink + generateImageLink, + rawEvents = [], + hasMoreEvents, + isLoading, + fetchMoreEvents }: IAdminEventView) => { const [mode, setMode] = useState("view-all-events") return ( -
+

Events

@@ -102,6 +141,10 @@ const AdminEventView = ({ mode={mode} handlePostEvent={handlePostEvent} generateImageLink={generateImageLink} + rawEvents={rawEvents} + hasMoreEvents={hasMoreEvents} + isLoading={isLoading} + fetchMoreEvents={fetchMoreEvents} />
) diff --git a/client/src/components/composite/Admin/AdminEventView/WrappedAdminEventView.tsx b/client/src/components/composite/Admin/AdminEventView/WrappedAdminEventView.tsx index 2f207634..42997a96 100644 --- a/client/src/components/composite/Admin/AdminEventView/WrappedAdminEventView.tsx +++ b/client/src/components/composite/Admin/AdminEventView/WrappedAdminEventView.tsx @@ -3,9 +3,26 @@ import { useCreateEventMutation } from "@/services/Admin/AdminMutations" import AdminEventView from "./AdminEventView" import StorageService from "@/services/Storage/StorageService" +import { useLatestEventsQuery } from "@/services/Event/EventQueries" +import { useMemo } from "react" const WrappedAdminEventView = () => { const { mutateAsync: handleEventCreation } = useCreateEventMutation() + const { + data, + isPending, + hasNextPage, + isFetching, + fetchNextPage, + isFetchingNextPage + } = useLatestEventsQuery() + const rawEvents = useMemo(() => { + const flattenedEvents = data?.pages.flatMap((page) => { + return page.data || [] + }) + return flattenedEvents + }, [data]) + return ( <> { generateImageLink={async (image) => await StorageService.uploadEventImage(image) } + rawEvents={rawEvents || []} + hasMoreEvents={hasNextPage} + isLoading={isPending} + fetchMoreEvents={() => { + if (!isFetchingNextPage && !isFetching) { + fetchNextPage() + } + }} /> ) diff --git a/client/src/components/composite/EventsView/EventsView.tsx b/client/src/components/composite/EventsView/EventsView.tsx index b2e1cc90..dc8c3ddc 100644 --- a/client/src/components/composite/EventsView/EventsView.tsx +++ b/client/src/components/composite/EventsView/EventsView.tsx @@ -199,7 +199,8 @@ const EventsPage = ({ eventList.upcomingAndCurrentEvents?.map((event) => { return EventRenderingUtils.previewTransformer( event, - eventSelectionHandler + eventSelectionHandler, + "view more" ) }) || [] @@ -207,7 +208,8 @@ const EventsPage = ({ eventList.pastEvents?.map((event) => { return EventRenderingUtils.previewTransformer( event, - eventSelectionHandler + eventSelectionHandler, + "view more" ) }) || [] diff --git a/client/src/components/generic/Event/EventPreview/EventPreview.tsx b/client/src/components/generic/Event/EventPreview/EventPreview.tsx index 4e34d731..9c8762dd 100644 --- a/client/src/components/generic/Event/EventPreview/EventPreview.tsx +++ b/client/src/components/generic/Event/EventPreview/EventPreview.tsx @@ -1,7 +1,7 @@ // 4 props: 3 string, 1 image import Image from "next/image" import Arrow from "@/assets/icons/rightarrow.svg" - +export type EventCardPreviewVariant = "regular" | "admin" /** * The interface (props) associated with {@link EventsCardPreview} */ @@ -35,19 +35,28 @@ export interface IEventsCardPreview { * Headline of the preview - generally is the title of the event */ title: string + /** + * The text to display on the view button + */ + viewButtonText?: string + /** + * The variant of the card to render + */ + variant?: EventCardPreviewVariant } type ViewButtonProps = { onClick: () => void + viewButtonText?: string } -const ViewButton = ({ onClick }: ViewButtonProps) => { +const ViewButton = ({ onClick, viewButtonText }: ViewButtonProps) => { return ( @@ -67,13 +76,15 @@ const EventsCardPreview = ({ onClick, image = "", signUpOpenDate, - isPastEvent + isPastEvent, + viewButtonText = "view more", + variant = "regular" }: IEventsCardPreview) => { return (
{location}

- +
) diff --git a/client/src/components/generic/Event/EventUtils.ts b/client/src/components/generic/Event/EventUtils.ts index 094812da..9af98ec3 100644 --- a/client/src/components/generic/Event/EventUtils.ts +++ b/client/src/components/generic/Event/EventUtils.ts @@ -1,7 +1,10 @@ import { DateUtils } from "@/components/utils/DateUtils" import { Event } from "@/models/Events" import { MS_IN_SECOND } from "@/utils/Constants" -import { IEventsCardPreview } from "./EventPreview/EventPreview" +import { + EventCardPreviewVariant, + IEventsCardPreview +} from "./EventPreview/EventPreview" export const IMAGE_PLACEHOLDER_SRC = "https://placehold.co/600x400?text=UASC+Event" as const @@ -78,7 +81,9 @@ export const EventRenderingUtils = { */ previewTransformer: ( event: Event, - eventSetter: (id?: string) => void + eventSetter: (id?: string) => void, + buttonText?: string, + variant?: EventCardPreviewVariant ): IEventsCardPreview => { let eventStartDate @@ -114,7 +119,9 @@ export const EventRenderingUtils = { ), onClick: () => { eventSetter(event.id) - } + }, + viewButtonText: buttonText, + variant } } }