Skip to content

Commit

Permalink
Page layout for accessing an event's program
Browse files Browse the repository at this point in the history
  • Loading branch information
beverloo committed Jan 6, 2024
1 parent 8788008 commit 3c0959a
Show file tree
Hide file tree
Showing 11 changed files with 292 additions and 0 deletions.
14 changes: 14 additions & 0 deletions app/admin/events/[slug]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -124,6 +126,17 @@ export default async function EventLayout(props: React.PropsWithChildren<EventLa
// this is not yet supported by `ts-sql-query`. This'll do in the mean time.
info.teams.sort((lhs, rhs) => 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: <EventNoteIcon />,
label: 'Program',
url: `/admin/events/${slug}/program/requests`,
});
}

const volunteersMenu: AdminSidebarMenuEntry[] = [
{
icon: <GridViewIcon />,
Expand All @@ -136,6 +149,7 @@ export default async function EventLayout(props: React.PropsWithChildren<EventLa
privilege: Privilege.EventHotelManagement,
url: `/admin/events/${slug}/hotels`,
},
...programEntry,
{
icon: <MonetizationOnIcon />,
label: 'Refunds',
Expand Down
85 changes: 85 additions & 0 deletions app/admin/events/[slug]/program/ProgramNavigation.tsx
Original file line number Diff line number Diff line change
@@ -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 <ProgramNavigation> component.
*/
export interface ProgramNavigationProps {
/**
* Slug of the event for which the navigation is being shown.
*/
slug: string;
}

/**
* The <ProgramNavigation> 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: <MoveToInboxIcon />,
url: `/admin/events/${props.slug}/program/requests`
},
{
label: 'Activities',
icon: <EventNoteIcon />,
url: `/admin/events/${props.slug}/program/activities`
},
{
label: 'Locations',
icon: <LocationOnIcon />,
url: `/admin/events/${props.slug}/program/locations`
},
{
label: 'Areas',
icon: <MapIcon />,
url: `/admin/events/${props.slug}/program/areas`
},
]), [ props.slug ]);

const [ selectedTabIndex, setSelectedTabIndex ] =
useState<number | undefined>(/* 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 (
<Tabs onChange={handleChange} value={selectedTabIndex} variant="fullWidth">
{ navigationOptions.map(({ label, icon }, index) =>
<Tab key={index} icon={icon} iconPosition="start" label={label} /> )}
</Tabs>
);
}
21 changes: 21 additions & 0 deletions app/admin/events/[slug]/program/activities/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -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 <ProgramLayout> 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 (
<Typography variant="body2">
ProgramActivityPage ({props.params.slug}, {props.params.id})
</Typography>
);
}
21 changes: 21 additions & 0 deletions app/admin/events/[slug]/program/activities/page.tsx
Original file line number Diff line number Diff line change
@@ -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 <ProgramActivitiesPage> 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 (
<Typography variant="body2">
ProgramActivitiesPage ({props.params.slug})
</Typography>
);
}
21 changes: 21 additions & 0 deletions app/admin/events/[slug]/program/areas/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -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 <ProgramAreaPage> 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 (
<Typography variant="body2">
ProgramAreaPage ({props.params.slug}, {props.params.id})
</Typography>
);
}
21 changes: 21 additions & 0 deletions app/admin/events/[slug]/program/areas/page.tsx
Original file line number Diff line number Diff line change
@@ -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 <ProgramAreasPage> 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 (
<Typography variant="body2">
ProgramAreasPage ({props.params.slug})
</Typography>
);
}
32 changes: 32 additions & 0 deletions app/admin/events/[slug]/program/layout.tsx
Original file line number Diff line number Diff line change
@@ -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 <ProgramLayout> 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<NextRouterParams<'slug'>>)
{
const { event } = await verifyAccessAndFetchPageInfo(props.params);
if (!event.festivalId)
notFound(); // events must be associated with a `festivalId` in order to have a program

return (
<>
<Paper>
<ProgramNavigation slug={event.slug} />
{props.children}
</Paper>
{ /* TODO: Changelog */ }
</>
);
}
21 changes: 21 additions & 0 deletions app/admin/events/[slug]/program/locations/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -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 <ProgramLocationPage> 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 (
<Typography variant="body2">
ProgramLocationPage ({props.params.slug}, {props.params.id})
</Typography>
);
}
21 changes: 21 additions & 0 deletions app/admin/events/[slug]/program/locations/page.tsx
Original file line number Diff line number Diff line change
@@ -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 <ProgramLocationsPage> 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 (
<Typography variant="body2">
ProgramLocationsPage ({props.params.slug})
</Typography>
);
}
14 changes: 14 additions & 0 deletions app/admin/events/[slug]/program/page.tsx
Original file line number Diff line number Diff line change
@@ -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 <ProgramPage> 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();
}
21 changes: 21 additions & 0 deletions app/admin/events/[slug]/program/requests/page.tsx
Original file line number Diff line number Diff line change
@@ -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 <ProgramRequestsPage> 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 (
<Typography variant="body2">
ProgramRequestsPage ({props.params.slug})
</Typography>
);
}

0 comments on commit 3c0959a

Please sign in to comment.