Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions src/app/day/2026/amsterdam/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ import { CtaCardSection } from "../components/cta-card-section"
import { MarqueeRows } from "@/app/conf/2026/components/marquee-rows"
import { NavbarPlaceholder } from "../components/navbar"
import { GallerySection } from "../../gallery-section"
import { ScheduleSection } from "@/app/day/2026/amsterdam/schedule-section.tsx"
import { EventScheduleSection } from "../components/event-schedule-section"
import {
amsterdamSessions,
AMSTERDAM_TIMEZONE,
AMSTERDAM_TIMEZONE_LABEL,
tagColors,
} from "./schedule-data"

const MARQUEE_ITEMS = [
["AMSTERDAM", "JUNE 2026", "GRAPHQL DAY", "FOST", "COMMUNITY", "APIs"],
Expand Down Expand Up @@ -57,7 +63,12 @@ export default function AmsterdamPage() {
/>
<div className="gql-container gql-conf-navbar-strip text-neu-900 before:bg-white/40 before:dark:bg-blk/30">
<WhyAttendSection />
<ScheduleSection />
<EventScheduleSection
sessions={amsterdamSessions}
timezone={AMSTERDAM_TIMEZONE}
timezoneLabel={AMSTERDAM_TIMEZONE_LABEL}
tagColors={tagColors}
/>
<EventPartnersSection />
<GallerySection moving />
<CtaCardSection
Expand Down
31 changes: 4 additions & 27 deletions src/app/day/2026/amsterdam/schedule-data.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,15 @@
import type { StaticImageData } from "next/image"

import anNgoAvatar from "./speakers/an-ngo.webp"
import christianErnstAvatar from "./speakers/christian-ernst.webp"
import jensNeuseAvatar from "./speakers/jens-neuse.webp"
import martinBonninAvatar from "./speakers/martin-bonnin.webp"
import michaelStaibAvatar from "./speakers/michael-staib.webp"
import thoreKoritziusAvatar from "./speakers/thore-koritzius.webp"

export interface AmsterdamSpeaker {
id: number
name: string
company: string
jobtitle: string
avatar: StaticImageData
socialurls: { service: string; url: string }[]
}

export interface AmsterdamSession {
id: number
uuid: string
title: string
/** ISO 8601 in venue local time, Europe/Amsterdam. */
start: string
/** ISO 8601 in venue local time, Europe/Amsterdam. */
end: string
/** Topic tags derived from the session description. */
tags: string[]
/** HTML */
description: string
venue: string
speakers: AmsterdamSpeaker[]
}
import type { EventSession } from "../components/event-schedule-section"

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

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

export const amsterdamSessions: AmsterdamSession[] = [
export const amsterdamSessions: EventSession[] = [
{
id: 3224,
uuid: "80952503-07dd-4e31-acaf-b9e400f55126",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { useState } from "react"
import Image from "next/image"
import type { StaticImageData } from "next/image"
import clsx from "clsx"

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

import {
AmsterdamSession,
AmsterdamSpeaker,
amsterdamSessions,
tagColors,
} from "./schedule-data"

const TIME_RANGE = new Intl.DateTimeFormat("en-US", {
hour: "2-digit",
minute: "2-digit",
hour12: false,
timeZone: "Europe/Amsterdam",
})
export interface EventSpeaker {
id: number
name: string
company: string
jobtitle: string
avatar?: StaticImageData
socialurls: { service: string; url: string }[]
}

const DATE_FORMAT = new Intl.DateTimeFormat("en-US", {
day: "numeric",
month: "long",
timeZone: "Europe/Amsterdam",
})
export interface EventSession {
id: number
uuid: string
title: string
/** ISO 8601 in venue local time */
start: string
/** ISO 8601 in venue local time */
end: string
/** Topic tags derived from the session description. */
tags: string[]
/** HTML */
description: string
venue: string
speakers: EventSpeaker[]
}

export function ScheduleSection() {
export function EventScheduleSection({
sessions,
timezone,
timezoneLabel,
tagColors,
}: {
sessions: EventSession[]
timezone: string
timezoneLabel: string
tagColors: Record<string, string>
}) {
return (
<section
id="schedule"
Expand All @@ -46,16 +61,16 @@ export function ScheduleSection() {
<div className="border-neu-200 dark:border-neu-100 xs:border-x">
<div className="flex flex-wrap items-baseline justify-between gap-4 px-2 py-8 sm:px-3 lg:py-12 2xl:py-16">
<h2 className="typography-h2">Schedule</h2>
<p className="typography-body-md text-neu-700">
All times in Amsterdam Time (CET, UTC+2)
</p>
<p className="typography-body-md text-neu-700">{timezoneLabel}</p>
</div>

{amsterdamSessions.map((session, i) => (
{sessions.map((session, i) => (
<SessionBlock
key={session.id}
session={session}
isFirst={i === 0}
timezone={timezone}
tagColors={tagColors}
/>
))}
</div>
Expand All @@ -68,21 +83,27 @@ export function ScheduleSection() {
function SessionBlock({
session,
isFirst,
timezone,
tagColors,
}: {
session: AmsterdamSession
session: EventSession
isFirst: boolean
timezone: string
tagColors: Record<string, string>
}) {
// On xl+ with a single speaker we slot the card next to the last two
// paragraphs of the description so it sits in the bottom-right corner.
// Multi-speaker sessions keep the regular "speakers below" layout.
const sideSpeaker = session.speakers.length === 1 ? session.speakers[0] : null

return (
<article>
<Hr
className={isFirst ? "mt-8 lg:mt-12 xl:mt-0" : "mt-12 lg:mt-16 xl:mt-0"}
/>
<SessionHeader session={session} className="px-2 py-8 sm:px-3 lg:py-12" />
<SessionHeader
session={session}
timezone={timezone}
tagColors={tagColors}
className="px-2 py-8 sm:px-3 lg:py-12"
/>
{session.description && (
<>
<Hr className="mt-0 lg:mt-10 xl:mt-0 2xl:mt-16" />
Expand Down Expand Up @@ -110,7 +131,7 @@ function SessionDescription({
sideSpeaker,
}: {
description: string
sideSpeaker: AmsterdamSpeaker | null
sideSpeaker: EventSpeaker | null
}) {
const [expanded, setExpanded] = useState(false)
const paragraphs = parseParagraphs(description)
Expand Down Expand Up @@ -165,12 +186,6 @@ function SessionDescription({
)
}

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

function SessionHeader({
session,
timezone,
tagColors,
className,
}: {
session: AmsterdamSession
session: EventSession
timezone: string
tagColors: Record<string, string>
className?: string
}) {
const timeRange = new Intl.DateTimeFormat("en-US", {
hour: "2-digit",
minute: "2-digit",
hour12: false,
timeZone: timezone,
})
const dateFormat = new Intl.DateTimeFormat("en-US", {
day: "numeric",
month: "long",
timeZone: timezone,
})
const start = new Date(session.start)
const end = new Date(session.end)

Expand All @@ -196,9 +226,9 @@ function SessionHeader({
<div className="flex items-center gap-2">
<CalendarIcon className="size-5 text-sec-darker dark:text-sec-light/90 sm:size-6" />
<time dateTime={session.start}>
{DATE_FORMAT.format(start)}
{dateFormat.format(start)}
{", "}
{TIME_RANGE.formatRange(start, end)}
{timeRange.formatRange(start, end)}
</time>
</div>
{session.venue && (
Expand Down Expand Up @@ -229,7 +259,7 @@ function SessionSpeakers({
speakers,
className,
}: {
speakers: AmsterdamSpeaker[]
speakers: EventSpeaker[]
className?: string
}) {
return (
Expand Down Expand Up @@ -265,7 +295,7 @@ function SpeakerCard({
speaker,
index,
}: {
speaker: AmsterdamSpeaker
speaker: EventSpeaker
index: number
}) {
const variant = STRIPE_VARIANTS[index % STRIPE_VARIANTS.length]
Expand All @@ -280,15 +310,21 @@ function SpeakerCard({
<article className="group relative overflow-hidden border border-t-0 border-neu-200 bg-transparent @container dark:border-neu-100">
<div className="flex h-full flex-col gap-4 p-4 @[420px]:flex-row md:gap-6 md:p-6">
<div className="relative aspect-square h-full overflow-hidden @[420px]:w-[176px] @[420px]:shrink-0">
<div className="absolute inset-0 z-[1] bg-sec-light mix-blend-multiply" />
<Image
src={speaker.avatar}
alt=""
width={176}
height={176}
placeholder="blur"
className="size-full object-cover saturate-[.1]"
/>
{speaker.avatar && (
<div className="absolute inset-0 z-[1] bg-sec-light mix-blend-multiply" />
)}
{speaker.avatar ? (
<Image
src={speaker.avatar}
alt=""
width={176}
height={176}
placeholder="blur"
className="size-full object-cover saturate-[.1]"
/>
) : (
<div className="size-full bg-neu-200 dark:bg-neu-100" />
)}
<div
role="presentation"
className="pointer-events-none absolute inset-0 inset-y-[-20px]"
Expand Down Expand Up @@ -322,11 +358,7 @@ function SpeakerCard({
)
}

function SpeakerSocialLinks({
links,
}: {
links: AmsterdamSpeaker["socialurls"]
}) {
function SpeakerSocialLinks({ links }: { links: EventSpeaker["socialurls"] }) {
const ordered = SocialIconType.all
.map(service =>
links.find(l => l.service.toLowerCase() === service.toLowerCase()),
Expand Down
Loading
Loading