Compare commits

...

3 Commits

13 changed files with 199 additions and 54 deletions
+18 -2
View File
@@ -1,7 +1,7 @@
from django import forms from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.db.models import Min, Q, UniqueConstraint from django.db.models import Min, Prefetch, Q, UniqueConstraint
from django.utils import timezone from django.utils import timezone
from django.utils.html import mark_safe from django.utils.html import mark_safe
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@@ -45,7 +45,23 @@ class EventIndex(HeadlessMixin, Page):
subpage_types = ["events.EventPage"] subpage_types = ["events.EventPage"]
def future_events(self, info, **kwargs): def future_events(self, info, **kwargs):
return EventPage.objects.live().future().order_by("next_occurrence") return (
EventPage.objects.live()
.future()
.order_by("next_occurrence")
.select_related("featured_image")
.prefetch_related(
Prefetch(
"occurrences",
queryset=EventOccurrence.objects.select_related("venue"),
),
"categories",
Prefetch(
"organizers",
queryset=EventOrganizer.objects.select_related("association"),
),
)
)
graphql_fields = [ graphql_fields = [
GraphQLCollection( GraphQLCollection(
+60
View File
@@ -2,6 +2,8 @@ from datetime import datetime, timedelta
import pytest import pytest
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import connection
from django.test.utils import CaptureQueriesContext
from django.utils import timezone from django.utils import timezone
from events.admin import EventDateColumn, OrganizersColumn from events.admin import EventDateColumn, OrganizersColumn
@@ -188,6 +190,64 @@ def test_graphql_event_index_future_events_query(event_index, graphql_post):
assert "Upcoming gig" in titles assert "Upcoming gig" in titles
def test_future_events_does_not_have_n_plus_one_queries(
event_index, venue, association_index, graphql_post
):
"""Regression test: query count for futureEvents stays bounded as events grow."""
konsert = EventCategory.objects.create(name="Konsert", slug="konsert")
association = AssociationPageFactory(parent=association_index, title="DNS")
org = EventOrganizer.objects.create(name="Forening", slug="forening", association=association)
image = CustomImageFactory(title="Cover")
now = timezone.now()
for i in range(5):
event = EventPageFactory(
parent=event_index,
title=f"Event {i}",
body=[("paragraph", "<p>x</p>")],
featured_image=image,
)
event.categories.add(konsert)
EventOrganizerLink.objects.create(event=event, organizer=org)
EventOccurrence.objects.create(
event=event,
start=now + timedelta(days=i + 1),
venue=venue,
)
home_query = """
query {
eventIndex {
futureEvents {
id
title
subtitle
body { blockType }
featuredImage { url }
occurrences { start end venueCustom venue { title } }
categories { name slug }
organizers { name slug association { title } }
}
}
}
"""
with CaptureQueriesContext(connection) as ctx:
response, body = graphql_post(home_query)
assert response.status_code == 200
assert "errors" not in body, body
assert len(body["data"]["eventIndex"]["futureEvents"]) == 5
# Bump only alongside an intentional resolver change.
max_queries = 6
assert len(ctx) <= max_queries, (
f"futureEvents took {len(ctx)} queries for 5 events — likely N+1. "
f"Captured queries:\n"
+ "\n".join(f" {i + 1}. {q['sql'][:120]}" for i, q in enumerate(ctx.captured_queries))
)
def test_graphql_event_index_future_events_ordered_by_next_occurrence(event_index, graphql_post): def test_graphql_event_index_future_events_ordered_by_next_occurrence(event_index, graphql_post):
now = timezone.now() now = timezone.now()
+1
View File
@@ -34,6 +34,7 @@ export default function RootLayout({
return ( return (
<html lang="no"> <html lang="no">
<head> <head>
<link rel="preconnect" href="https://use.typekit.net" crossOrigin="anonymous" />
<link rel="stylesheet" href="https://use.typekit.net/spa5smt.css" /> <link rel="stylesheet" href="https://use.typekit.net/spa5smt.css" />
{process.env.UMAMI_SCRIPT_URL && process.env.UMAMI_WEBSITE_ID && ( {process.env.UMAMI_SCRIPT_URL && process.env.UMAMI_WEBSITE_ID && (
<script <script
+6 -1
View File
@@ -6,6 +6,7 @@ import { Image } from "@/components/general/Image";
import { import {
SingularEvent, SingularEvent,
EventFragment, EventFragment,
EventListItemFragment,
getFutureOccurrences, getFutureOccurrences,
} from "@/lib/event"; } from "@/lib/event";
import { import {
@@ -22,7 +23,11 @@ export const EventItem = ({
size, size,
imageLoading, imageLoading,
}: { }: {
event: SingularEvent | EventFragment; event:
| SingularEvent
| SingularEvent<EventListItemFragment>
| EventFragment
| EventListItemFragment;
mode: "list" | "calendar" | "singular-time-only"; mode: "list" | "calendar" | "singular-time-only";
size?: "small" | "medium" | "large"; size?: "small" | "medium" | "large";
imageLoading?: "eager" | "lazy"; imageLoading?: "eager" | "lazy";
+2 -2
View File
@@ -1,10 +1,10 @@
import { EventFragment } from "@/gql/graphql"; import { EventListItemFragment } from "@/gql/graphql";
import { EventItem } from "./EventItem"; import { EventItem } from "./EventItem";
import styles from "./featuredEvents.module.scss"; import styles from "./featuredEvents.module.scss";
import { SectionHeader } from "../general/SectionHeader"; import { SectionHeader } from "../general/SectionHeader";
import { SectionFooter } from "../general/SectionFooter"; import { SectionFooter } from "../general/SectionFooter";
export const FeaturedEvents = ({ events }: { events: EventFragment[] }) => { export const FeaturedEvents = ({ events }: { events: EventListItemFragment[] }) => {
return ( return (
<section className={styles.featuredEvents}> <section className={styles.featuredEvents}>
<SectionHeader heading="Arrangementer" link="/arrangementer" linkText="Se alle arrangementer" /> <SectionHeader heading="Arrangementer" link="/arrangementer" linkText="Se alle arrangementer" />
+3 -5
View File
@@ -1,18 +1,16 @@
import { EventFragment } from "@/gql/graphql"; import { EventListItemFragment } from "@/gql/graphql";
import { isTodayOrFuture, formatDate } from "@/lib/date"; import { isTodayOrFuture } from "@/lib/date";
import { parse } from "date-fns";
import { import {
getSingularEvents, getSingularEvents,
organizeEventsByDate, organizeEventsByDate,
sortSingularEvents, sortSingularEvents,
} from "@/lib/event"; } from "@/lib/event";
import Link from "next/link";
import { EventItem } from "./EventItem"; import { EventItem } from "./EventItem";
import styles from "./upcomingEvents.module.scss"; import styles from "./upcomingEvents.module.scss";
import { SectionHeader } from "../general/SectionHeader"; import { SectionHeader } from "../general/SectionHeader";
import { SectionFooter } from "../general/SectionFooter"; import { SectionFooter } from "../general/SectionFooter";
export const UpcomingEvents = ({ events }: { events: EventFragment[] }) => { export const UpcomingEvents = ({ events }: { events: EventListItemFragment[] }) => {
const upcomingSingularEvents = sortSingularEvents( const upcomingSingularEvents = sortSingularEvents(
getSingularEvents(events).filter((event) => getSingularEvents(events).filter((event) =>
isTodayOrFuture(event.occurrence.start) isTodayOrFuture(event.occurrence.start)
+8 -8
View File
@@ -2,8 +2,8 @@ import Link from "next/link";
import { graphql } from "@/gql"; import { graphql } from "@/gql";
import { HomeFragment } from "@/gql/graphql"; import { HomeFragment } from "@/gql/graphql";
import { getClient } from "@/app/client"; import { getClient } from "@/app/client";
import { EventFragment } from "@/lib/event"; import { EventListItemFragment } from "@/lib/event";
import { NewsFragment } from "@/lib/news"; import { NewsListItemFragment } from "@/lib/news";
import { FeaturedEvents } from "@/components/events/FeaturedEvents"; import { FeaturedEvents } from "@/components/events/FeaturedEvents";
import { UpcomingEvents } from "@/components/events/UpcomingEvents"; import { UpcomingEvents } from "@/components/events/UpcomingEvents";
import { Icon } from "@/components/general/Icon"; import { Icon } from "@/components/general/Icon";
@@ -28,7 +28,7 @@ const homeQuery = graphql(`
... on EventIndex { ... on EventIndex {
futureEvents { futureEvents {
... on EventPage { ... on EventPage {
...Event ...EventListItem
} }
} }
} }
@@ -40,7 +40,7 @@ const homeQuery = graphql(`
} }
news: pages(contentType: "news.newsPage", order: "-first_published_at", limit: 4) { news: pages(contentType: "news.newsPage", order: "-first_published_at", limit: 4) {
... on NewsPage { ... on NewsPage {
...News ...NewsListItem
} }
} }
} }
@@ -48,8 +48,8 @@ const homeQuery = graphql(`
export type HomePageViewProps = { export type HomePageViewProps = {
home: HomeFragment; home: HomeFragment;
events: EventFragment[]; events: EventListItemFragment[];
news: NewsFragment[]; news: NewsListItemFragment[];
}; };
export async function loadHomePageProps(overrides?: { export async function loadHomePageProps(overrides?: {
@@ -59,8 +59,8 @@ export async function loadHomePageProps(overrides?: {
if (error) throw new Error(error.message); if (error) throw new Error(error.message);
const home = overrides?.homeOverride ?? (data?.home as HomeFragment | undefined); const home = overrides?.homeOverride ?? (data?.home as HomeFragment | undefined);
if (!home) throw new Error("Failed to load /"); if (!home) throw new Error("Failed to load /");
const events = (data?.events?.futureEvents ?? []) as EventFragment[]; const events = (data?.events?.futureEvents ?? []) as EventListItemFragment[];
const news = (data?.news ?? []) as NewsFragment[]; const news = (data?.news ?? []) as NewsListItemFragment[];
return { home, events, news }; return { home, events, news };
} }
+2 -2
View File
@@ -1,10 +1,10 @@
import styles from "./newsItem.module.scss"; import styles from "./newsItem.module.scss";
import { Image } from "@/components/general/Image"; import { Image } from "@/components/general/Image";
import { NewsFragment } from "@/lib/news"; import { NewsFragment, NewsListItemFragment } from "@/lib/news";
import { formatDate } from "@/lib/date"; import { formatDate } from "@/lib/date";
import Link from "next/link"; import Link from "next/link";
export const NewsItem = ({ news }: { news: NewsFragment }) => { export const NewsItem = ({ news }: { news: NewsFragment | NewsListItemFragment }) => {
const featuredImage: any = news.featuredImage; const featuredImage: any = news.featuredImage;
return ( return (
+2 -2
View File
@@ -3,7 +3,7 @@ import { useState } from "react";
import { SectionHeader } from "../general/SectionHeader"; import { SectionHeader } from "../general/SectionHeader";
import { NewsItem } from "./NewsItem"; import { NewsItem } from "./NewsItem";
import styles from "./newsList.module.scss"; import styles from "./newsList.module.scss";
import { NewsFragment } from "@/lib/news"; import { NewsFragment, NewsListItemFragment } from "@/lib/news";
import { SectionFooter } from "../general/SectionFooter"; import { SectionFooter } from "../general/SectionFooter";
export const NewsList = ({ export const NewsList = ({
@@ -11,7 +11,7 @@ export const NewsList = ({
heading, heading,
featured featured
}: { }: {
news: NewsFragment[]; news: (NewsFragment | NewsListItemFragment)[];
heading?: string; heading?: string;
featured?: boolean; featured?: boolean;
}) => { }) => {
+15 -3
View File
@@ -45,7 +45,7 @@ type Documents = {
"\n fragment Generic on GenericPage {\n __typename\n id\n urlPath\n seoTitle\n searchDescription\n title\n lead\n pig\n body {\n ...Blocks\n }\n }\n": typeof types.GenericFragmentDoc, "\n fragment Generic on GenericPage {\n __typename\n id\n urlPath\n seoTitle\n searchDescription\n title\n lead\n pig\n body {\n ...Blocks\n }\n }\n": typeof types.GenericFragmentDoc,
"\n query genericPageByUrl($urlPath: String!) {\n page: page(contentType: \"generic.GenericPage\", urlPath: $urlPath) {\n ... on GenericPage {\n ...Generic\n }\n }\n }\n": typeof types.GenericPageByUrlDocument, "\n query genericPageByUrl($urlPath: String!) {\n page: page(contentType: \"generic.GenericPage\", urlPath: $urlPath) {\n ... on GenericPage {\n ...Generic\n }\n }\n }\n": typeof types.GenericPageByUrlDocument,
"\n fragment Home on HomePage {\n __typename\n featuredEvents {\n id\n }\n }\n": typeof types.HomeFragmentDoc, "\n fragment Home on HomePage {\n __typename\n featuredEvents {\n id\n }\n }\n": typeof types.HomeFragmentDoc,
"\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\", order: \"-first_published_at\", limit: 4) {\n ... on NewsPage {\n ...News\n }\n }\n }\n": typeof types.HomeDocument, "\n query home {\n events: eventIndex {\n ... on EventIndex {\n futureEvents {\n ... on EventPage {\n ...EventListItem\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\", order: \"-first_published_at\", limit: 4) {\n ... on NewsPage {\n ...NewsListItem\n }\n }\n }\n": typeof types.HomeDocument,
"\n query newsBySlug($slug: String!) {\n news: page(contentType: \"news.NewsPage\", slug: $slug) {\n ... on NewsPage {\n ...News\n }\n }\n }\n": typeof types.NewsBySlugDocument, "\n query newsBySlug($slug: String!) {\n news: page(contentType: \"news.NewsPage\", slug: $slug) {\n ... on NewsPage {\n ...News\n }\n }\n }\n": typeof types.NewsBySlugDocument,
"\n fragment Sponsor on SponsorBlock {\n id\n name\n logo {\n ...Image\n }\n text\n website\n }\n": typeof types.SponsorFragmentDoc, "\n fragment Sponsor on SponsorBlock {\n id\n name\n logo {\n ...Image\n }\n text\n website\n }\n": typeof types.SponsorFragmentDoc,
"\n fragment SponsorsPage on SponsorsPage {\n __typename\n title\n seoTitle\n searchDescription\n lead\n body {\n ...Blocks\n }\n sponsors {\n ... on SponsorBlock {\n ...Sponsor\n }\n }\n }\n": typeof types.SponsorsPageFragmentDoc, "\n fragment SponsorsPage on SponsorsPage {\n __typename\n title\n seoTitle\n searchDescription\n lead\n body {\n ...Blocks\n }\n sponsors {\n ... on SponsorBlock {\n ...Sponsor\n }\n }\n }\n": typeof types.SponsorsPageFragmentDoc,
@@ -65,10 +65,12 @@ type Documents = {
"\n fragment ContactEntity on ContactEntity {\n id\n name\n contactType\n title\n email\n phoneNumber\n image {\n ...Image\n }\n }\n": typeof 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": typeof types.ContactEntityFragmentDoc,
"\n fragment EventCategory on EventCategory {\n __typename\n name\n slug\n pig\n showInFilters\n }\n": typeof types.EventCategoryFragmentDoc, "\n fragment EventCategory on EventCategory {\n __typename\n name\n slug\n pig\n showInFilters\n }\n": typeof types.EventCategoryFragmentDoc,
"\n fragment EventOrganizer on EventOrganizer {\n __typename\n id\n name\n slug\n externalUrl\n association {\n ... on AssociationPage {\n url\n }\n }\n }\n": typeof types.EventOrganizerFragmentDoc, "\n fragment EventOrganizer on EventOrganizer {\n __typename\n id\n name\n slug\n externalUrl\n association {\n ... on AssociationPage {\n url\n }\n }\n }\n": typeof types.EventOrganizerFragmentDoc,
"\n fragment EventListItem on EventPage {\n __typename\n id\n slug\n title\n subtitle\n featuredImage {\n ...Image\n }\n occurrences(limit: 5000) {\n ... on EventOccurrence {\n __typename\n id\n start\n end\n }\n }\n }\n": typeof types.EventListItemFragmentDoc,
"\n fragment Event on EventPage {\n __typename\n id\n slug\n seoTitle\n searchDescription\n title\n subtitle\n lead\n body {\n ...OneLevelOfBlocks\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 ...EventCategory\n }\n }\n occurrences(limit: 5000) {\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 venueCustom\n }\n }\n organizers {\n ... on EventOrganizer {\n ...EventOrganizer\n }\n }\n }\n": typeof types.EventFragmentDoc, "\n fragment Event on EventPage {\n __typename\n id\n slug\n seoTitle\n searchDescription\n title\n subtitle\n lead\n body {\n ...OneLevelOfBlocks\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 ...EventCategory\n }\n }\n occurrences(limit: 5000) {\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 venueCustom\n }\n }\n organizers {\n ... on EventOrganizer {\n ...EventOrganizer\n }\n }\n }\n": typeof types.EventFragmentDoc,
"\n fragment EventIndex on EventIndex {\n __typename\n id\n slug\n seoTitle\n searchDescription\n title\n }\n": typeof types.EventIndexFragmentDoc, "\n fragment EventIndex on EventIndex {\n __typename\n id\n slug\n seoTitle\n searchDescription\n title\n }\n": typeof types.EventIndexFragmentDoc,
"\n query eventIndexMetadata {\n index: eventIndex {\n ... on EventIndex {\n ...EventIndex\n }\n }\n }\n": typeof types.EventIndexMetadataDocument, "\n query eventIndexMetadata {\n index: eventIndex {\n ... on EventIndex {\n ...EventIndex\n }\n }\n }\n": typeof types.EventIndexMetadataDocument,
"\n query futureEvents {\n index: eventIndex {\n ... on EventIndex {\n ...EventIndex\n }\n }\n events: eventIndex {\n ... on EventIndex {\n futureEvents {\n ... on EventPage {\n ...Event\n }\n }\n }\n }\n eventCategories: eventCategories(limit: 5000) {\n ... on EventCategory {\n ...EventCategory\n }\n }\n eventOrganizers: eventOrganizers(limit: 5000) {\n ... on EventOrganizer {\n ...EventOrganizer\n }\n }\n venues: pages(contentType: \"venues.VenuePage\") {\n ... on VenuePage {\n id\n title\n slug\n preposition\n }\n }\n }\n": typeof types.FutureEventsDocument, "\n query futureEvents {\n index: eventIndex {\n ... on EventIndex {\n ...EventIndex\n }\n }\n events: eventIndex {\n ... on EventIndex {\n futureEvents {\n ... on EventPage {\n ...Event\n }\n }\n }\n }\n eventCategories: eventCategories(limit: 5000) {\n ... on EventCategory {\n ...EventCategory\n }\n }\n eventOrganizers: eventOrganizers(limit: 5000) {\n ... on EventOrganizer {\n ...EventOrganizer\n }\n }\n venues: pages(contentType: \"venues.VenuePage\") {\n ... on VenuePage {\n id\n title\n slug\n preposition\n }\n }\n }\n": typeof types.FutureEventsDocument,
"\n fragment NewsListItem on NewsPage {\n __typename\n id\n slug\n title\n firstPublishedAt\n excerpt\n featuredImage {\n ...Image\n }\n }\n": typeof types.NewsListItemFragmentDoc,
"\n fragment News on NewsPage {\n __typename\n id\n slug\n seoTitle\n searchDescription\n title\n firstPublishedAt\n excerpt\n lead\n featuredImage {\n ...Image\n }\n body {\n ...Blocks\n }\n }\n": typeof types.NewsFragmentDoc, "\n fragment News on NewsPage {\n __typename\n id\n slug\n seoTitle\n searchDescription\n title\n firstPublishedAt\n excerpt\n lead\n featuredImage {\n ...Image\n }\n body {\n ...Blocks\n }\n }\n": typeof types.NewsFragmentDoc,
"\n fragment NewsIndex on NewsIndex {\n __typename\n id\n slug\n seoTitle\n searchDescription\n title\n lead\n }\n": typeof types.NewsIndexFragmentDoc, "\n fragment NewsIndex on NewsIndex {\n __typename\n id\n slug\n seoTitle\n searchDescription\n title\n lead\n }\n": typeof types.NewsIndexFragmentDoc,
"\n query news {\n index: newsIndex {\n ... on NewsIndex {\n ...NewsIndex\n }\n }\n news: pages(contentType: \"news.NewsPage\", order: \"-first_published_at\", limit: 1000) {\n ... on NewsPage {\n ...News\n }\n }\n }\n": typeof types.NewsDocument, "\n query news {\n index: newsIndex {\n ... on NewsIndex {\n ...NewsIndex\n }\n }\n news: pages(contentType: \"news.NewsPage\", order: \"-first_published_at\", limit: 1000) {\n ... on NewsPage {\n ...News\n }\n }\n }\n": typeof types.NewsDocument,
@@ -109,7 +111,7 @@ const documents: Documents = {
"\n fragment Generic on GenericPage {\n __typename\n id\n urlPath\n seoTitle\n searchDescription\n title\n lead\n pig\n body {\n ...Blocks\n }\n }\n": types.GenericFragmentDoc, "\n fragment Generic on GenericPage {\n __typename\n id\n urlPath\n seoTitle\n searchDescription\n title\n lead\n pig\n body {\n ...Blocks\n }\n }\n": types.GenericFragmentDoc,
"\n query genericPageByUrl($urlPath: String!) {\n page: page(contentType: \"generic.GenericPage\", urlPath: $urlPath) {\n ... on GenericPage {\n ...Generic\n }\n }\n }\n": types.GenericPageByUrlDocument, "\n query genericPageByUrl($urlPath: String!) {\n page: page(contentType: \"generic.GenericPage\", urlPath: $urlPath) {\n ... on GenericPage {\n ...Generic\n }\n }\n }\n": types.GenericPageByUrlDocument,
"\n fragment Home on HomePage {\n __typename\n featuredEvents {\n id\n }\n }\n": types.HomeFragmentDoc, "\n fragment Home on HomePage {\n __typename\n featuredEvents {\n id\n }\n }\n": types.HomeFragmentDoc,
"\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\", order: \"-first_published_at\", limit: 4) {\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 ...EventListItem\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\", order: \"-first_published_at\", limit: 4) {\n ... on NewsPage {\n ...NewsListItem\n }\n }\n }\n": types.HomeDocument,
"\n query newsBySlug($slug: String!) {\n news: page(contentType: \"news.NewsPage\", slug: $slug) {\n ... on NewsPage {\n ...News\n }\n }\n }\n": types.NewsBySlugDocument, "\n query newsBySlug($slug: String!) {\n news: page(contentType: \"news.NewsPage\", slug: $slug) {\n ... on NewsPage {\n ...News\n }\n }\n }\n": types.NewsBySlugDocument,
"\n fragment Sponsor on SponsorBlock {\n id\n name\n logo {\n ...Image\n }\n text\n website\n }\n": types.SponsorFragmentDoc, "\n fragment Sponsor on SponsorBlock {\n id\n name\n logo {\n ...Image\n }\n text\n website\n }\n": types.SponsorFragmentDoc,
"\n fragment SponsorsPage on SponsorsPage {\n __typename\n title\n seoTitle\n searchDescription\n lead\n body {\n ...Blocks\n }\n sponsors {\n ... on SponsorBlock {\n ...Sponsor\n }\n }\n }\n": types.SponsorsPageFragmentDoc, "\n fragment SponsorsPage on SponsorsPage {\n __typename\n title\n seoTitle\n searchDescription\n lead\n body {\n ...Blocks\n }\n sponsors {\n ... on SponsorBlock {\n ...Sponsor\n }\n }\n }\n": types.SponsorsPageFragmentDoc,
@@ -129,10 +131,12 @@ const documents: Documents = {
"\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 EventCategory on EventCategory {\n __typename\n name\n slug\n pig\n showInFilters\n }\n": types.EventCategoryFragmentDoc, "\n fragment EventCategory on EventCategory {\n __typename\n name\n slug\n pig\n showInFilters\n }\n": types.EventCategoryFragmentDoc,
"\n fragment EventOrganizer on EventOrganizer {\n __typename\n id\n name\n slug\n externalUrl\n association {\n ... on AssociationPage {\n url\n }\n }\n }\n": types.EventOrganizerFragmentDoc, "\n fragment EventOrganizer on EventOrganizer {\n __typename\n id\n name\n slug\n externalUrl\n association {\n ... on AssociationPage {\n url\n }\n }\n }\n": types.EventOrganizerFragmentDoc,
"\n fragment EventListItem on EventPage {\n __typename\n id\n slug\n title\n subtitle\n featuredImage {\n ...Image\n }\n occurrences(limit: 5000) {\n ... on EventOccurrence {\n __typename\n id\n start\n end\n }\n }\n }\n": types.EventListItemFragmentDoc,
"\n fragment Event on EventPage {\n __typename\n id\n slug\n seoTitle\n searchDescription\n title\n subtitle\n lead\n body {\n ...OneLevelOfBlocks\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 ...EventCategory\n }\n }\n occurrences(limit: 5000) {\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 venueCustom\n }\n }\n organizers {\n ... on EventOrganizer {\n ...EventOrganizer\n }\n }\n }\n": types.EventFragmentDoc, "\n fragment Event on EventPage {\n __typename\n id\n slug\n seoTitle\n searchDescription\n title\n subtitle\n lead\n body {\n ...OneLevelOfBlocks\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 ...EventCategory\n }\n }\n occurrences(limit: 5000) {\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 venueCustom\n }\n }\n organizers {\n ... on EventOrganizer {\n ...EventOrganizer\n }\n }\n }\n": types.EventFragmentDoc,
"\n fragment EventIndex on EventIndex {\n __typename\n id\n slug\n seoTitle\n searchDescription\n title\n }\n": types.EventIndexFragmentDoc, "\n fragment EventIndex on EventIndex {\n __typename\n id\n slug\n seoTitle\n searchDescription\n title\n }\n": types.EventIndexFragmentDoc,
"\n query eventIndexMetadata {\n index: eventIndex {\n ... on EventIndex {\n ...EventIndex\n }\n }\n }\n": types.EventIndexMetadataDocument, "\n query eventIndexMetadata {\n index: eventIndex {\n ... on EventIndex {\n ...EventIndex\n }\n }\n }\n": types.EventIndexMetadataDocument,
"\n query futureEvents {\n index: eventIndex {\n ... on EventIndex {\n ...EventIndex\n }\n }\n events: eventIndex {\n ... on EventIndex {\n futureEvents {\n ... on EventPage {\n ...Event\n }\n }\n }\n }\n eventCategories: eventCategories(limit: 5000) {\n ... on EventCategory {\n ...EventCategory\n }\n }\n eventOrganizers: eventOrganizers(limit: 5000) {\n ... on EventOrganizer {\n ...EventOrganizer\n }\n }\n venues: pages(contentType: \"venues.VenuePage\") {\n ... on VenuePage {\n id\n title\n slug\n preposition\n }\n }\n }\n": types.FutureEventsDocument, "\n query futureEvents {\n index: eventIndex {\n ... on EventIndex {\n ...EventIndex\n }\n }\n events: eventIndex {\n ... on EventIndex {\n futureEvents {\n ... on EventPage {\n ...Event\n }\n }\n }\n }\n eventCategories: eventCategories(limit: 5000) {\n ... on EventCategory {\n ...EventCategory\n }\n }\n eventOrganizers: eventOrganizers(limit: 5000) {\n ... on EventOrganizer {\n ...EventOrganizer\n }\n }\n venues: pages(contentType: \"venues.VenuePage\") {\n ... on VenuePage {\n id\n title\n slug\n preposition\n }\n }\n }\n": types.FutureEventsDocument,
"\n fragment NewsListItem on NewsPage {\n __typename\n id\n slug\n title\n firstPublishedAt\n excerpt\n featuredImage {\n ...Image\n }\n }\n": types.NewsListItemFragmentDoc,
"\n fragment News on NewsPage {\n __typename\n id\n slug\n seoTitle\n searchDescription\n title\n firstPublishedAt\n excerpt\n lead\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 seoTitle\n searchDescription\n title\n firstPublishedAt\n excerpt\n lead\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 seoTitle\n searchDescription\n title\n lead\n }\n": types.NewsIndexFragmentDoc, "\n fragment NewsIndex on NewsIndex {\n __typename\n id\n slug\n seoTitle\n searchDescription\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\", order: \"-first_published_at\", limit: 1000) {\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\", order: \"-first_published_at\", limit: 1000) {\n ... on NewsPage {\n ...News\n }\n }\n }\n": types.NewsDocument,
@@ -283,7 +287,7 @@ export function graphql(source: "\n fragment Home on HomePage {\n __typename
/** /**
* 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 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\", order: \"-first_published_at\", limit: 4) {\n ... on NewsPage {\n ...News\n }\n }\n }\n"): (typeof 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\", order: \"-first_published_at\", limit: 4) {\n ... on NewsPage {\n ...News\n }\n }\n }\n"]; export function graphql(source: "\n query home {\n events: eventIndex {\n ... on EventIndex {\n futureEvents {\n ... on EventPage {\n ...EventListItem\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\", order: \"-first_published_at\", limit: 4) {\n ... on NewsPage {\n ...NewsListItem\n }\n }\n }\n"): (typeof documents)["\n query home {\n events: eventIndex {\n ... on EventIndex {\n futureEvents {\n ... on EventPage {\n ...EventListItem\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\", order: \"-first_published_at\", limit: 4) {\n ... on NewsPage {\n ...NewsListItem\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.
*/ */
@@ -360,6 +364,10 @@ export function graphql(source: "\n fragment EventCategory on EventCategory {\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 EventOrganizer on EventOrganizer {\n __typename\n id\n name\n slug\n externalUrl\n association {\n ... on AssociationPage {\n url\n }\n }\n }\n"): (typeof documents)["\n fragment EventOrganizer on EventOrganizer {\n __typename\n id\n name\n slug\n externalUrl\n association {\n ... on AssociationPage {\n url\n }\n }\n }\n"]; export function graphql(source: "\n fragment EventOrganizer on EventOrganizer {\n __typename\n id\n name\n slug\n externalUrl\n association {\n ... on AssociationPage {\n url\n }\n }\n }\n"): (typeof documents)["\n fragment EventOrganizer on EventOrganizer {\n __typename\n id\n name\n slug\n externalUrl\n association {\n ... on AssociationPage {\n url\n }\n }\n }\n"];
/**
* 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 EventListItem on EventPage {\n __typename\n id\n slug\n title\n subtitle\n featuredImage {\n ...Image\n }\n occurrences(limit: 5000) {\n ... on EventOccurrence {\n __typename\n id\n start\n end\n }\n }\n }\n"): (typeof documents)["\n fragment EventListItem on EventPage {\n __typename\n id\n slug\n title\n subtitle\n featuredImage {\n ...Image\n }\n occurrences(limit: 5000) {\n ... on EventOccurrence {\n __typename\n id\n start\n end\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.
*/ */
@@ -376,6 +384,10 @@ export function graphql(source: "\n query eventIndexMetadata {\n index: even
* 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 index: eventIndex {\n ... on EventIndex {\n ...EventIndex\n }\n }\n events: eventIndex {\n ... on EventIndex {\n futureEvents {\n ... on EventPage {\n ...Event\n }\n }\n }\n }\n eventCategories: eventCategories(limit: 5000) {\n ... on EventCategory {\n ...EventCategory\n }\n }\n eventOrganizers: eventOrganizers(limit: 5000) {\n ... on EventOrganizer {\n ...EventOrganizer\n }\n }\n venues: pages(contentType: \"venues.VenuePage\") {\n ... on VenuePage {\n id\n title\n slug\n preposition\n }\n }\n }\n"): (typeof documents)["\n query futureEvents {\n index: eventIndex {\n ... on EventIndex {\n ...EventIndex\n }\n }\n events: eventIndex {\n ... on EventIndex {\n futureEvents {\n ... on EventPage {\n ...Event\n }\n }\n }\n }\n eventCategories: eventCategories(limit: 5000) {\n ... on EventCategory {\n ...EventCategory\n }\n }\n eventOrganizers: eventOrganizers(limit: 5000) {\n ... on EventOrganizer {\n ...EventOrganizer\n }\n }\n venues: pages(contentType: \"venues.VenuePage\") {\n ... on VenuePage {\n id\n title\n slug\n preposition\n }\n }\n }\n"]; export function graphql(source: "\n query futureEvents {\n index: eventIndex {\n ... on EventIndex {\n ...EventIndex\n }\n }\n events: eventIndex {\n ... on EventIndex {\n futureEvents {\n ... on EventPage {\n ...Event\n }\n }\n }\n }\n eventCategories: eventCategories(limit: 5000) {\n ... on EventCategory {\n ...EventCategory\n }\n }\n eventOrganizers: eventOrganizers(limit: 5000) {\n ... on EventOrganizer {\n ...EventOrganizer\n }\n }\n venues: pages(contentType: \"venues.VenuePage\") {\n ... on VenuePage {\n id\n title\n slug\n preposition\n }\n }\n }\n"): (typeof documents)["\n query futureEvents {\n index: eventIndex {\n ... on EventIndex {\n ...EventIndex\n }\n }\n events: eventIndex {\n ... on EventIndex {\n futureEvents {\n ... on EventPage {\n ...Event\n }\n }\n }\n }\n eventCategories: eventCategories(limit: 5000) {\n ... on EventCategory {\n ...EventCategory\n }\n }\n eventOrganizers: eventOrganizers(limit: 5000) {\n ... on EventOrganizer {\n ...EventOrganizer\n }\n }\n venues: pages(contentType: \"venues.VenuePage\") {\n ... on VenuePage {\n id\n title\n slug\n preposition\n }\n }\n }\n"];
/**
* 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 NewsListItem on NewsPage {\n __typename\n id\n slug\n title\n firstPublishedAt\n excerpt\n featuredImage {\n ...Image\n }\n }\n"): (typeof documents)["\n fragment NewsListItem on NewsPage {\n __typename\n id\n slug\n title\n firstPublishedAt\n excerpt\n featuredImage {\n ...Image\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
+58 -25
View File
@@ -12,17 +12,23 @@ import { graphql, unmaskFragment } from "@/gql";
import { import {
type EventCategoryFragment, type EventCategoryFragment,
type EventFragment, type EventFragment,
type EventListItemFragment,
type EventOrganizerFragment, type EventOrganizerFragment,
} from "@/gql/graphql"; } from "@/gql/graphql";
import { PIG_NAMES, randomElement } from "@/lib/common"; import { PIG_NAMES, randomElement } from "@/lib/common";
export type EventOccurrence = EventFragment["occurrences"][number]; export type EventOccurrence = EventFragment["occurrences"][number];
export type EventListItemOccurrence = EventListItemFragment["occurrences"][number];
export type EventCategory = EventCategoryFragment; export type EventCategory = EventCategoryFragment;
export type EventOrganizer = EventOrganizerFragment; export type EventOrganizer = EventOrganizerFragment;
export type { EventFragment }; export type { EventFragment, EventListItemFragment };
export type SingularEvent = EventFragment & { type EventListable = {
occurrence: EventOccurrence; occurrences: ReadonlyArray<{ id: string | null; start: string; end: string | null }>;
};
export type SingularEvent<E extends EventListable = EventFragment> = E & {
occurrence: E["occurrences"][number];
}; };
export const EventCategoryFragmentDefinition = graphql(` export const EventCategoryFragmentDefinition = graphql(`
@@ -50,6 +56,27 @@ export const EventOrganizerFragmentDefinition = graphql(`
} }
`); `);
const EventListItemFragmentDefinition = graphql(`
fragment EventListItem on EventPage {
__typename
id
slug
title
subtitle
featuredImage {
...Image
}
occurrences(limit: 5000) {
... on EventOccurrence {
__typename
id
start
end
}
}
}
`);
const EventFragmentDefinition = graphql(` const EventFragmentDefinition = graphql(`
fragment Event on EventPage { fragment Event on EventPage {
__typename __typename
@@ -161,37 +188,39 @@ export const eventsOverviewQuery = graphql(`
} }
`); `);
export function getSingularEvents(events: EventFragment[]): SingularEvent[] { export function getSingularEvents<E extends EventListable>(
return events events: E[]
.map((event) => { ): SingularEvent<E>[] {
return event.occurrences.map((occurrence) => { return events.flatMap((event) =>
const eventOccurrence: any = structuredClone(event); event.occurrences.map((occurrence) => {
eventOccurrence.occurrence = occurrence; const eventOccurrence = structuredClone(event) as SingularEvent<E>;
return eventOccurrence; eventOccurrence.occurrence = occurrence;
}); return eventOccurrence;
}) })
.flat(); );
} }
export function sortSingularEvents(events: SingularEvent[]) { export function sortSingularEvents<E extends EventListable>(
events: SingularEvent<E>[]
) {
return events.sort((a, b) => return events.sort((a, b) =>
compareDates(a.occurrence.start, b.occurrence.start) compareDates(a.occurrence.start, b.occurrence.start)
); );
} }
interface EventCalendar { interface EventCalendar<E extends EventListable = EventFragment> {
[yearMonth: string]: { [yearMonth: string]: {
[week: string]: { [week: string]: {
[day: string]: SingularEvent[]; [day: string]: SingularEvent<E>[];
}; };
}; };
} }
export function organizeEventsInCalendar( export function organizeEventsInCalendar<E extends EventListable>(
events: SingularEvent[] events: SingularEvent<E>[]
): EventCalendar { ): EventCalendar<E> {
const sortedEvents = sortSingularEvents(events); const sortedEvents = sortSingularEvents(events);
const calendar: EventCalendar = {}; const calendar: EventCalendar<E> = {};
const minDate = new Date(sortedEvents[0]?.occurrence.start); const minDate = new Date(sortedEvents[0]?.occurrence.start);
const maxDate = new Date( const maxDate = new Date(
@@ -243,13 +272,15 @@ export function organizeEventsInCalendar(
return calendar; return calendar;
} }
interface EventsByDate { interface EventsByDate<E extends EventListable = EventFragment> {
[day: string]: SingularEvent[]; [day: string]: SingularEvent<E>[];
} }
export function organizeEventsByDate(events: SingularEvent[]): EventsByDate { export function organizeEventsByDate<E extends EventListable>(
events: SingularEvent<E>[]
): EventsByDate<E> {
const sortedEvents = sortSingularEvents(events); const sortedEvents = sortSingularEvents(events);
const eventsByDate: EventsByDate = {}; const eventsByDate: EventsByDate<E> = {};
sortedEvents.forEach((event) => { sortedEvents.forEach((event) => {
const start = toLocalTime(event.occurrence.start); const start = toLocalTime(event.occurrence.start);
@@ -263,7 +294,9 @@ export function organizeEventsByDate(events: SingularEvent[]): EventsByDate {
return eventsByDate; return eventsByDate;
} }
export function getFutureOccurrences(event: EventFragment): EventOccurrence[] { export function getFutureOccurrences<E extends EventListable>(
event: E
): E["occurrences"][number][] {
const today = startOfToday(); const today = startOfToday();
const occurrences = event?.occurrences ?? []; const occurrences = event?.occurrences ?? [];
const futureOccurrences = occurrences.filter((occurrence) => const futureOccurrences = occurrences.filter((occurrence) =>
@@ -272,7 +305,7 @@ export function getFutureOccurrences(event: EventFragment): EventOccurrence[] {
futureOccurrences.sort( futureOccurrences.sort(
(a, b) => parseISO(a.start).getTime() - parseISO(b.start).getTime() (a, b) => parseISO(a.start).getTime() - parseISO(b.start).getTime()
); );
return futureOccurrences as EventOccurrence[]; return futureOccurrences as E["occurrences"][number][];
} }
export function getEventPig(event: EventFragment): string | null { export function getEventPig(event: EventFragment): string | null {
+15 -1
View File
@@ -1,7 +1,21 @@
import { graphql } from "@/gql"; import { graphql } from "@/gql";
import { NewsFragment } from "@/gql/graphql"; import { NewsFragment } from "@/gql/graphql";
export type { NewsFragment, NewsIndexFragment } from "@/gql/graphql"; export type { NewsFragment, NewsIndexFragment, NewsListItemFragment } from "@/gql/graphql";
const NewsListItemFragmentDefinition = graphql(`
fragment NewsListItem on NewsPage {
__typename
id
slug
title
firstPublishedAt
excerpt
featuredImage {
...Image
}
}
`);
const NewsFragmentDefinition = graphql(` const NewsFragmentDefinition = graphql(`
fragment News on NewsPage { fragment News on NewsPage {