Skip to content

Commit 7893ca1

Browse files
authored
Add GraphQL Day NYC schedule (#2407)
Adds the full May 13 talk lineup to the NYC event page (6 sessions, 7 speakers), following the same post-event schedule treatment introduced for Singapore. Extracts a shared EventScheduleSection component from the duplicated amsterdam/schedule-section.tsx and singapore/schedule-section.tsx, which are deleted. Each event's schedule-data.ts now uses the shared EventSession and EventSpeaker types. The shared component adds Amsterdam's read-more toggle to all events and handles speakers without photos. Adds NYC Schedule to the footer alongside the Singapore Schedule link. Note that we still need a photo for one presenter.
1 parent ed029d8 commit 7893ca1

15 files changed

Lines changed: 341 additions & 480 deletions

src/app/day/2026/amsterdam/page.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@ import { CtaCardSection } from "../components/cta-card-section"
1010
import { MarqueeRows } from "@/app/conf/2026/components/marquee-rows"
1111
import { NavbarPlaceholder } from "../components/navbar"
1212
import { GallerySection } from "../../gallery-section"
13-
import { ScheduleSection } from "@/app/day/2026/amsterdam/schedule-section.tsx"
13+
import { EventScheduleSection } from "../components/event-schedule-section"
14+
import {
15+
amsterdamSessions,
16+
AMSTERDAM_TIMEZONE,
17+
AMSTERDAM_TIMEZONE_LABEL,
18+
tagColors,
19+
} from "./schedule-data"
1420

1521
const MARQUEE_ITEMS = [
1622
["AMSTERDAM", "JUNE 2026", "GRAPHQL DAY", "FOST", "COMMUNITY", "APIs"],
@@ -57,7 +63,12 @@ export default function AmsterdamPage() {
5763
/>
5864
<div className="gql-container gql-conf-navbar-strip text-neu-900 before:bg-white/40 before:dark:bg-blk/30">
5965
<WhyAttendSection />
60-
<ScheduleSection />
66+
<EventScheduleSection
67+
sessions={amsterdamSessions}
68+
timezone={AMSTERDAM_TIMEZONE}
69+
timezoneLabel={AMSTERDAM_TIMEZONE_LABEL}
70+
tagColors={tagColors}
71+
/>
6172
<EventPartnersSection />
6273
<GallerySection moving />
6374
<CtaCardSection

src/app/day/2026/amsterdam/schedule-data.ts

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,15 @@
1-
import type { StaticImageData } from "next/image"
2-
31
import anNgoAvatar from "./speakers/an-ngo.webp"
42
import christianErnstAvatar from "./speakers/christian-ernst.webp"
53
import jensNeuseAvatar from "./speakers/jens-neuse.webp"
64
import martinBonninAvatar from "./speakers/martin-bonnin.webp"
75
import michaelStaibAvatar from "./speakers/michael-staib.webp"
86
import thoreKoritziusAvatar from "./speakers/thore-koritzius.webp"
97

10-
export interface AmsterdamSpeaker {
11-
id: number
12-
name: string
13-
company: string
14-
jobtitle: string
15-
avatar: StaticImageData
16-
socialurls: { service: string; url: string }[]
17-
}
18-
19-
export interface AmsterdamSession {
20-
id: number
21-
uuid: string
22-
title: string
23-
/** ISO 8601 in venue local time, Europe/Amsterdam. */
24-
start: string
25-
/** ISO 8601 in venue local time, Europe/Amsterdam. */
26-
end: string
27-
/** Topic tags derived from the session description. */
28-
tags: string[]
29-
/** HTML */
30-
description: string
31-
venue: string
32-
speakers: AmsterdamSpeaker[]
33-
}
8+
import type { EventSession } from "../components/event-schedule-section"
349

3510
export const AMSTERDAM_TIMEZONE = "Europe/Amsterdam"
11+
export const AMSTERDAM_TIMEZONE_LABEL =
12+
"All times in Amsterdam Time (CEST, UTC+2)"
3613

3714
/** Color per topic, picked to read clearly against the cream/dark backgrounds. */
3815
export const tagColors: Record<string, string> = {
@@ -46,7 +23,7 @@ export const tagColors: Record<string, string> = {
4623
Observability: "#1a5b77",
4724
}
4825

49-
export const amsterdamSessions: AmsterdamSession[] = [
26+
export const amsterdamSessions: EventSession[] = [
5027
{
5128
id: 3224,
5229
uuid: "80952503-07dd-4e31-acaf-b9e400f55126",

src/app/day/2026/amsterdam/schedule-section.tsx renamed to src/app/day/2026/components/event-schedule-section.tsx

Lines changed: 87 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { useState } from "react"
44
import Image from "next/image"
5+
import type { StaticImageData } from "next/image"
56
import clsx from "clsx"
67

78
import { Tag } from "@/app/conf/_design-system/tag"
@@ -13,29 +14,43 @@ import {
1314
SocialIconType,
1415
} from "@/app/conf/_design-system/social-icon"
1516
import { formatDescription } from "@/app/conf/2026/schedule/[id]/format-description"
16-
import ArrowDownIcon from "@/app/conf/_design-system/pixelarticons/arrow-down.svg?svgr"
1717

18-
import {
19-
AmsterdamSession,
20-
AmsterdamSpeaker,
21-
amsterdamSessions,
22-
tagColors,
23-
} from "./schedule-data"
24-
25-
const TIME_RANGE = new Intl.DateTimeFormat("en-US", {
26-
hour: "2-digit",
27-
minute: "2-digit",
28-
hour12: false,
29-
timeZone: "Europe/Amsterdam",
30-
})
18+
export interface EventSpeaker {
19+
id: number
20+
name: string
21+
company: string
22+
jobtitle: string
23+
avatar?: StaticImageData
24+
socialurls: { service: string; url: string }[]
25+
}
3126

32-
const DATE_FORMAT = new Intl.DateTimeFormat("en-US", {
33-
day: "numeric",
34-
month: "long",
35-
timeZone: "Europe/Amsterdam",
36-
})
27+
export interface EventSession {
28+
id: number
29+
uuid: string
30+
title: string
31+
/** ISO 8601 in venue local time */
32+
start: string
33+
/** ISO 8601 in venue local time */
34+
end: string
35+
/** Topic tags derived from the session description. */
36+
tags: string[]
37+
/** HTML */
38+
description: string
39+
venue: string
40+
speakers: EventSpeaker[]
41+
}
3742

38-
export function ScheduleSection() {
43+
export function EventScheduleSection({
44+
sessions,
45+
timezone,
46+
timezoneLabel,
47+
tagColors,
48+
}: {
49+
sessions: EventSession[]
50+
timezone: string
51+
timezoneLabel: string
52+
tagColors: Record<string, string>
53+
}) {
3954
return (
4055
<section
4156
id="schedule"
@@ -46,16 +61,16 @@ export function ScheduleSection() {
4661
<div className="border-neu-200 dark:border-neu-100 xs:border-x">
4762
<div className="flex flex-wrap items-baseline justify-between gap-4 px-2 py-8 sm:px-3 lg:py-12 2xl:py-16">
4863
<h2 className="typography-h2">Schedule</h2>
49-
<p className="typography-body-md text-neu-700">
50-
All times in Amsterdam Time (CET, UTC+2)
51-
</p>
64+
<p className="typography-body-md text-neu-700">{timezoneLabel}</p>
5265
</div>
5366

54-
{amsterdamSessions.map((session, i) => (
67+
{sessions.map((session, i) => (
5568
<SessionBlock
5669
key={session.id}
5770
session={session}
5871
isFirst={i === 0}
72+
timezone={timezone}
73+
tagColors={tagColors}
5974
/>
6075
))}
6176
</div>
@@ -68,21 +83,27 @@ export function ScheduleSection() {
6883
function SessionBlock({
6984
session,
7085
isFirst,
86+
timezone,
87+
tagColors,
7188
}: {
72-
session: AmsterdamSession
89+
session: EventSession
7390
isFirst: boolean
91+
timezone: string
92+
tagColors: Record<string, string>
7493
}) {
75-
// On xl+ with a single speaker we slot the card next to the last two
76-
// paragraphs of the description so it sits in the bottom-right corner.
77-
// Multi-speaker sessions keep the regular "speakers below" layout.
7894
const sideSpeaker = session.speakers.length === 1 ? session.speakers[0] : null
7995

8096
return (
8197
<article>
8298
<Hr
8399
className={isFirst ? "mt-8 lg:mt-12 xl:mt-0" : "mt-12 lg:mt-16 xl:mt-0"}
84100
/>
85-
<SessionHeader session={session} className="px-2 py-8 sm:px-3 lg:py-12" />
101+
<SessionHeader
102+
session={session}
103+
timezone={timezone}
104+
tagColors={tagColors}
105+
className="px-2 py-8 sm:px-3 lg:py-12"
106+
/>
86107
{session.description && (
87108
<>
88109
<Hr className="mt-0 lg:mt-10 xl:mt-0 2xl:mt-16" />
@@ -110,7 +131,7 @@ function SessionDescription({
110131
sideSpeaker,
111132
}: {
112133
description: string
113-
sideSpeaker: AmsterdamSpeaker | null
134+
sideSpeaker: EventSpeaker | null
114135
}) {
115136
const [expanded, setExpanded] = useState(false)
116137
const paragraphs = parseParagraphs(description)
@@ -165,12 +186,6 @@ function SessionDescription({
165186
)
166187
}
167188

168-
/**
169-
* Split FOST description HTML (a sequence of `<p>...</p>` blocks) into the
170-
* inner HTML of each paragraph so we can render them as real React `<p>`
171-
* siblings — needed so we can splice the speaker card in alongside the last
172-
* couple of paragraphs at xl+.
173-
*/
174189
function parseParagraphs(html: string): string[] {
175190
const formatted = formatDescription(html)
176191
const matches = formatted.match(/<p>[\s\S]*?<\/p>/g)
@@ -180,11 +195,26 @@ function parseParagraphs(html: string): string[] {
180195

181196
function SessionHeader({
182197
session,
198+
timezone,
199+
tagColors,
183200
className,
184201
}: {
185-
session: AmsterdamSession
202+
session: EventSession
203+
timezone: string
204+
tagColors: Record<string, string>
186205
className?: string
187206
}) {
207+
const timeRange = new Intl.DateTimeFormat("en-US", {
208+
hour: "2-digit",
209+
minute: "2-digit",
210+
hour12: false,
211+
timeZone: timezone,
212+
})
213+
const dateFormat = new Intl.DateTimeFormat("en-US", {
214+
day: "numeric",
215+
month: "long",
216+
timeZone: timezone,
217+
})
188218
const start = new Date(session.start)
189219
const end = new Date(session.end)
190220

@@ -196,9 +226,9 @@ function SessionHeader({
196226
<div className="flex items-center gap-2">
197227
<CalendarIcon className="size-5 text-sec-darker dark:text-sec-light/90 sm:size-6" />
198228
<time dateTime={session.start}>
199-
{DATE_FORMAT.format(start)}
229+
{dateFormat.format(start)}
200230
{", "}
201-
{TIME_RANGE.formatRange(start, end)}
231+
{timeRange.formatRange(start, end)}
202232
</time>
203233
</div>
204234
{session.venue && (
@@ -229,7 +259,7 @@ function SessionSpeakers({
229259
speakers,
230260
className,
231261
}: {
232-
speakers: AmsterdamSpeaker[]
262+
speakers: EventSpeaker[]
233263
className?: string
234264
}) {
235265
return (
@@ -265,7 +295,7 @@ function SpeakerCard({
265295
speaker,
266296
index,
267297
}: {
268-
speaker: AmsterdamSpeaker
298+
speaker: EventSpeaker
269299
index: number
270300
}) {
271301
const variant = STRIPE_VARIANTS[index % STRIPE_VARIANTS.length]
@@ -280,15 +310,21 @@ function SpeakerCard({
280310
<article className="group relative overflow-hidden border border-t-0 border-neu-200 bg-transparent @container dark:border-neu-100">
281311
<div className="flex h-full flex-col gap-4 p-4 @[420px]:flex-row md:gap-6 md:p-6">
282312
<div className="relative aspect-square h-full overflow-hidden @[420px]:w-[176px] @[420px]:shrink-0">
283-
<div className="absolute inset-0 z-[1] bg-sec-light mix-blend-multiply" />
284-
<Image
285-
src={speaker.avatar}
286-
alt=""
287-
width={176}
288-
height={176}
289-
placeholder="blur"
290-
className="size-full object-cover saturate-[.1]"
291-
/>
313+
{speaker.avatar && (
314+
<div className="absolute inset-0 z-[1] bg-sec-light mix-blend-multiply" />
315+
)}
316+
{speaker.avatar ? (
317+
<Image
318+
src={speaker.avatar}
319+
alt=""
320+
width={176}
321+
height={176}
322+
placeholder="blur"
323+
className="size-full object-cover saturate-[.1]"
324+
/>
325+
) : (
326+
<div className="size-full bg-neu-200 dark:bg-neu-100" />
327+
)}
292328
<div
293329
role="presentation"
294330
className="pointer-events-none absolute inset-0 inset-y-[-20px]"
@@ -322,11 +358,7 @@ function SpeakerCard({
322358
)
323359
}
324360

325-
function SpeakerSocialLinks({
326-
links,
327-
}: {
328-
links: AmsterdamSpeaker["socialurls"]
329-
}) {
361+
function SpeakerSocialLinks({ links }: { links: EventSpeaker["socialurls"] }) {
330362
const ordered = SocialIconType.all
331363
.map(service =>
332364
links.find(l => l.service.toLowerCase() === service.toLowerCase()),

0 commit comments

Comments
 (0)