diff --git a/app/admin/events/[slug]/layout.tsx b/app/admin/events/[slug]/layout.tsx index f4ef1564..afb64182 100644 --- a/app/admin/events/[slug]/layout.tsx +++ b/app/admin/events/[slug]/layout.tsx @@ -3,6 +3,7 @@ import { notFound } from 'next/navigation'; +import EventNoteIcon from '@mui/icons-material/EventNote'; import FeedOutlinedIcon from '@mui/icons-material/FeedOutlined'; import GridViewIcon from '@mui/icons-material/GridView'; import HistoryEduIcon from '@mui/icons-material/HistoryEdu'; @@ -66,6 +67,7 @@ async function fetchEventSidebarInformation(user: User, eventSlug: string) { event: { name: tEvents.eventShortName, slug: tEvents.eventSlug, + festivalId: tEvents.eventFestivalId, }, teams: dbInstance.aggregateAsArray({ id: teamsJoin.teamId, @@ -124,6 +126,17 @@ export default async function EventLayout(props: React.PropsWithChildren lhs.name!.localeCompare(rhs.name!)); + // Only display the "Program" entry when an event has been associated with a Festival ID. This + // is how AnPlan maps the events, and we rely on the key to import information. + const programEntry: AdminSidebarMenuEntry[] = [ /* empty */ ]; + if (!!info.event.festivalId) { + programEntry.push({ + icon: , + label: 'Program', + url: `/admin/events/${slug}/program/requests`, + }); + } + const volunteersMenu: AdminSidebarMenuEntry[] = [ { icon: , @@ -136,6 +149,7 @@ export default async function EventLayout(props: React.PropsWithChildren, label: 'Refunds', diff --git a/app/admin/events/[slug]/program/ProgramNavigation.tsx b/app/admin/events/[slug]/program/ProgramNavigation.tsx new file mode 100644 index 00000000..f76c3fec --- /dev/null +++ b/app/admin/events/[slug]/program/ProgramNavigation.tsx @@ -0,0 +1,85 @@ +// Copyright 2023 Peter Beverloo & AnimeCon. All rights reserved. +// Use of this source code is governed by a MIT license that can be found in the LICENSE file. + +'use client'; + +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { usePathname, useRouter } from 'next/navigation'; + +import EventNoteIcon from '@mui/icons-material/EventNote'; +import LocationOnIcon from '@mui/icons-material/LocationOn'; +import MapIcon from '@mui/icons-material/Map'; +import MoveToInboxIcon from '@mui/icons-material/MoveToInbox'; +import Tabs from '@mui/material/Tabs'; +import Tab from '@mui/material/Tab'; + +/** + * Props accepted by the component. + */ +export interface ProgramNavigationProps { + /** + * Slug of the event for which the navigation is being shown. + */ + slug: string; +} + +/** + * The component displays a tab-like bar at the top of the Program page, which + * enables the volunteer to navigate between different sections of the program. + */ +export function ProgramNavigation(props: ProgramNavigationProps) { + const pathname = usePathname(); + const router = useRouter(); + + const navigationOptions = useMemo(() => ([ + { + label: 'Requests', + icon: , + url: `/admin/events/${props.slug}/program/requests` + }, + { + label: 'Activities', + icon: , + url: `/admin/events/${props.slug}/program/activities` + }, + { + label: 'Locations', + icon: , + url: `/admin/events/${props.slug}/program/locations` + }, + { + label: 'Areas', + icon: , + url: `/admin/events/${props.slug}/program/areas` + }, + ]), [ props.slug ]); + + const [ selectedTabIndex, setSelectedTabIndex ] = + useState(/* Requests= */ 3); + + useEffect(() => { + for (let index = 0; index < navigationOptions.length; ++index) { + if (!pathname.startsWith(navigationOptions[index].url)) + continue; + + setSelectedTabIndex(index); + break; + } + }, [ navigationOptions, pathname, props.slug ]); + + const handleChange = useCallback((event: unknown, index: number) => { + if (index < 0 || index > navigationOptions.length) + return; + + // Note that the `useEffect` above will take care of updating the tab bar. + router.push(navigationOptions[index].url); + + }, [ navigationOptions, router ]); + + return ( + + { navigationOptions.map(({ label, icon }, index) => + )} + + ); +} diff --git a/app/admin/events/[slug]/program/activities/[id]/page.tsx b/app/admin/events/[slug]/program/activities/[id]/page.tsx new file mode 100644 index 00000000..e12c2bca --- /dev/null +++ b/app/admin/events/[slug]/program/activities/[id]/page.tsx @@ -0,0 +1,21 @@ +// Copyright 2023 Peter Beverloo & AnimeCon. All rights reserved. +// Use of this source code is governed by a MIT license that can be found in the LICENSE file. + +import Typography from '@mui/material/Typography'; + +import type { NextRouterParams } from '@lib/NextRouterParams'; +import { verifyAccessAndFetchPageInfo } from '@app/admin/events/verifyAccessAndFetchPageInfo'; + +/** + * The component contains the common elements between the different pages that make + * up the Program section of the Volunteer Manager. A program is bound to an event. + */ +export default async function ProgramActivityPage(props: NextRouterParams<'slug' | 'id'>) { + const { event } = await verifyAccessAndFetchPageInfo(props.params); + + return ( + + ProgramActivityPage ({props.params.slug}, {props.params.id}) + + ); +} diff --git a/app/admin/events/[slug]/program/activities/page.tsx b/app/admin/events/[slug]/program/activities/page.tsx new file mode 100644 index 00000000..812905e0 --- /dev/null +++ b/app/admin/events/[slug]/program/activities/page.tsx @@ -0,0 +1,21 @@ +// Copyright 2023 Peter Beverloo & AnimeCon. All rights reserved. +// Use of this source code is governed by a MIT license that can be found in the LICENSE file. + +import Typography from '@mui/material/Typography'; + +import type { NextRouterParams } from '@lib/NextRouterParams'; +import { verifyAccessAndFetchPageInfo } from '@app/admin/events/verifyAccessAndFetchPageInfo'; + +/** + * The component contains the activities that are part of the program of a + * particular event. Each activity can link through to a detail page. + */ +export default async function ProgramActivitiesPage(props: NextRouterParams<'slug'>) { + const { event } = await verifyAccessAndFetchPageInfo(props.params); + + return ( + + ProgramActivitiesPage ({props.params.slug}) + + ); +} diff --git a/app/admin/events/[slug]/program/areas/[id]/page.tsx b/app/admin/events/[slug]/program/areas/[id]/page.tsx new file mode 100644 index 00000000..a0ed3870 --- /dev/null +++ b/app/admin/events/[slug]/program/areas/[id]/page.tsx @@ -0,0 +1,21 @@ +// Copyright 2023 Peter Beverloo & AnimeCon. All rights reserved. +// Use of this source code is governed by a MIT license that can be found in the LICENSE file. + +import Typography from '@mui/material/Typography'; + +import type { NextRouterParams } from '@lib/NextRouterParams'; +import { verifyAccessAndFetchPageInfo } from '@app/admin/events/verifyAccessAndFetchPageInfo'; + +/** + * The component contains the information about an individual area that's part of + * the festival's program. Limited modifications can be made by the volunteering leads. + */ +export default async function ProgramAreaPage(props: NextRouterParams<'slug' | 'id'>) { + const { event } = await verifyAccessAndFetchPageInfo(props.params); + + return ( + + ProgramAreaPage ({props.params.slug}, {props.params.id}) + + ); +} diff --git a/app/admin/events/[slug]/program/areas/page.tsx b/app/admin/events/[slug]/program/areas/page.tsx new file mode 100644 index 00000000..ce2610bf --- /dev/null +++ b/app/admin/events/[slug]/program/areas/page.tsx @@ -0,0 +1,21 @@ +// Copyright 2023 Peter Beverloo & AnimeCon. All rights reserved. +// Use of this source code is governed by a MIT license that can be found in the LICENSE file. + +import Typography from '@mui/material/Typography'; + +import type { NextRouterParams } from '@lib/NextRouterParams'; +import { verifyAccessAndFetchPageInfo } from '@app/admin/events/verifyAccessAndFetchPageInfo'; + +/** + * The component contains the areas that are part of the program of a particular + * event, or rather, its location. Each area links through to a detailed page. + */ +export default async function ProgramAreasPage(props: NextRouterParams<'slug'>) { + const { event } = await verifyAccessAndFetchPageInfo(props.params); + + return ( + + ProgramAreasPage ({props.params.slug}) + + ); +} diff --git a/app/admin/events/[slug]/program/layout.tsx b/app/admin/events/[slug]/program/layout.tsx new file mode 100644 index 00000000..8b5cbce5 --- /dev/null +++ b/app/admin/events/[slug]/program/layout.tsx @@ -0,0 +1,32 @@ +// Copyright 2023 Peter Beverloo & AnimeCon. All rights reserved. +// Use of this source code is governed by a MIT license that can be found in the LICENSE file. + +import { notFound } from 'next/navigation'; + +import Paper from '@mui/material/Paper'; + +import type { NextRouterParams } from '@lib/NextRouterParams'; +import { ProgramNavigation } from './ProgramNavigation'; +import { verifyAccessAndFetchPageInfo } from '@app/admin/events/verifyAccessAndFetchPageInfo'; + +/** + * The component contains the common elements between the different pages that make + * up the Program section of the Volunteer Manager. A program is bound to an event. + */ +export default async function ProgramLayout( + props: React.PropsWithChildren>) +{ + const { event } = await verifyAccessAndFetchPageInfo(props.params); + if (!event.festivalId) + notFound(); // events must be associated with a `festivalId` in order to have a program + + return ( + <> + + + {props.children} + + { /* TODO: Changelog */ } + + ); +} diff --git a/app/admin/events/[slug]/program/locations/[id]/page.tsx b/app/admin/events/[slug]/program/locations/[id]/page.tsx new file mode 100644 index 00000000..0d7c09b2 --- /dev/null +++ b/app/admin/events/[slug]/program/locations/[id]/page.tsx @@ -0,0 +1,21 @@ +// Copyright 2023 Peter Beverloo & AnimeCon. All rights reserved. +// Use of this source code is governed by a MIT license that can be found in the LICENSE file. + +import Typography from '@mui/material/Typography'; + +import type { NextRouterParams } from '@lib/NextRouterParams'; +import { verifyAccessAndFetchPageInfo } from '@app/admin/events/verifyAccessAndFetchPageInfo'; + +/** + * The component contains information about a location that's part of the + * festival's venue. Limited modifications can be made by volunteering leads. + */ +export default async function ProgramLocationPage(props: NextRouterParams<'slug' | 'id'>) { + const { event } = await verifyAccessAndFetchPageInfo(props.params); + + return ( + + ProgramLocationPage ({props.params.slug}, {props.params.id}) + + ); +} diff --git a/app/admin/events/[slug]/program/locations/page.tsx b/app/admin/events/[slug]/program/locations/page.tsx new file mode 100644 index 00000000..2238afc5 --- /dev/null +++ b/app/admin/events/[slug]/program/locations/page.tsx @@ -0,0 +1,21 @@ +// Copyright 2023 Peter Beverloo & AnimeCon. All rights reserved. +// Use of this source code is governed by a MIT license that can be found in the LICENSE file. + +import Typography from '@mui/material/Typography'; + +import type { NextRouterParams } from '@lib/NextRouterParams'; +import { verifyAccessAndFetchPageInfo } from '@app/admin/events/verifyAccessAndFetchPageInfo'; + +/** + * The component contains the locations that are part of the program of a + * particular event, or rather, its venue. Each location links through to a more detailed page. + */ +export default async function ProgramLocationsPage(props: NextRouterParams<'slug'>) { + const { event } = await verifyAccessAndFetchPageInfo(props.params); + + return ( + + ProgramLocationsPage ({props.params.slug}) + + ); +} diff --git a/app/admin/events/[slug]/program/page.tsx b/app/admin/events/[slug]/program/page.tsx new file mode 100644 index 00000000..e5ba7c51 --- /dev/null +++ b/app/admin/events/[slug]/program/page.tsx @@ -0,0 +1,14 @@ +// Copyright 2023 Peter Beverloo & AnimeCon. All rights reserved. +// Use of this source code is governed by a MIT license that can be found in the LICENSE file. + +import { notFound } from 'next/navigation'; + +import type { NextRouterParams } from '@lib/NextRouterParams'; + +/** + * The component is the main page of a festival's program. Because there is nothing to + * show here per se, we display an error page instead. + */ +export default async function ProgramPage(props: NextRouterParams<'slug'>) { + notFound(); +} diff --git a/app/admin/events/[slug]/program/requests/page.tsx b/app/admin/events/[slug]/program/requests/page.tsx new file mode 100644 index 00000000..4d7807cf --- /dev/null +++ b/app/admin/events/[slug]/program/requests/page.tsx @@ -0,0 +1,21 @@ +// Copyright 2023 Peter Beverloo & AnimeCon. All rights reserved. +// Use of this source code is governed by a MIT license that can be found in the LICENSE file. + +import Typography from '@mui/material/Typography'; + +import type { NextRouterParams } from '@lib/NextRouterParams'; +import { verifyAccessAndFetchPageInfo } from '@app/admin/events/verifyAccessAndFetchPageInfo'; + +/** + * The component lists the program entries where the organiser has requested + * help from the volunteering teams. Requests must be managed directly by our team. + */ +export default async function ProgramRequestsPage(props: NextRouterParams<'slug'>) { + const { event } = await verifyAccessAndFetchPageInfo(props.params); + + return ( + + ProgramRequestsPage ({props.params.slug}) + + ); +}