web: prerender the search page shell and show a skeleton for the event list
This commit is contained in:
4 files changed
+98
-20
No files matched your search
+27
-18
@@ -1,3 +1,4 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
import { connection } from "next/server";
|
import { connection } from "next/server";
|
||||||
import { getClient } from "@/app/client";
|
import { getClient } from "@/app/client";
|
||||||
import { uncached } from "@/lib/revalidation";
|
import { uncached } from "@/lib/revalidation";
|
||||||
@@ -10,17 +11,27 @@ import { graphql } from "@/gql";
|
|||||||
|
|
||||||
// TODO: seo metadata?
|
// TODO: seo metadata?
|
||||||
|
|
||||||
// Results depend on the query in the URL, so the route renders per request
|
// The results depend on the query in the URL and render per request; the
|
||||||
// and is allowed to block
|
// heading and the search field around them are prerendered
|
||||||
export const instant = false;
|
export const ensureStatic = "shell";
|
||||||
|
|
||||||
export default async function Page({
|
type SearchParams = Promise<{ q?: string }>;
|
||||||
|
|
||||||
|
export default function Page({
|
||||||
searchParams,
|
searchParams,
|
||||||
}: {
|
}: {
|
||||||
searchParams?: Promise<{
|
searchParams?: SearchParams;
|
||||||
q?: string;
|
|
||||||
}>;
|
|
||||||
}) {
|
}) {
|
||||||
|
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) ?? {};
|
const { q: query } = (await searchParams) ?? {};
|
||||||
// search always runs at request time
|
// search always runs at request time
|
||||||
await connection();
|
await connection();
|
||||||
@@ -80,16 +91,14 @@ export default async function Page({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="site-main" id="main">
|
<SearchShell initialQuery={query ?? ""}>
|
||||||
<SearchShell initialQuery={query ?? ""}>
|
{query ? (
|
||||||
{query ? (
|
<SearchResults
|
||||||
<SearchResults
|
results={results}
|
||||||
results={results}
|
totalCount={totalCount}
|
||||||
totalCount={totalCount}
|
query={query}
|
||||||
query={query}
|
/>
|
||||||
/>
|
) : null}
|
||||||
) : null}
|
</SearchShell>
|
||||||
</SearchShell>
|
|
||||||
</main>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ import { connection } from "next/server";
|
|||||||
import { VenueFragment } from "@/gql/graphql";
|
import { VenueFragment } from "@/gql/graphql";
|
||||||
import { cmsQueryForToday } from "@/app/client";
|
import { cmsQueryForToday } from "@/app/client";
|
||||||
import { EventContainer } from "@/components/events/EventContainer";
|
import { EventContainer } from "@/components/events/EventContainer";
|
||||||
|
import { EventListSkeleton } from "@/components/events/EventListSkeleton";
|
||||||
import { PageHeader } from "@/components/general/PageHeader";
|
import { PageHeader } from "@/components/general/PageHeader";
|
||||||
import {
|
import {
|
||||||
EventCategory,
|
EventCategory,
|
||||||
@@ -49,8 +50,7 @@ export function EventIndexView(props: EventIndexViewProps) {
|
|||||||
return (
|
return (
|
||||||
<main className="site-main" id="main">
|
<main className="site-main" id="main">
|
||||||
<PageHeader heading="Dette skjer på Chateau Neuf" align="left" />
|
<PageHeader heading="Dette skjer på Chateau Neuf" align="left" />
|
||||||
{/* the fallback reserves the list's height to keep the footer in place */}
|
<Suspense fallback={<EventListSkeleton />}>
|
||||||
<Suspense fallback={<div style={{ minHeight: "100vh" }} />}>
|
|
||||||
<RequestTimeEventContainer {...props} />
|
<RequestTimeEventContainer {...props} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</main>
|
</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);
|
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 {
|
.displayOptions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|||||||
Reference in new issue
Block a user