Skip to content

Commit 4455b81

Browse files
committed
Add session card, work around Sched rate limit
1 parent 280ef2f commit 4455b81

File tree

8 files changed

+290
-108
lines changed

8 files changed

+290
-108
lines changed

src/app/(development)/workroom/page.tsx

Lines changed: 67 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,80 @@
11
import SpeakerOpengraphImage from "@/app/conf/2025/components/speaker-opengraph-image"
2+
import ScheduleOpengraphImage from "@/app/conf/2025/components/session-opengraph-image"
3+
import { SchedSpeaker } from "@/app/conf/2023/types"
24

35
/**
46
* This is cheaper than maintaining a Storybook config.
57
*/
68
export default function WorkroomPage() {
9+
const enisdenjo: SchedSpeaker = {
10+
name: "Denis Badurina",
11+
username: "enisdenjo",
12+
avatar: "https://github.com/enisdenjo.png",
13+
company: "The Guild",
14+
position: "Software Architect",
15+
about:
16+
"Denis is a software architect at The Guild. He is a passionate about GraphQL and the GraphQL ecosystem.",
17+
role: "speaker",
18+
socialurls: [],
19+
year: "2025",
20+
}
21+
22+
const saihaj: SchedSpeaker = {
23+
name: "Saihajpreet Singh",
24+
username: "saihaj",
25+
avatar: "https://github.com/saihaj.png",
26+
company: "The Guild",
27+
position: "Head of Growth & Product",
28+
about:
29+
"I'm an engineer focused on building developer tools, infrastructure, and application solutions, while experimenting with practical AI applications. Always accelerating efficiently.",
30+
role: "speaker",
31+
socialurls: [],
32+
year: "2025",
33+
}
34+
735
return (
8-
<main>
36+
<main className="gql-conf-section gql-conf-container [&>p]:pt-8 [&>p]:font-mono [&>p]:text-sm [&>p]:text-neu-600">
37+
<p>SpeakerOpengraphImage</p>
938
<SpeakerOpengraphImage
10-
speaker={{
11-
name: "Denis Badurina",
12-
username: "enisdenjo",
13-
avatar: "https://github.com/enisdenjo.png",
14-
company: "The Guild",
15-
position: "Software Architect",
16-
about:
17-
"Denis is a software architect at The Guild. He is a passionate about GraphQL and the GraphQL ecosystem.",
18-
role: "speaker",
19-
socialurls: [],
20-
year: "2025",
39+
speaker={enisdenjo}
40+
date="September 8-10"
41+
year="2025"
42+
location="Amsterdam, Netherlands"
43+
/>
44+
45+
<p>ScheduleOpengraphImage / no speakers</p>
46+
<ScheduleOpengraphImage
47+
session={{
48+
name: "Welcome & Opening Remarks",
49+
speakers: [],
50+
event_type: "",
51+
}}
52+
date="September 8-10"
53+
year="2025"
54+
location="Amsterdam, Netherlands"
55+
/>
56+
57+
<p>ScheduleOpengraphImage / single speaker</p>
58+
<ScheduleOpengraphImage
59+
session={{
60+
name: "The State of Distributed GraphQL",
61+
speakers: [enisdenjo],
62+
event_type: "Keynote Sessions",
63+
}}
64+
date="September 8-10"
65+
year="2025"
66+
location="Amsterdam, Netherlands"
67+
/>
68+
69+
<p>ScheduleOpengraphImage / multiple speakers</p>
70+
<ScheduleOpengraphImage
71+
session={{
72+
name: "TSC Panel",
73+
speakers: [enisdenjo, saihaj],
74+
event_type: "Keynote Sessions",
2175
}}
2276
date="September 8-10"
77+
year="2025"
2378
location="Amsterdam, Netherlands"
2479
/>
2580
</main>

src/app/conf/2024/_data.ts

Lines changed: 4 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,18 @@
11
import "server-only"
22
import { stripHtml } from "string-strip-html"
33
import { SchedSpeaker, ScheduleSession } from "@/app/conf/2023/types"
4-
import pLimit from "p-limit"
54

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-
cache: "force-cache",
15-
})
16-
const data = await response.json()
17-
return data
18-
} catch (error) {
19-
throw new Error(
20-
`Error fetching data from ${url}: ${(error as Error).message || (error as Error).toString()}`,
21-
)
22-
}
23-
}
5+
import { fetchData } from "../_api/sched-client"
246

257
const token = process.env.SCHED_ACCESS_TOKEN_2024
268

27-
async function getUsernames(): Promise<string[]> {
28-
const response = await fetchData<{ username: string }[]>(
29-
`https://graphqlconf2024.sched.com/api/user/list?api_key=${token}&format=json&fields=username`,
30-
)
31-
return response.map(user => user.username)
32-
}
33-
34-
const limit = pLimit(40) // rate limit is 30req/min
35-
369
async function getSpeakers(): Promise<SchedSpeaker[]> {
37-
const usernames = await getUsernames()
38-
39-
const users = await Promise.all(
40-
usernames.map(username =>
41-
limit(() => {
42-
return fetchData<SchedSpeaker>(
43-
`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`,
44-
)
45-
}),
46-
),
10+
const users = await fetchData<SchedSpeaker[]>(
11+
`https://graphqlconf2024.sched.com/api/user/list?api_key=${token}&format=json&fields=username,company,position,name,about,location,url,avatar,role,socialurls`,
4712
)
4813

4914
const result = users
50-
.filter(speaker => speaker.role.includes("speaker"))
15+
.filter(user => user.role.includes("speaker"))
5116
.map(user => {
5217
return {
5318
...user,

src/app/conf/2025/_data.ts

Lines changed: 16 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import "server-only"
22
import { stripHtml } from "string-strip-html"
33
import { SchedSpeaker, ScheduleSession } from "@/app/conf/2023/types"
4-
import pLimit from "p-limit"
54

6-
const USE_2025 = false
5+
import { fetchData } from "../_api/sched-client"
6+
import { speakers as speakers2024 } from "../2024/_data"
7+
import { speakers as speakers2023 } from "../2023/_data"
8+
9+
const USE_2025 = true
710

811
const apiUrl = USE_2025
912
? "https://graphqlconf2025.sched.com/api"
@@ -13,45 +16,9 @@ const token = USE_2025
1316
? process.env.SCHED_ACCESS_TOKEN_2025
1417
: process.env.SCHED_ACCESS_TOKEN_2024
1518

16-
async function fetchData<T>(url: string): Promise<T> {
17-
try {
18-
const response = await fetch(url, {
19-
method: "POST",
20-
headers: {
21-
"Content-Type": "application/json",
22-
"User-Agent": "GraphQL Conf / GraphQL Foundation",
23-
},
24-
cache: "force-cache",
25-
})
26-
const data = await response.json()
27-
return data
28-
} catch (error) {
29-
throw new Error(
30-
`Error fetching data from ${url}: ${(error as Error).message || (error as Error).toString()}`,
31-
)
32-
}
33-
}
34-
35-
async function getUsernames(): Promise<string[]> {
36-
const response = await fetchData<{ username: string }[]>(
37-
`${apiUrl}/user/list?api_key=${token}&format=json&fields=username`,
38-
)
39-
return response.map(user => user.username)
40-
}
41-
42-
const limit = pLimit(40) // rate limit is 30req/min
43-
4419
async function getSpeakers(): Promise<SchedSpeaker[]> {
45-
const usernames = await getUsernames()
46-
47-
const users = await Promise.all(
48-
usernames.map(username =>
49-
limit(() => {
50-
return fetchData<SchedSpeaker>(
51-
`${apiUrl}/user/get?api_key=${token}&by=username&term=${username}&format=json&fields=username,company,position,name,about,location,url,avatar,role,socialurls`,
52-
)
53-
}),
54-
),
20+
const users = await fetchData<SchedSpeaker[]>(
21+
`${apiUrl}/user/list?api_key=${token}&format=json&fields=username,company,position,name,about,location,url,avatar,role,socialurls`,
5522
)
5623

5724
const result = users
@@ -120,9 +87,6 @@ for (const session of schedule) {
12087

12188
export const returningSpeakers = new Set<SpeakerUsername>()
12289

123-
import { speakers as speakers2024 } from "../2024/_data"
124-
import { speakers as speakers2023 } from "../2023/_data"
125-
12690
for (const { username } of speakers2024) {
12791
if (speakerSessions.has(username)) {
12892
returningSpeakers.add(username)
@@ -134,3 +98,12 @@ for (const { username } of speakers2023) {
13498
returningSpeakers.add(username)
13599
}
136100
}
101+
102+
const longestSessionName = schedule.reduce((max, session) => {
103+
if (session.name.length > max.length) {
104+
return session.name
105+
}
106+
return max
107+
}, "")
108+
109+
console.log({ longestSessionName })
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import type { ScheduleSession } from "@/app/conf/2025/schedule/_components/session-list"
2+
import type { SchedSpeaker } from "@/app/conf/2023/types"
3+
import { ConferenceOpengraphImageHeader } from "./speaker-opengraph-image"
4+
import { getEventTitle } from "../utils"
5+
import { formatSpeakerPosition } from "./format-speaker-position"
6+
7+
interface ScheduleOpengraphImageProps
8+
extends React.HTMLAttributes<HTMLElement> {
9+
session: Pick<ScheduleSession, "name" | "speakers" | "event_type">
10+
date: string
11+
year: string
12+
location: string
13+
}
14+
15+
function isString(x: unknown): x is string {
16+
return Object.prototype.toString.call(x) === "[object String]"
17+
}
18+
19+
export default function ScheduleOpengraphImage({
20+
session,
21+
date,
22+
location,
23+
year,
24+
...rest
25+
}: ScheduleOpengraphImageProps) {
26+
const speakers = session.speakers
27+
? isString(session.speakers)
28+
? (session.speakers as string)
29+
.split(",")
30+
.map(name => ({ name, username: "", avatar: "" }))
31+
: (session.speakers as SchedSpeaker[])
32+
: []
33+
34+
const eventTitle = getEventTitle(
35+
session,
36+
speakers.map(s => s.name),
37+
)
38+
39+
return (
40+
<article
41+
className="flex h-[630px] w-[1200px] flex-col overflow-hidden border-2 border-neu-300 bg-neu-100"
42+
{...rest}
43+
>
44+
<ConferenceOpengraphImageHeader
45+
year={year}
46+
date={date}
47+
location={location}
48+
/>
49+
50+
<div className="flex flex-1 flex-col justify-between p-10">
51+
<div className="flex flex-col gap-10">
52+
<h3 className="m-0 font-sans text-[72px] font-normal leading-tight text-neu-900">
53+
{eventTitle}
54+
</h3>
55+
</div>
56+
57+
{speakers.length === 1 && speakers[0] && (
58+
<div className="flex items-center gap-10">
59+
{speakers[0]?.avatar && (
60+
<div className="relative overflow-hidden">
61+
<div className="absolute inset-0 z-[1] bg-sec-lighter mix-blend-multiply" />
62+
<img
63+
src={speakers[0].avatar}
64+
alt=""
65+
className="size-[120px] object-cover"
66+
width={120}
67+
height={120}
68+
/>
69+
</div>
70+
)}
71+
<div className="flex flex-col gap-4">
72+
<h4 className="m-0 font-sans text-[48px] font-normal leading-tight text-neu-900">
73+
{speakers[0].name}
74+
</h4>
75+
{"company" in speakers[0] && speakers[0].company && (
76+
<span className="font-sans text-[32px] font-normal leading-tight text-neu-700">
77+
{formatSpeakerPosition(speakers[0] as SchedSpeaker)}
78+
</span>
79+
)}
80+
</div>
81+
</div>
82+
)}
83+
84+
{speakers.length > 1 && (
85+
<div className="flex flex-col gap-4">
86+
<h4 className="m-0 font-sans text-[48px] font-normal leading-tight text-neu-900">
87+
{speakers.map(s => s.name).join(", ")}
88+
</h4>
89+
</div>
90+
)}
91+
</div>
92+
93+
{session.event_type && (
94+
<footer className="flex items-center border-t-2 border-neu-300 px-16 py-8 pl-10">
95+
<span className="font-mono text-2xl font-normal uppercase leading-none text-neu-900">
96+
{session.event_type}
97+
</span>
98+
</footer>
99+
)}
100+
</article>
101+
)
102+
}

src/app/conf/2025/components/speaker-opengraph-image.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,24 @@ const RIGHT_COLUMN_WIDTH_PX = 476
99
interface SpeakerOpengraphImageProps extends React.HTMLAttributes<HTMLElement> {
1010
speaker: SchedSpeaker
1111
date: string
12+
year: string
1213
location: string
1314
}
1415

1516
export default function SpeakerOpengraphImage({
1617
speaker,
1718
date,
19+
year,
1820
location,
1921
...rest
2022
}: SpeakerOpengraphImageProps) {
2123
return (
2224
<article
23-
className="flex h-[630px] w-[1200px] flex-col overflow-hidden border border-neu-300 bg-neu-50"
25+
className="flex h-[630px] w-[1200px] flex-col overflow-hidden border-2 border-neu-300 bg-neu-100"
2426
{...rest}
2527
>
2628
<ConferenceOpengraphImageHeader
27-
year={speaker.year}
29+
year={year}
2830
date={date}
2931
location={location}
3032
/>
@@ -79,8 +81,8 @@ export function ConferenceOpengraphImageHeader({
7981
location: string
8082
}) {
8183
return (
82-
<header className="flex items-center border-b border-neu-300">
83-
<div className="flex flex-1 items-center gap-6 border-r border-neu-300 p-10 pr-16">
84+
<header className="flex items-center border-b-2 border-neu-300">
85+
<div className="flex flex-1 items-center gap-6 border-r-2 border-neu-300 p-10 pr-16">
8486
<div className="flex items-center gap-4">
8587
<div className="font-mono font-normal uppercase leading-none text-neu-900">
8688
<div className="flex h-[74px] items-center gap-4 text-[40px]/none uppercase">
@@ -102,7 +104,7 @@ export function ConferenceOpengraphImageHeader({
102104
width: RIGHT_COLUMN_WIDTH_PX,
103105
}}
104106
>
105-
<div className="flex items-center gap-6 border-b border-neu-300 px-6 py-[26px]">
107+
<div className="flex items-center gap-6 border-b-2 border-neu-300 px-6 py-[26px]">
106108
<div className="flex items-center gap-2">
107109
<CalendarIcon
108110
width="24"

0 commit comments

Comments
 (0)