add filter on organizer

This commit is contained in:
2024-06-20 02:14:21 +02:00
parent 7294e500c6
commit 90ada3e449
9 changed files with 126 additions and 25 deletions

View File

@ -198,7 +198,8 @@ class EventOrganizer(ClusterableModel):
] ]
graphql_fields = [ graphql_fields = [
GraphQLString("name"), GraphQLString("name", required=True),
GraphQLString("slug", required=True),
GraphQLForeignKey("association", "associations.AssociationPage", required=False), GraphQLForeignKey("association", "associations.AssociationPage", required=False),
GraphQLString("external_url"), GraphQLString("external_url"),
] ]

View File

@ -4,7 +4,7 @@ import {
futureEventsQuery, futureEventsQuery,
EventFragment, EventFragment,
EventCategory, EventCategory,
EventOccurrence, EventOrganizer,
} 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";
@ -13,12 +13,17 @@ 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[];
return ( return (
<main className="site-main" id="main"> <main className="site-main" id="main">
<PageHeader heading="Dette skjer på Chateau Neuf" /> <PageHeader heading="Dette skjer på Chateau Neuf" />
<Suspense> <Suspense>
<EventContainer events={events} eventCategories={eventCategories} /> <EventContainer
events={events}
eventCategories={eventCategories}
eventOrganizers={eventOrganizers}
/>
</Suspense> </Suspense>
</main> </main>
); );

View File

@ -9,11 +9,13 @@ import {
SingularEvent, SingularEvent,
getSingularEvents, getSingularEvents,
organizeEventsByDate, organizeEventsByDate,
EventOrganizer,
} from "@/lib/event"; } from "@/lib/event";
import { isTodayOrFuture } from "@/lib/date"; import { isTodayOrFuture } from "@/lib/date";
import styles from "./eventContainer.module.scss"; import styles from "./eventContainer.module.scss";
import { formatDate, formatYearMonth } from "@/lib/date"; import { formatDate, formatYearMonth } from "@/lib/date";
import { parse } from "date-fns"; import { parse } from "date-fns";
import { unique } from "@/lib/common";
/* /*
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
@ -22,22 +24,44 @@ import { parse } from "date-fns";
export const EventContainer = ({ export const EventContainer = ({
events, events,
eventCategories, eventCategories,
eventOrganizers,
}: { }: {
events: EventFragment[]; events: EventFragment[];
eventCategories: EventCategory[]; eventCategories: EventCategory[];
eventOrganizers: EventOrganizer[];
}) => { }) => {
const [mode, setMode] = useQueryState( const [mode, setMode] = useQueryState(
"mode", "mode",
parseAsStringLiteral(["list", "calendar"]).withDefault("list") parseAsStringLiteral(["list", "calendar"]).withDefault("list")
); );
const [category, setCategory] = useQueryState("category", parseAsString);
const [organizer, setOrganizer] = useQueryState("organizer", parseAsString);
const filterableCategories = eventCategories.filter((x) => x.showInFilters); const filterableCategories = eventCategories.filter((x) => x.showInFilters);
const [category, setCategory] = useQueryState("category", parseAsString);
const filteredEvents = events.filter( const uniqueOrganizers: string[] = unique(
events
.map((x) => x.organizers)
.flat()
.filter((x) => x.__typename === "EventOrganizer")
.map((x) => x.slug)
);
const filterableOrganizers = uniqueOrganizers
.map((slug) => eventOrganizers.find((haystack) => haystack.slug === slug))
.filter((x) => x !== undefined) as EventOrganizer[];
const filteredEvents = events
.filter(
(x) =>
!organizer ||
x.organizers.map((organizer) => organizer.slug).includes(organizer)
)
.filter(
(x) => (x) =>
!category || !category ||
x.categories.map((eventCategory) => eventCategory.slug).includes(category) x.categories
.map((eventCategory) => eventCategory.slug)
.includes(category)
); );
return ( return (
@ -46,12 +70,15 @@ export const EventContainer = ({
eventCategories={filterableCategories} eventCategories={filterableCategories}
setCategory={setCategory} setCategory={setCategory}
activeCategory={category} activeCategory={category}
eventOrganizers={filterableOrganizers}
setOrganizer={setOrganizer}
activeOrganizer={organizer}
/> />
<div className={styles.eventWrapper}> <div className={styles.eventWrapper}>
<div className={styles.displayOptions}> <div className={styles.displayOptions}>
<button onClick={() => setMode(null)}>Vis liste</button> <button onClick={() => setMode(null)}>Vis liste</button>
<button onClick={() => setMode("calendar")}>Vis kalender</button> <button onClick={() => setMode("calendar")}>Vis kalender</button>
<button>Filter</button> <button>Filtrer</button>
</div> </div>
{mode === "list" && <EventList events={filteredEvents} />} {mode === "list" && <EventList events={filteredEvents} />}
{mode === "calendar" && <EventCalendar events={filteredEvents} />} {mode === "calendar" && <EventCalendar events={filteredEvents} />}

View File

@ -1,4 +1,4 @@
import { EventCategory } from "@/lib/event"; import { EventCategory, EventOrganizer } from "@/lib/event";
import styles from "./eventFilter.module.scss"; import styles from "./eventFilter.module.scss";
@ -6,14 +6,25 @@ export const EventFilter = ({
eventCategories, eventCategories,
setCategory, setCategory,
activeCategory, activeCategory,
eventOrganizers,
setOrganizer,
activeOrganizer,
}: { }: {
eventCategories: EventCategory[]; eventCategories: EventCategory[];
setCategory: (slug: string | null) => void; setCategory: (slug: string | null) => void;
activeCategory: string | null; activeCategory: string | null;
eventOrganizers: EventOrganizer[];
setOrganizer: (slug: string | null) => void;
activeOrganizer: string | null;
}) => { }) => {
const onOrganizerSelect = (e: React.ChangeEvent<HTMLSelectElement>) => {
const organizer = e.target.value;
setOrganizer(organizer === "" ? null : organizer);
};
return ( return (
<div className={styles.eventFilter}> <div className={styles.eventFilter}>
<span className="suphead">Filter</span> <span className="suphead">Kategori</span>
<ul> <ul>
{eventCategories {eventCategories
.filter((x) => x.showInFilters) .filter((x) => x.showInFilters)
@ -34,6 +45,23 @@ export const EventFilter = ({
</li> </li>
))} ))}
</ul> </ul>
<label htmlFor="organizer" className="suphead">
Arrangør
</label>
<div>
<select
name="organizer"
value={activeOrganizer ?? ""}
onChange={onOrganizerSelect}
>
<option value="">Vis alle</option>
{eventOrganizers.map((organizer) => (
<option key={organizer.slug} value={organizer.slug}>
{organizer.name}
</option>
))}
</select>
</div>
</div> </div>
); );
}; };

View File

@ -20,25 +20,45 @@ export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>, _documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> fragmentType: FragmentType<DocumentTypeDecoration<TType, any>>
): TType; ): TType;
// return nullable if `fragmentType` is undefined
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | undefined
): TType | undefined;
// return nullable if `fragmentType` is nullable // return nullable if `fragmentType` is nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | null
): TType | null;
// return nullable if `fragmentType` is nullable or undefined
export function useFragment<TType>( export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>, _documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | null | undefined fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | null | undefined
): TType | null | undefined; ): TType | null | undefined;
// return array of non-nullable if `fragmentType` is array of non-nullable // return array of non-nullable if `fragmentType` is array of non-nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: Array<FragmentType<DocumentTypeDecoration<TType, any>>>
): Array<TType>;
// return array of nullable if `fragmentType` is array of nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: Array<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
): Array<TType> | null | undefined;
// return readonly array of non-nullable if `fragmentType` is array of non-nullable
export function useFragment<TType>( export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>, _documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
): ReadonlyArray<TType>; ): ReadonlyArray<TType>;
// return array of nullable if `fragmentType` is array of nullable // return readonly array of nullable if `fragmentType` is array of nullable
export function useFragment<TType>( export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>, _documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
): ReadonlyArray<TType> | null | undefined; ): ReadonlyArray<TType> | null | undefined;
export function useFragment<TType>( export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>, _documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | Array<FragmentType<DocumentTypeDecoration<TType, any>>> | ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
): TType | ReadonlyArray<TType> | null | undefined { ): TType | Array<TType> | ReadonlyArray<TType> | null | undefined {
return fragmentType as any; return fragmentType as any;
} }

View File

@ -33,8 +33,8 @@ const documents = {
"\n query home {\n events: eventIndex {\n ... on EventIndex {\n futureEvents {\n ... on EventPage {\n ...Event\n }\n }\n }\n }\n home: page(contentType: \"home.HomePage\", urlPath: \"/home/\") {\n ... on HomePage {\n ...Home\n }\n }\n news: pages(contentType: \"news.newsPage\", limit: 3) {\n ... on NewsPage {\n ...News\n }\n }\n }\n ": types.HomeDocument, "\n query home {\n events: eventIndex {\n ... on EventIndex {\n futureEvents {\n ... on EventPage {\n ...Event\n }\n }\n }\n }\n home: page(contentType: \"home.HomePage\", urlPath: \"/home/\") {\n ... on HomePage {\n ...Home\n }\n }\n news: pages(contentType: \"news.newsPage\", limit: 3) {\n ... on NewsPage {\n ...News\n }\n }\n }\n ": types.HomeDocument,
"\n fragment Blocks on StreamFieldInterface {\n id\n blockType\n field\n ... on RichTextBlock {\n rawValue\n value\n }\n ... on ImageWithTextBlock {\n image {\n ...Image\n }\n imageFormat\n text\n }\n ... on ImageSliderBlock {\n images {\n ... on ImageSliderItemBlock {\n image {\n ...Image\n }\n text\n }\n }\n }\n ... on HorizontalRuleBlock {\n color\n }\n ... on FeaturedBlock {\n title\n featuredBlockText: text\n linkText\n imagePosition\n featuredPage {\n contentType\n pageType\n url\n ... on EventPage {\n featuredImage {\n ...Image\n }\n }\n ... on NewsPage {\n featuredImage {\n ...Image\n }\n }\n }\n featuredImageOverride {\n ...Image\n }\n }\n }\n": types.BlocksFragmentDoc, "\n fragment Blocks on StreamFieldInterface {\n id\n blockType\n field\n ... on RichTextBlock {\n rawValue\n value\n }\n ... on ImageWithTextBlock {\n image {\n ...Image\n }\n imageFormat\n text\n }\n ... on ImageSliderBlock {\n images {\n ... on ImageSliderItemBlock {\n image {\n ...Image\n }\n text\n }\n }\n }\n ... on HorizontalRuleBlock {\n color\n }\n ... on FeaturedBlock {\n title\n featuredBlockText: text\n linkText\n imagePosition\n featuredPage {\n contentType\n pageType\n url\n ... on EventPage {\n featuredImage {\n ...Image\n }\n }\n ... on NewsPage {\n featuredImage {\n ...Image\n }\n }\n }\n featuredImageOverride {\n ...Image\n }\n }\n }\n": types.BlocksFragmentDoc,
"\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 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 name\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 }\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 }\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,
@ -137,11 +137,11 @@ export function graphql(source: "\n fragment Image on CustomImage {\n id\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.
*/ */
export function graphql(source: "\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 name\n externalUrl\n association {\n ... on AssociationPage {\n url\n }\n }\n }\n }\n }\n"): (typeof documents)["\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 name\n externalUrl\n association {\n ... on AssociationPage {\n url\n }\n }\n }\n }\n }\n"]; export function graphql(source: "\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"): (typeof documents)["\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"];
/** /**
* 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 }\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 }\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 }\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"];
/** /**
* 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

@ -33,6 +33,10 @@ export function randomElement(array: any[]): any | undefined {
: undefined; : undefined;
} }
export function unique<T>(array: any[]): any[] {
return Array.from(array.reduce((set, item) => set.add(item), new Set()));
}
const BlockFragmentDefinition = graphql(` const BlockFragmentDefinition = graphql(`
fragment Blocks on StreamFieldInterface { fragment Blocks on StreamFieldInterface {
id id

View File

@ -74,7 +74,9 @@ const EventFragmentDefinition = graphql(`
} }
organizers { organizers {
... on EventOrganizer { ... on EventOrganizer {
id
name name
slug
externalUrl externalUrl
association { association {
... on AssociationPage { ... on AssociationPage {
@ -104,6 +106,19 @@ export const futureEventsQuery = graphql(`
showInFilters showInFilters
} }
} }
eventOrganizers: eventOrganizers {
... on EventOrganizer {
id
name
slug
externalUrl
association {
... on AssociationPage {
url
}
}
}
}
} }
`); `);