Skip to content

Commit 538a4f4

Browse files
committed
Draft new Speakers and Schedule pages
1 parent a10a23e commit 538a4f4

File tree

18 files changed

+1395
-18
lines changed

18 files changed

+1395
-18
lines changed

src/app/conf/2025/_data.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import "server-only"
2+
import { stripHtml } from "string-strip-html"
3+
import { SchedSpeaker, ScheduleSession } from "@/app/conf/2023/types"
4+
import pLimit from "p-limit"
5+
6+
async function fetchData<T>(url: string): Promise<T> {
7+
try {
8+
const response = await fetch(url, {
9+
method: "POST",
10+
headers: {
11+
"Content-Type": "application/json",
12+
"User-Agent": "GraphQL Conf / GraphQL Foundation",
13+
},
14+
})
15+
const data = await response.json()
16+
return data
17+
} catch (error) {
18+
throw new Error(
19+
`Error fetching data from ${url}: ${(error as Error).message || (error as Error).toString()}`,
20+
)
21+
}
22+
}
23+
24+
const token = process.env.SCHED_ACCESS_TOKEN_2024
25+
26+
async function getUsernames(): Promise<string[]> {
27+
const response = await fetchData<{ username: string }[]>(
28+
`https://graphqlconf2024.sched.com/api/user/list?api_key=${token}&format=json&fields=username`,
29+
)
30+
return response.map(user => user.username)
31+
}
32+
33+
const limit = pLimit(40) // rate limit is 30req/min
34+
35+
async function getSpeakers(): Promise<SchedSpeaker[]> {
36+
const usernames = await getUsernames()
37+
38+
const users = await Promise.all(
39+
usernames.map(username =>
40+
limit(() => {
41+
return fetchData<SchedSpeaker>(
42+
`https://graphqlconf2024.sched.com/api/user/get?api_key=${token}&by=username&term=${username}&format=json&fields=username,company,position,name,about,location,url,avatar,role,socialurls`,
43+
)
44+
}),
45+
),
46+
)
47+
48+
const result = users
49+
.filter(speaker => speaker.role.includes("speaker"))
50+
.map(user => {
51+
return {
52+
...user,
53+
about: stripHtml(user.about).result,
54+
}
55+
})
56+
57+
return result
58+
}
59+
60+
async function getSchedule(): Promise<ScheduleSession[]> {
61+
const sessions = await fetchData<ScheduleSession[]>(
62+
`https://graphqlconf2024.sched.com/api/session/export?api_key=${token}&format=json`,
63+
)
64+
65+
const result = sessions.map(session => {
66+
const { description } = session
67+
if (description?.includes("<")) {
68+
// console.log(`Found HTML element in about field for session "${session.name}"`)
69+
}
70+
71+
return {
72+
...session,
73+
description: description && stripHtml(description).result,
74+
}
75+
})
76+
77+
return result
78+
}
79+
80+
// @ts-expect-error -- fixme
81+
export const speakers = await getSpeakers()
82+
83+
// @ts-expect-error -- fixme
84+
export const schedule = await getSchedule()

src/app/conf/2025/_videos.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export const videos: {
2+
id: string
3+
title: string
4+
}[] = [
5+
// temporary
6+
{
7+
id: "fA81OFu9BVY",
8+
title: `Top 10 GraphQL Security Checks for Every Developer - Ankita Gupta, Ankush Jain - Akto.io`,
9+
},
10+
]

src/app/conf/2025/components/hero/index.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export function Hero() {
1414
return (
1515
<article className="gql-conf-navbar-strip relative isolate flex flex-col justify-center bg-pri-base text-neu-0 selection:bg-blk/40 before:bg-white/30 dark:bg-pri-darker dark:text-neu-900 dark:selection:bg-white/40 before:dark:bg-blk/40">
1616
<article className="relative">
17-
<Stripes />
17+
<HeroStripes />
1818
<div className="gql-conf-container mx-auto flex max-w-full flex-col gap-12 overflow-hidden p-4 pt-6 sm:p-8 sm:pt-12 md:gap-12 md:bg-left md:p-12 lg:px-24 lg:pb-16 lg:pt-24">
1919
<div className="flex gap-10 max-md:flex-col md:justify-between">
2020
<h1 className="flex flex-wrap gap-2 typography-d1">
@@ -35,7 +35,7 @@ export function Hero() {
3535
</div>
3636

3737
<div className="flex flex-col gap-8">
38-
<DateAndLocation />
38+
<HeroDateAndLocation />
3939
<Button className="md:w-fit" href={GET_TICKETS_LINK}>
4040
Get your tickets
4141
</Button>
@@ -55,7 +55,7 @@ export function Hero() {
5555
)
5656
}
5757

58-
function DateAndLocation() {
58+
export function HeroDateAndLocation() {
5959
return (
6060
<div className="flex flex-col gap-4 typography-body-md md:flex-row md:gap-6">
6161
<div className="flex items-center gap-2">
@@ -77,7 +77,7 @@ const maskEven =
7777
const maskOdd =
7878
"repeating-linear-gradient(to right, black, black 12px, transparent 12px, transparent 24px)"
7979

80-
function Stripes() {
80+
export function HeroStripes() {
8181
return (
8282
<ImageLoaded
8383
role="presentation"

src/app/conf/2025/components/image-loaded.tsx

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,34 @@
33
import type { StaticImageData } from "next/image"
44
import { useEffect, useState } from "react"
55

6+
const _cache = new Map<string, HTMLImageElement>()
7+
68
export interface ImageLoadedProps extends React.HTMLAttributes<HTMLDivElement> {
79
image: string | StaticImageData
810
}
911

1012
export function ImageLoaded({ image, ...rest }: ImageLoadedProps) {
1113
const [loaded, setLoaded] = useState(false)
14+
const src = typeof image === "string" ? image : image.src
15+
16+
const alreadyLoaded = _cache.get(src)?.complete
1217

1318
useEffect(() => {
14-
const img = new Image()
15-
const src = typeof image === "string" ? image : image.src
16-
img.src = src
17-
img.onload = () => setLoaded(true)
18-
}, [image])
19+
let img: HTMLImageElement
20+
if (_cache.has(src)) {
21+
img = _cache.get(src)!
22+
if (img.complete) {
23+
setLoaded(true)
24+
} else {
25+
img.addEventListener("load", () => setLoaded(true))
26+
}
27+
} else {
28+
img = new Image()
29+
img.src = src
30+
img.addEventListener("load", () => setLoaded(true))
31+
_cache.set(src, img)
32+
}
33+
}, [src])
1934

20-
return <div data-loaded={loaded} {...rest} />
35+
return <div data-loaded={alreadyLoaded || loaded} {...rest} />
2136
}

src/app/conf/2025/components/navbar.tsx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,7 @@ export function Navbar({ links, year }: NavbarProps): ReactElement {
3636
mobileDrawerOpen ? "static" : "absolute",
3737
)}
3838
/>
39-
<div
40-
// placeholder: the colors here on `before` must match the ones on Hero `before` strip
41-
className="absolute h-[calc(var(--navbar-h)+1px)] w-full bg-pri-base before:absolute before:top-0 before:h-[calc(var(--navbar-h)+1px)] before:w-full before:bg-white/30 dark:bg-pri-darker dark:before:bg-black/40"
42-
/>
39+
<NavbarPlaceholder className="bg-pri-base before:bg-white/30 dark:bg-pri-darker dark:before:bg-black/40" />
4340
<header
4441
className={clsx(
4542
"gql-all-anchors-focusable top-0 z-10 w-full border-b border-black/60 font-mono text-neu-900 antialiased dark:border-white/80",
@@ -131,3 +128,19 @@ function BackdropBlur() {
131128
/>
132129
)
133130
}
131+
132+
export function NavbarPlaceholder({
133+
className,
134+
...rest
135+
}: React.HTMLAttributes<HTMLDivElement>) {
136+
return (
137+
<div
138+
// placeholder: the colors here on `before` must match the ones on Hero `before` strip
139+
className={clsx(
140+
"absolute h-[calc(var(--navbar-h)+1px)] w-full before:absolute before:top-0 before:h-[calc(var(--navbar-h)+1px)] before:w-full",
141+
className,
142+
)}
143+
{...rest}
144+
/>
145+
)
146+
}

src/app/conf/2025/layout.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@ export default function Layout({
4040
<Navbar
4141
year={2025}
4242
links={[
43-
{ children: "Sponsor", href: "/conf/2025/#sponsors" },
44-
{ children: "Submit to Speak", href: "/conf/2025/#speakers" },
45-
{ children: "Register", href: "/conf/2025/#register" },
43+
{ children: "Schedule", href: "/conf/2025/schedule" },
44+
{ children: "Speakers", href: "/conf/2025/speakers" },
45+
{ children: "Sponsors", href: "/conf/2025/#sponsors" },
4646
{ children: "Recap", href: "/conf/2024" },
4747
{ children: "Resources", href: "/conf/2025/resources" },
4848
{ children: "FAQ", href: "/conf/2025/#faq" },

0 commit comments

Comments
 (0)