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.core.exceptions import ValidationError
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.html import mark_safe
from django.utils.translation import gettext_lazy as _
@@ -45,7 +45,23 @@ class EventIndex(HeadlessMixin, Page):
subpage_types = ["events.EventPage"]
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 = [
GraphQLCollection(
+60
View File
@@ -2,6 +2,8 @@ from datetime import datetime, timedelta
import pytest
from django.core.exceptions import ValidationError
from django.db import connection
from django.test.utils import CaptureQueriesContext
from django.utils import timezone
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
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):
now = timezone.now()
+1
View File
@@ -34,6 +34,7 @@ export default function RootLayout({
return (
<html lang="no">
<head>
<link rel="preconnect" href="https://use.typekit.net" crossOrigin="anonymous" />
<link rel="stylesheet" href="https://use.typekit.net/spa5smt.css" />
{process.env.UMAMI_SCRIPT_URL && process.env.UMAMI_WEBSITE_ID && (
<script
+6 -1
View File
@@ -6,6 +6,7 @@ import { Image } from "@/components/general/Image";
import {
SingularEvent,
EventFragment,
EventListItemFragment,
getFutureOccurrences,
} from "@/lib/event";
import {
@@ -22,7 +23,11 @@ export const EventItem = ({
size,
imageLoading,
}: {
event: SingularEvent | EventFragment;
event:
| SingularEvent
| SingularEvent<EventListItemFragment>
| EventFragment
| EventListItemFragment;
mode: "list" | "calendar" | "singular-time-only";
size?: "small" | "medium" | "large";
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 styles from "./featuredEvents.module.scss";
import { SectionHeader } from "../general/SectionHeader";
import { SectionFooter } from "../general/SectionFooter";
export const FeaturedEvents = ({ events }: { events: EventFragment[] }) => {
export const FeaturedEvents = ({ events }: { events: EventListItemFragment[] }) => {
return (
<section className={styles.featuredEvents}>
<SectionHeader heading="Arrangementer" link="/arrangementer" linkText="Se alle arrangementer" />
+3 -5
View File
@@ -1,18 +1,16 @@
import { EventFragment } from "@/gql/graphql";
import { isTodayOrFuture, formatDate } from "@/lib/date";
import { parse } from "date-fns";
import { EventListItemFragment } from "@/gql/graphql";
import { isTodayOrFuture } from "@/lib/date";
import {
getSingularEvents,
organizeEventsByDate,
sortSingularEvents,
} from "@/lib/event";
import Link from "next/link";
import { EventItem } from "./EventItem";
import styles from "./upcomingEvents.module.scss";
import { SectionHeader } from "../general/SectionHeader";
import { SectionFooter } from "../general/SectionFooter";
export const UpcomingEvents = ({ events }: { events: EventFragment[] }) => {
export const UpcomingEvents = ({ events }: { events: EventListItemFragment[] }) => {
const upcomingSingularEvents = sortSingularEvents(
getSingularEvents(events).filter((event) =>
isTodayOrFuture(event.occurrence.start)
+8 -8
View File
@@ -2,8 +2,8 @@ import Link from "next/link";
import { graphql } from "@/gql";
import { HomeFragment } from "@/gql/graphql";
import { getClient } from "@/app/client";
import { EventFragment } from "@/lib/event";
import { NewsFragment } from "@/lib/news";
import { EventListItemFragment } from "@/lib/event";
import { NewsListItemFragment } from "@/lib/news";
import { FeaturedEvents } from "@/components/events/FeaturedEvents";
import { UpcomingEvents } from "@/components/events/UpcomingEvents";
import { Icon } from "@/components/general/Icon";
@@ -28,7 +28,7 @@ const homeQuery = graphql(`
... on EventIndex {
futureEvents {
... on EventPage {
...Event
...EventListItem
}
}
}
@@ -40,7 +40,7 @@ const homeQuery = graphql(`
}
news: pages(contentType: "news.newsPage", order: "-first_published_at", limit: 4) {
... on NewsPage {
...News
...NewsListItem
}
}
}
@@ -48,8 +48,8 @@ const homeQuery = graphql(`
export type HomePageViewProps = {
home: HomeFragment;
events: EventFragment[];
news: NewsFragment[];
events: EventListItemFragment[];
news: NewsListItemFragment[];
};
export async function loadHomePageProps(overrides?: {
@@ -59,8 +59,8 @@ export async function loadHomePageProps(overrides?: {
if (error) throw new Error(error.message);
const home = overrides?.homeOverride ?? (data?.home as HomeFragment | undefined);
if (!home) throw new Error("Failed to load /");
const events = (data?.events?.futureEvents ?? []) as EventFragment[];
const news = (data?.news ?? []) as NewsFragment[];
const events = (data?.events?.futureEvents ?? []) as EventListItemFragment[];
const news = (data?.news ?? []) as NewsListItemFragment[];
return { home, events, news };
}
+2 -2
View File
@@ -1,10 +1,10 @@
import styles from "./newsItem.module.scss";
import { Image } from "@/components/general/Image";
import { NewsFragment } from "@/lib/news";
import { NewsFragment, NewsListItemFragment } from "@/lib/news";
import { formatDate } from "@/lib/date";
import Link from "next/link";
export const NewsItem = ({ news }: { news: NewsFragment }) => {
export const NewsItem = ({ news }: { news: NewsFragment | NewsListItemFragment }) => {
const featuredImage: any = news.featuredImage;
return (
+2 -2
View File
@@ -3,7 +3,7 @@ import { useState } from "react";
import { SectionHeader } from "../general/SectionHeader";
import { NewsItem } from "./NewsItem";
import styles from "./newsList.module.scss";
import { NewsFragment } from "@/lib/news";
import { NewsFragment, NewsListItemFragment } from "@/lib/news";
import { SectionFooter } from "../general/SectionFooter";
export const NewsList = ({
@@ -11,7 +11,7 @@ export const NewsList = ({
heading,
featured
}: {
news: NewsFragment[];
news: (NewsFragment | NewsListItemFragment)[];
heading?: string;
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 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 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 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,
@@ -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 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 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 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 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 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,
@@ -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 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 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 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,
@@ -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 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 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 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 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 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,
@@ -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.
*/
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.
*/
@@ -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.
*/
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.
*/
@@ -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.
*/
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.
*/
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 {
type EventCategoryFragment,
type EventFragment,
type EventListItemFragment,
type EventOrganizerFragment,
} from "@/gql/graphql";
import { PIG_NAMES, randomElement } from "@/lib/common";
export type EventOccurrence = EventFragment["occurrences"][number];
export type EventListItemOccurrence = EventListItemFragment["occurrences"][number];
export type EventCategory = EventCategoryFragment;
export type EventOrganizer = EventOrganizerFragment;
export type { EventFragment };
export type { EventFragment, EventListItemFragment };
export type SingularEvent = EventFragment & {
occurrence: EventOccurrence;
type EventListable = {
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(`
@@ -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(`
fragment Event on EventPage {
__typename
@@ -161,37 +188,39 @@ export const eventsOverviewQuery = graphql(`
}
`);
export function getSingularEvents(events: EventFragment[]): SingularEvent[] {
return events
.map((event) => {
return event.occurrences.map((occurrence) => {
const eventOccurrence: any = structuredClone(event);
eventOccurrence.occurrence = occurrence;
return eventOccurrence;
});
export function getSingularEvents<E extends EventListable>(
events: E[]
): SingularEvent<E>[] {
return events.flatMap((event) =>
event.occurrences.map((occurrence) => {
const eventOccurrence = structuredClone(event) as SingularEvent<E>;
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) =>
compareDates(a.occurrence.start, b.occurrence.start)
);
}
interface EventCalendar {
interface EventCalendar<E extends EventListable = EventFragment> {
[yearMonth: string]: {
[week: string]: {
[day: string]: SingularEvent[];
[day: string]: SingularEvent<E>[];
};
};
}
export function organizeEventsInCalendar(
events: SingularEvent[]
): EventCalendar {
export function organizeEventsInCalendar<E extends EventListable>(
events: SingularEvent<E>[]
): EventCalendar<E> {
const sortedEvents = sortSingularEvents(events);
const calendar: EventCalendar = {};
const calendar: EventCalendar<E> = {};
const minDate = new Date(sortedEvents[0]?.occurrence.start);
const maxDate = new Date(
@@ -243,13 +272,15 @@ export function organizeEventsInCalendar(
return calendar;
}
interface EventsByDate {
[day: string]: SingularEvent[];
interface EventsByDate<E extends EventListable = EventFragment> {
[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 eventsByDate: EventsByDate = {};
const eventsByDate: EventsByDate<E> = {};
sortedEvents.forEach((event) => {
const start = toLocalTime(event.occurrence.start);
@@ -263,7 +294,9 @@ export function organizeEventsByDate(events: SingularEvent[]): EventsByDate {
return eventsByDate;
}
export function getFutureOccurrences(event: EventFragment): EventOccurrence[] {
export function getFutureOccurrences<E extends EventListable>(
event: E
): E["occurrences"][number][] {
const today = startOfToday();
const occurrences = event?.occurrences ?? [];
const futureOccurrences = occurrences.filter((occurrence) =>
@@ -272,7 +305,7 @@ export function getFutureOccurrences(event: EventFragment): EventOccurrence[] {
futureOccurrences.sort(
(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 {
+15 -1
View File
@@ -1,7 +1,21 @@
import { graphql } from "@/gql";
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(`
fragment News on NewsPage {