web: prerender the search page shell and show a skeleton for the event list

This commit is contained in:
ponas committed 2026-10-14 00:29:18 +02:00
1 parent 5f9a8b93cb
commit 9679663589
4 files changed
+89 -11

No files matched your search

+18 -9
View File
@@ -1,3 +1,4 @@
import { Suspense } from "react";
import { connection } from "next/server";
import { getClient } from "@/app/client";
import { uncached } from "@/lib/revalidation";
@@ -10,17 +11,27 @@ import { graphql } from "@/gql";
// TODO: seo metadata?
// Results depend on the query in the URL, so the route renders per request
// and is allowed to block
export const instant = false;
// The results depend on the query in the URL and render per request; the
// heading and the search field around them are prerendered
export const ensureStatic = "shell";
export default async function Page({
type SearchParams = Promise<{ q?: string }>;
export default function Page({
searchParams,
}: {
searchParams?: Promise<{
q?: string;
}>;
searchParams?: SearchParams;
}) {
return (
<main className="site-main" id="main">
<Suspense fallback={<SearchShell initialQuery="">{null}</SearchShell>}>
<Search searchParams={searchParams} />
</Suspense>
</main>
);
}
async function Search({ searchParams }: { searchParams?: SearchParams }) {
const { q: query } = (await searchParams) ?? {};
// search always runs at request time
await connection();
@@ -80,7 +91,6 @@ export default async function Page({
}
return (
<main className="site-main" id="main">
<SearchShell initialQuery={query ?? ""}>
{query ? (
<SearchResults
@@ -90,6 +100,5 @@ export default async function Page({
/>
) : null}
</SearchShell>
</main>
);
}
+2 -2
View File
@@ -3,6 +3,7 @@ import { connection } from "next/server";
import { VenueFragment } from "@/gql/graphql";
import { cmsQueryForToday } from "@/app/client";
import { EventContainer } from "@/components/events/EventContainer";
import { EventListSkeleton } from "@/components/events/EventListSkeleton";
import { PageHeader } from "@/components/general/PageHeader";
import {
EventCategory,
@@ -49,8 +50,7 @@ export function EventIndexView(props: EventIndexViewProps) {
return (
<main className="site-main" id="main">
<PageHeader heading="Dette skjer på Chateau Neuf" align="left" />
{/* the fallback reserves the list's height to keep the footer in place */}
<Suspense fallback={<div style={{ minHeight: "100vh" }} />}>
<Suspense fallback={<EventListSkeleton />}>
<RequestTimeEventContainer {...props} />
</Suspense>
</main>
@@ -0,0 +1,40 @@
import { Icon } from "../general/Icon";
import styles from "./eventContainer.module.scss";
// Stands in for the event list while it streams in. The toolbar is the real
// markup, so the cards start at exactly the height they will end up at; the
// cards have the real image ratio and room for a typical title and date
export function EventListSkeleton() {
return (
<div className={styles.events} inert>
<div className={styles.eventWrapper}>
<div className={styles.displayOptions}>
<div className={styles.displayButtons}>
<button className="button toggler hasIcon" data-active>
<span>Vis liste</span>
<Icon type="list" />
</button>
<button className="button toggler hasIcon" data-active={false}>
<span>Vis kalender</span>
<Icon type="calendar" />
</button>
</div>
<div className={styles.filterButtons}>
<button className="button tertiary">
<span>Filter</span>
<Icon type="filter" />
</button>
</div>
</div>
<ul className={styles.eventList}>
{Array.from({ length: 6 }, (_, i) => (
<li key={i} className={styles.skeletonItem}>
<div className={styles.skeletonImage} />
<div className={styles.skeletonText} />
</li>
))}
</ul>
</div>
</div>
);
}
@@ -20,6 +20,35 @@
padding-bottom: var(--spacing-section-bottom);
}
// mirrors a large EventItem (eventItem.module.scss): image, then text
.skeletonItem {
list-style: none;
margin-bottom: 1rem;
}
.skeletonImage {
padding-top: 60%;
margin-bottom: var(--spacing-xs);
background-color: var(--color-background-secondary);
}
.skeletonText {
// a two-line title and a date; real cards vary with the title's length
height: 5rem;
background:
linear-gradient(var(--color-background-secondary) 0 0) 0 0.4rem / 80% 1.2rem,
linear-gradient(var(--color-background-secondary) 0 0) 0 2.2rem / 55% 1.2rem,
linear-gradient(var(--color-background-secondary) 0 0) 0 4rem / 30% 0.8rem;
background-repeat: no-repeat;
}
@media (max-width: 1200px) {
.skeletonText {
// titles wrap less in the wider one- and two-column cards
height: 4.4rem;
}
}
.displayOptions {
display: flex;
flex-wrap: wrap;