Wrap next/link in a shared Link component that ships prefetch=false by default, so no route is ever prefetched (viewport or hover) anymore, and drop the DNS prefetch hint. Removes hidden background requests that were the source of intermittent issues.
179 lines
4.6 KiB
TypeScript
179 lines
4.6 KiB
TypeScript
import { count, desc, eq, inArray } from "drizzle-orm";
|
|
import Image from "next/image";
|
|
import { getTranslations } from "next-intl/server";
|
|
import Link from "@/components/link";
|
|
import { ContentCard, EmptyState } from "@/components/public/ui";
|
|
import {
|
|
db,
|
|
WebsiteEvent,
|
|
WebsiteEventRegistration,
|
|
WebsiteEventType,
|
|
} from "@/lib/db";
|
|
import { excerpt, slugify } from "@/lib/format";
|
|
import { formatDate } from "@/lib/format-date";
|
|
|
|
export const metadata = { title: "Events" };
|
|
|
|
export default async function EventsPage() {
|
|
const t = await getTranslations("pages.events");
|
|
|
|
const eventRows = await db
|
|
.select({
|
|
id: WebsiteEvent.id,
|
|
title: WebsiteEvent.title,
|
|
description: WebsiteEvent.description,
|
|
status: WebsiteEvent.status,
|
|
image: WebsiteEvent.image,
|
|
startsAt: WebsiteEvent.startsAt,
|
|
maxPlayers: WebsiteEvent.maxPlayers,
|
|
typeName: WebsiteEventType.name,
|
|
typeColor: WebsiteEventType.color,
|
|
typeSlug: WebsiteEventType.slug,
|
|
})
|
|
.from(WebsiteEvent)
|
|
.innerJoin(WebsiteEventType, eq(WebsiteEvent.typeId, WebsiteEventType.id))
|
|
.where(inArray(WebsiteEvent.status, ["published", "completed"]))
|
|
.orderBy(desc(WebsiteEvent.startsAt))
|
|
.limit(50)
|
|
.catch(() => []);
|
|
|
|
const eventIds = eventRows.map((e) => e.id);
|
|
const regCounts =
|
|
eventIds.length > 0
|
|
? await db
|
|
.select({
|
|
eventId: WebsiteEventRegistration.eventId,
|
|
value: count(),
|
|
})
|
|
.from(WebsiteEventRegistration)
|
|
.where(inArray(WebsiteEventRegistration.eventId, eventIds))
|
|
.groupBy(WebsiteEventRegistration.eventId)
|
|
.catch(() => [])
|
|
: [];
|
|
const countByEvent = new Map(
|
|
regCounts.map((r) => [r.eventId, Number(r.value)]),
|
|
);
|
|
|
|
const events = eventRows.map((event) => ({
|
|
...event,
|
|
type: {
|
|
name: event.typeName,
|
|
color: event.typeColor,
|
|
slug: event.typeSlug,
|
|
},
|
|
_count: { registrations: countByEvent.get(event.id) ?? 0 },
|
|
}));
|
|
|
|
return (
|
|
<main style={{ display: "grid", gap: "1.5rem" }}>
|
|
<ContentCard icon="📅" title={t("title")} subtitle={t("subtitle")} />
|
|
|
|
<ContentCard padded={events.length === 0}>
|
|
{events.length === 0 ? (
|
|
<EmptyState icon="📅">{t("empty")}</EmptyState>
|
|
) : (
|
|
<div className="card-grid sm-2 lg-3" style={{ padding: "1rem" }}>
|
|
{events.map((event) => {
|
|
const href = `/events/${slugify(event.title)}-${event.id}`;
|
|
const spots =
|
|
event.maxPlayers != null
|
|
? t("spots", {
|
|
count: event._count.registrations,
|
|
max: event.maxPlayers,
|
|
})
|
|
: t("registered", { count: event._count.registrations });
|
|
|
|
return (
|
|
<Link
|
|
key={event.id}
|
|
href={href}
|
|
className="card hover"
|
|
style={{
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
textDecoration: "none",
|
|
padding: 0,
|
|
overflow: "hidden",
|
|
color: "inherit",
|
|
}}
|
|
>
|
|
{event.image ? (
|
|
<div
|
|
style={{
|
|
position: "relative",
|
|
width: "100%",
|
|
aspectRatio: "16/9",
|
|
}}
|
|
>
|
|
<Image
|
|
src={event.image}
|
|
alt=""
|
|
fill
|
|
style={{ objectFit: "cover" }}
|
|
unoptimized
|
|
/>
|
|
</div>
|
|
) : (
|
|
<div
|
|
style={{
|
|
width: "100%",
|
|
aspectRatio: "16/9",
|
|
backgroundColor:
|
|
event.type.color ||
|
|
"color-mix(in srgb, var(--color-primary) 10%, var(--color-navbar))",
|
|
}}
|
|
/>
|
|
)}
|
|
<div
|
|
style={{
|
|
padding: "1rem 1.1rem",
|
|
flex: 1,
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: "0.35rem",
|
|
}}
|
|
>
|
|
<p
|
|
className="muted"
|
|
style={{ margin: 0, fontSize: "0.75rem" }}
|
|
>
|
|
{event.type.name}
|
|
{event.status === "completed"
|
|
? ` · ${t("statusCompleted")}`
|
|
: ""}
|
|
</p>
|
|
<h3 style={{ margin: 0, fontSize: "1.05rem" }}>
|
|
{event.title}
|
|
</h3>
|
|
<p
|
|
className="muted"
|
|
style={{ margin: 0, fontSize: "0.85rem" }}
|
|
>
|
|
{formatDate(event.startsAt, "datetime", "")}
|
|
</p>
|
|
<p
|
|
style={{
|
|
margin: 0,
|
|
fontSize: "0.88rem",
|
|
lineHeight: 1.5,
|
|
}}
|
|
>
|
|
{excerpt(event.description, 120)}
|
|
</p>
|
|
<p
|
|
className="muted"
|
|
style={{ margin: "auto 0 0", fontSize: "0.8rem" }}
|
|
>
|
|
{spots}
|
|
</p>
|
|
</div>
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</ContentCard>
|
|
</main>
|
|
);
|
|
}
|