add filter on venue

This commit is contained in:
2024-07-10 18:10:44 +02:00
parent f77a4ee943
commit 6dc5c26165
6 changed files with 84 additions and 4 deletions

View File

@ -8,12 +8,14 @@ import {
} from "@/lib/event"; } from "@/lib/event";
import { PageHeader } from "@/components/general/PageHeader"; import { PageHeader } from "@/components/general/PageHeader";
import { Suspense } from "react"; import { Suspense } from "react";
import { VenueFragment } from "@/gql/graphql";
export default async function Page() { export default async function Page() {
const { data, error } = await getClient().query(futureEventsQuery, {}); const { data, error } = await getClient().query(futureEventsQuery, {});
const events = (data?.events?.futureEvents ?? []) as EventFragment[]; const events = (data?.events?.futureEvents ?? []) as EventFragment[];
const eventCategories = (data?.eventCategories ?? []) as EventCategory[]; const eventCategories = (data?.eventCategories ?? []) as EventCategory[];
const eventOrganizers = (data?.eventOrganizers ?? []) as EventOrganizer[]; const eventOrganizers = (data?.eventOrganizers ?? []) as EventOrganizer[];
const venues = (data?.venues ?? []) as VenueFragment[];
return ( return (
<main className="site-main" id="main"> <main className="site-main" id="main">
@ -23,6 +25,7 @@ export default async function Page() {
events={events} events={events}
eventCategories={eventCategories} eventCategories={eventCategories}
eventOrganizers={eventOrganizers} eventOrganizers={eventOrganizers}
venues={venues}
/> />
</Suspense> </Suspense>
</main> </main>

View File

@ -18,6 +18,7 @@ import { parse } from "date-fns";
import { unique } from "@/lib/common"; import { unique } from "@/lib/common";
import Icon from "../general/Icon"; import Icon from "../general/Icon";
import { useState } from "react"; import { useState } from "react";
import { VenueFragment } from "@/gql/graphql";
/* /*
TODO: canonical / alternate URLs https://github.com/47ng/nuqs?tab=readme-ov-file#seo TODO: canonical / alternate URLs https://github.com/47ng/nuqs?tab=readme-ov-file#seo
@ -27,10 +28,12 @@ export const EventContainer = ({
events, events,
eventCategories, eventCategories,
eventOrganizers, eventOrganizers,
venues,
}: { }: {
events: EventFragment[]; events: EventFragment[];
eventCategories: EventCategory[]; eventCategories: EventCategory[];
eventOrganizers: EventOrganizer[]; eventOrganizers: EventOrganizer[];
venues: VenueFragment[];
}) => { }) => {
const [mode, setMode] = useQueryState( const [mode, setMode] = useQueryState(
"mode", "mode",
@ -38,20 +41,46 @@ export const EventContainer = ({
); );
const [category, setCategory] = useQueryState("category", parseAsString); const [category, setCategory] = useQueryState("category", parseAsString);
const [organizer, setOrganizer] = useQueryState("organizer", parseAsString); const [organizer, setOrganizer] = useQueryState("organizer", parseAsString);
const [venue, setVenue] = useQueryState("venue", parseAsString);
/* Allow filtering on all categories that are configured to be shown */
const filterableCategories = eventCategories.filter((x) => x.showInFilters); const filterableCategories = eventCategories.filter((x) => x.showInFilters);
/*
Allow filtering on all organizers that have upcoming events
Filtering on an organizer with no upcoming events will work, but be hidden from dropdown
*/
const uniqueOrganizers: string[] = unique( const uniqueOrganizers: string[] = unique(
events events
.map((x) => x.organizers) .map((x) => x.organizers)
.flat() .flat()
.filter((x) => x.__typename === "EventOrganizer") .filter((x) => x.__typename === "EventOrganizer")
.map((x) => x.slug) .map((x) => x.slug)
.filter((x) => typeof x === "string")
); );
const filterableOrganizers = uniqueOrganizers const filterableOrganizers = uniqueOrganizers
.map((slug) => eventOrganizers.find((haystack) => haystack.slug === slug)) .map((slug) => eventOrganizers.find((haystack) => haystack.slug === slug))
.filter((x) => x !== undefined) as EventOrganizer[]; .filter((x) => x !== undefined) as EventOrganizer[];
/*
Allow filtering on all venues that have upcoming events
Filtering on a venue with no upcoming events will work,
and in that case it's included in the dropdown
*/
const venueSlugsWithUpcomingEvents = events
.map((x) => x.occurrences)
.flat()
.filter((x) => x.venue?.__typename === "VenuePage")
.map((x) => x.venue?.slug)
.filter((x) => typeof x === "string");
const filterableVenues = venues
.map((x) =>
venues.find(
(haystack) => haystack.slug === x.slug || haystack.slug === venue
)
)
.filter((x) => x !== undefined) as VenueFragment[];
const filteredEvents = events const filteredEvents = events
.filter( .filter(
(x) => (x) =>
@ -64,6 +93,14 @@ export const EventContainer = ({
x.categories x.categories
.map((eventCategory) => eventCategory.slug) .map((eventCategory) => eventCategory.slug)
.includes(category) .includes(category)
)
.filter(
(x) =>
!venue ||
x.occurrences
.map((occurrence) => occurrence.venue?.slug)
.filter((x) => typeof x === "string")
.includes(venue)
); );
const [showFilter, setShowFilter] = useState(false); const [showFilter, setShowFilter] = useState(false);
@ -98,6 +135,9 @@ export const EventContainer = ({
eventOrganizers={filterableOrganizers} eventOrganizers={filterableOrganizers}
setOrganizer={setOrganizer} setOrganizer={setOrganizer}
activeOrganizer={organizer} activeOrganizer={organizer}
venues={filterableVenues}
setVenue={setVenue}
activeVenue={venue}
isVisible={showFilter} isVisible={showFilter}
/> />
{mode === "list" && <EventList events={filteredEvents} />} {mode === "list" && <EventList events={filteredEvents} />}

View File

@ -1,6 +1,7 @@
import { EventCategory, EventOrganizer } from "@/lib/event"; import { EventCategory, EventOrganizer } from "@/lib/event";
import styles from "./eventFilter.module.scss"; import styles from "./eventFilter.module.scss";
import { VenueFragment } from "@/gql/graphql";
export const EventFilter = ({ export const EventFilter = ({
eventCategories, eventCategories,
@ -9,6 +10,9 @@ export const EventFilter = ({
eventOrganizers, eventOrganizers,
setOrganizer, setOrganizer,
activeOrganizer, activeOrganizer,
venues,
activeVenue,
setVenue,
isVisible, isVisible,
}: { }: {
eventCategories: EventCategory[]; eventCategories: EventCategory[];
@ -17,12 +21,19 @@ export const EventFilter = ({
eventOrganizers: EventOrganizer[]; eventOrganizers: EventOrganizer[];
setOrganizer: (slug: string | null) => void; setOrganizer: (slug: string | null) => void;
activeOrganizer: string | null; activeOrganizer: string | null;
venues: VenueFragment[];
activeVenue: string | null;
setVenue: (slug: string | null) => void;
isVisible: boolean; isVisible: boolean;
}) => { }) => {
const onOrganizerSelect = (e: React.ChangeEvent<HTMLSelectElement>) => { const onOrganizerSelect = (e: React.ChangeEvent<HTMLSelectElement>) => {
const organizer = e.target.value; const organizer = e.target.value;
setOrganizer(organizer === "" ? null : organizer); setOrganizer(organizer === "" ? null : organizer);
}; };
const onVenueSelect = (e: React.ChangeEvent<HTMLSelectElement>) => {
const venue = e.target.value;
setVenue(venue === "" ? null : venue);
};
return ( return (
<div className={styles.eventFilter} data-show={isVisible}> <div className={styles.eventFilter} data-show={isVisible}>
@ -52,6 +63,25 @@ export const EventFilter = ({
))} ))}
</ul> </ul>
</div> </div>
<div className={styles.filterItem}>
<label htmlFor="venue" className={`suphead ${styles.heading}`}>
Lokale
</label>
<div>
<select
name="venue"
value={activeVenue ?? ""}
onChange={onVenueSelect}
>
<option value="">Vis alle</option>
{venues.map((venue) => (
<option key={venue.slug} value={venue.slug}>
{venue.title}
</option>
))}
</select>
</div>
</div>
<div className={styles.filterItem}> <div className={styles.filterItem}>
<label htmlFor="organizer" className={`suphead ${styles.heading}`}> <label htmlFor="organizer" className={`suphead ${styles.heading}`}>
Arrangør Arrangør

View File

@ -38,7 +38,7 @@ const documents = {
"\n fragment Image on CustomImage {\n id\n url\n width\n height\n alt\n attribution\n }\n": types.ImageFragmentDoc, "\n fragment Image on CustomImage {\n id\n url\n width\n height\n alt\n attribution\n }\n": types.ImageFragmentDoc,
"\n fragment ContactEntity on ContactEntity {\n id\n name\n contactType\n title\n email\n phoneNumber\n image {\n ...Image\n }\n }\n": types.ContactEntityFragmentDoc, "\n fragment ContactEntity on ContactEntity {\n id\n name\n contactType\n title\n email\n phoneNumber\n image {\n ...Image\n }\n }\n": types.ContactEntityFragmentDoc,
"\n fragment Event on EventPage {\n __typename\n id\n slug\n title\n body {\n id\n blockType\n field\n ... on RichTextBlock {\n rawValue\n value\n }\n }\n featuredImage {\n ...Image\n }\n pig\n facebookUrl\n ticketUrl\n free\n priceRegular\n priceMember\n priceStudent\n categories {\n ... on EventCategory {\n name\n slug\n pig\n }\n }\n occurrences {\n ... on EventOccurrence {\n __typename\n id\n start\n end\n venue {\n __typename\n id\n slug\n title\n preposition\n url\n }\n }\n }\n organizers {\n ... on EventOrganizer {\n id\n name\n slug\n externalUrl\n association {\n ... on AssociationPage {\n url\n }\n }\n }\n }\n }\n": types.EventFragmentDoc, "\n fragment Event on EventPage {\n __typename\n id\n slug\n title\n body {\n id\n blockType\n field\n ... on RichTextBlock {\n rawValue\n value\n }\n }\n featuredImage {\n ...Image\n }\n pig\n facebookUrl\n ticketUrl\n free\n priceRegular\n priceMember\n priceStudent\n categories {\n ... on EventCategory {\n name\n slug\n pig\n }\n }\n occurrences {\n ... on EventOccurrence {\n __typename\n id\n start\n end\n venue {\n __typename\n id\n slug\n title\n preposition\n url\n }\n }\n }\n organizers {\n ... on EventOrganizer {\n id\n name\n slug\n externalUrl\n association {\n ... on AssociationPage {\n url\n }\n }\n }\n }\n }\n": types.EventFragmentDoc,
"\n query futureEvents {\n events: eventIndex {\n ... on EventIndex {\n futureEvents {\n ... on EventPage {\n ...Event\n }\n }\n }\n }\n eventCategories: eventCategories {\n ... on EventCategory {\n name\n slug\n showInFilters\n }\n }\n eventOrganizers: eventOrganizers {\n ... on EventOrganizer {\n id\n name\n slug\n externalUrl\n association {\n ... on AssociationPage {\n url\n }\n }\n }\n }\n }\n": types.FutureEventsDocument, "\n query futureEvents {\n events: eventIndex {\n ... on EventIndex {\n futureEvents {\n ... on EventPage {\n ...Event\n }\n }\n }\n }\n eventCategories: eventCategories {\n ... on EventCategory {\n name\n slug\n showInFilters\n }\n }\n eventOrganizers: eventOrganizers {\n ... on EventOrganizer {\n id\n name\n slug\n externalUrl\n association {\n ... on AssociationPage {\n url\n }\n }\n }\n }\n venues: pages(contentType: \"venues.VenuePage\") {\n ... on VenuePage {\n id\n title\n slug\n }\n }\n }\n": types.FutureEventsDocument,
"\n fragment News on NewsPage {\n __typename\n id\n slug\n title\n firstPublishedAt\n excerpt\n featuredImage {\n ...Image\n }\n body {\n ...Blocks\n }\n }\n": types.NewsFragmentDoc, "\n fragment News on NewsPage {\n __typename\n id\n slug\n title\n firstPublishedAt\n excerpt\n featuredImage {\n ...Image\n }\n body {\n ...Blocks\n }\n }\n": types.NewsFragmentDoc,
"\n fragment NewsIndex on NewsIndex {\n __typename\n id\n slug\n title\n lead\n }\n": types.NewsIndexFragmentDoc, "\n fragment NewsIndex on NewsIndex {\n __typename\n id\n slug\n title\n lead\n }\n": types.NewsIndexFragmentDoc,
"\n query news {\n index: newsIndex {\n ... on NewsIndex {\n ...NewsIndex\n }\n }\n news: pages(contentType: \"news.NewsPage\") {\n ... on NewsPage {\n ...News\n }\n }\n }\n": types.NewsDocument, "\n query news {\n index: newsIndex {\n ... on NewsIndex {\n ...NewsIndex\n }\n }\n news: pages(contentType: \"news.NewsPage\") {\n ... on NewsPage {\n ...News\n }\n }\n }\n": types.NewsDocument,
@ -165,7 +165,7 @@ export function graphql(source: "\n fragment Event on EventPage {\n __typena
/** /**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
export function graphql(source: "\n query futureEvents {\n events: eventIndex {\n ... on EventIndex {\n futureEvents {\n ... on EventPage {\n ...Event\n }\n }\n }\n }\n eventCategories: eventCategories {\n ... on EventCategory {\n name\n slug\n showInFilters\n }\n }\n eventOrganizers: eventOrganizers {\n ... on EventOrganizer {\n id\n name\n slug\n externalUrl\n association {\n ... on AssociationPage {\n url\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query futureEvents {\n events: eventIndex {\n ... on EventIndex {\n futureEvents {\n ... on EventPage {\n ...Event\n }\n }\n }\n }\n eventCategories: eventCategories {\n ... on EventCategory {\n name\n slug\n showInFilters\n }\n }\n eventOrganizers: eventOrganizers {\n ... on EventOrganizer {\n id\n name\n slug\n externalUrl\n association {\n ... on AssociationPage {\n url\n }\n }\n }\n }\n }\n"]; export function graphql(source: "\n query futureEvents {\n events: eventIndex {\n ... on EventIndex {\n futureEvents {\n ... on EventPage {\n ...Event\n }\n }\n }\n }\n eventCategories: eventCategories {\n ... on EventCategory {\n name\n slug\n showInFilters\n }\n }\n eventOrganizers: eventOrganizers {\n ... on EventOrganizer {\n id\n name\n slug\n externalUrl\n association {\n ... on AssociationPage {\n url\n }\n }\n }\n }\n venues: pages(contentType: \"venues.VenuePage\") {\n ... on VenuePage {\n id\n title\n slug\n }\n }\n }\n"): (typeof documents)["\n query futureEvents {\n events: eventIndex {\n ... on EventIndex {\n futureEvents {\n ... on EventPage {\n ...Event\n }\n }\n }\n }\n eventCategories: eventCategories {\n ... on EventCategory {\n name\n slug\n showInFilters\n }\n }\n eventOrganizers: eventOrganizers {\n ... on EventOrganizer {\n id\n name\n slug\n externalUrl\n association {\n ... on AssociationPage {\n url\n }\n }\n }\n }\n venues: pages(contentType: \"venues.VenuePage\") {\n ... on VenuePage {\n id\n title\n slug\n }\n }\n }\n"];
/** /**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */

File diff suppressed because one or more lines are too long

View File

@ -118,6 +118,13 @@ export const futureEventsQuery = graphql(`
} }
} }
} }
venues: pages(contentType: "venues.VenuePage") {
... on VenuePage {
id
title
slug
}
}
} }
`); `);