Skip to content

Commit f9017e3

Browse files
committed
Extract schedule components, delete stub files and unused routes
Move schedule components from route-specific dirs to components/schedule/ for reuse across city pages. Make BackLink, ScheduleList, and useCurrentTimeMarker generic via props. Extract buildVideoIndex as explicit function. Delete empty stub files and placeholder schedule routes. Fix leading space in formatDescription class output and "hourse" typo.
1 parent 1ee9289 commit f9017e3

22 files changed

Lines changed: 275 additions & 684 deletions

src/app/day/2026/_data.ts

Lines changed: 0 additions & 6 deletions
This file was deleted.

src/app/day/2026/_videos.ts

Lines changed: 0 additions & 2 deletions
This file was deleted.

src/app/day/2026/schedule/_components/back-link.tsx renamed to src/app/day/2026/components/schedule/back-link.tsx

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,16 @@ import NextLink from "next/link"
22

33
import ArrowDownIcon from "@/app/conf/_design-system/pixelarticons/arrow-down.svg?svgr"
44

5-
export function BackLink({
6-
year,
7-
kind,
8-
}: {
9-
year: `20${number}`
10-
kind: "speakers" | "sessions" | "schedule"
11-
}) {
5+
export function BackLink({ href, label }: { href: string; label: string }) {
126
return (
137
<NextLink
14-
href={`/conf/${year}/${kind}`}
8+
href={href}
159
className="group typography-menu -m-2 inline-flex cursor-pointer items-center gap-2 p-2 text-sec-darker transition-all [text-box:trim-both_cap_alphabetic] hover:underline hover:underline-offset-4 dark:text-neu-700"
1610
>
1711
<div className="[--arrow-left-x:-1px] group-hover:animate-arrow-left group-focus:animate-arrow-left">
1812
<ArrowDownIcon className="inline-block size-4 translate-y-[-.5px] rotate-90" />
1913
</div>
20-
Back to {capitalize(kind)}
14+
{label}
2115
</NextLink>
2216
)
2317
}
24-
25-
function capitalize(str: string) {
26-
return str.substring(0, 1).toUpperCase() + str.substring(1).toLowerCase()
27-
}
File renamed without changes.

src/app/day/2026/schedule/_components/format-block-time.tsx renamed to src/app/day/2026/components/schedule/format-block-time.tsx

File renamed without changes.

src/app/day/2026/schedule/[id]/format-description.test.tsx renamed to src/app/day/2026/components/schedule/format-description.test.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ describe(formatDescription.name, () => {
99
formatDescription(
1010
`Check out Y! <a href="https://y.dev">https://y.dev</a>`,
1111
),
12-
`Check out Y! <a href="https://y.dev" rel="noopener noreferrer" target="_blank" class=" typography-link">y.dev</a>`,
12+
`Check out Y! <a href="https://y.dev" rel="noopener noreferrer" target="_blank" class="typography-link">y.dev</a>`,
1313
)
1414
})
1515

@@ -23,7 +23,7 @@ describe(formatDescription.name, () => {
2323
it("adds attributes to existing links without URL content", () => {
2424
assert.equal(
2525
formatDescription(`<a href="https://example.com">Click here</a>`),
26-
`<a href="https://example.com" rel="noopener noreferrer" target="_blank" class=" typography-link">Click here</a>`,
26+
`<a href="https://example.com" rel="noopener noreferrer" target="_blank" class="typography-link">Click here</a>`,
2727
)
2828
})
2929

@@ -32,7 +32,7 @@ describe(formatDescription.name, () => {
3232
formatDescription(
3333
`Check <a href="https://y.dev">Y site</a> and https://example.com`,
3434
),
35-
`Check <a href="https://y.dev" rel="noopener noreferrer" target="_blank" class=" typography-link">Y site</a> and <a href="https://example.com" target="_blank" rel="noopener noreferrer" class="typography-link">example.com</a>`,
35+
`Check <a href="https://y.dev" rel="noopener noreferrer" target="_blank" class="typography-link">Y site</a> and <a href="https://example.com" target="_blank" rel="noopener noreferrer" class="typography-link">example.com</a>`,
3636
)
3737
})
3838

src/app/day/2026/schedule/[id]/format-description.tsx renamed to src/app/day/2026/components/schedule/format-description.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export function formatDescription(text: string): string {
2222
}
2323

2424
if (!attrs.includes("class=")) {
25-
attrs += ' class=" typography-link"'
25+
attrs += ' class="typography-link"'
2626
} else if (!attrs.includes("typography-link")) {
2727
attrs = attrs.replace(
2828
/class\s*=\s*["']([^"']*)/gi,

src/app/day/2026/schedule/_components/schedule-list.tsx renamed to src/app/day/2026/components/schedule/schedule-list.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -107,17 +107,23 @@ function getSessionsByDay(
107107
export interface ScheduleListProps {
108108
showFilter?: boolean
109109
scheduleData: ScheduleSession[]
110-
year: `202${number}`
110+
basePath: string
111111
eventsColors: Record<string, string>
112112
filterFields: FiltersConfig
113+
conferenceStart: Date
114+
conferenceEnd: Date
115+
schedUrl?: string
113116
}
114117

115118
export function ScheduleList({
116119
showFilter = true,
117120
scheduleData,
118-
year,
121+
basePath,
119122
eventsColors,
120123
filterFields,
124+
conferenceStart,
125+
conferenceEnd,
126+
schedUrl,
121127
}: ScheduleListProps): ReactElement {
122128
const [filtersState, setFiltersState] = useState<FilterStates>(() =>
123129
FilterStates.initial(
@@ -140,12 +146,12 @@ export function ScheduleList({
140146
const firstDayIsDayZero = Object.keys(firstDay).length < 3
141147
const startIndex = firstDayIsDayZero ? 0 : 1
142148

143-
const { getTimeMarker } = useCurrentTimeMarker()
149+
const { getTimeMarker } = useCurrentTimeMarker(conferenceStart, conferenceEnd)
144150

145151
return (
146152
<>
147153
<div className="flex justify-between gap-1 max-lg:flex-col">
148-
<BookmarkOnSched year={year} />
154+
{schedUrl && <BookmarkOnSched url={schedUrl} />}
149155
<div className="flex gap-2 max-lg:mb-4">
150156
<Button
151157
href="#current-time-marker"
@@ -271,7 +277,7 @@ export function ScheduleList({
271277
<ScheduleSessionCard
272278
key={session.id}
273279
session={session}
274-
year={year}
280+
basePath={basePath}
275281
eventsColors={eventsColors}
276282
blockEnd={blockEnd}
277283
durationVisible={endTimesDiffer}
@@ -323,10 +329,10 @@ export function ScheduleList({
323329
)
324330
}
325331

326-
function BookmarkOnSched({ year }: { year: `202${number}` }) {
332+
function BookmarkOnSched({ url }: { url: string }) {
327333
return (
328334
<a
329-
href={`https://graphqlconf${year}.sched.com`}
335+
href={url}
330336
target="_blank"
331337
rel="noreferrer"
332338
className="typography-link mb-8 block w-fit decoration-neu-400 max-lg:hidden"

src/app/day/2026/schedule/_components/schedule-session-card.tsx renamed to src/app/day/2026/components/schedule/schedule-session-card.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,13 @@ function isString(x: unknown): x is string {
2727

2828
export function ScheduleSessionCard({
2929
session,
30-
year,
30+
basePath,
3131
eventsColors,
3232
blockEnd,
3333
durationVisible,
3434
}: {
3535
session: ScheduleSession
36-
year: `202${number}`
36+
basePath: string
3737
eventsColors: Record<string, string>
3838
blockEnd: Date
3939
durationVisible: boolean
@@ -98,7 +98,7 @@ export function ScheduleSessionCard({
9898
>
9999
<Anchor
100100
id={`session-${session.id}`}
101-
href={`/conf/${year}/schedule/${session.id}?name=${session.name}`}
101+
href={`${basePath}/schedule/${session.id}?name=${session.name}`}
102102
className="absolute inset-0 z-[1] ring-inset ring-neu-400 hover:ring-1 dark:ring-neu-100"
103103
aria-label={`Read more about "${eventTitle}" by ${speakers
104104
.map(s => s.name)
@@ -122,7 +122,7 @@ export function ScheduleSessionCard({
122122
<React.Fragment key={s.username || s.name}>
123123
{s.username ? (
124124
<Anchor
125-
href={`/conf/${year}/speakers/${s.username}`}
125+
href={`${basePath}/speakers/${s.username}`}
126126
className="relative z-[2] decoration-neu-600 hover:underline dark:decoration-neu-200"
127127
>
128128
{s.name}
@@ -166,7 +166,7 @@ function SessionDuration({
166166
new Date(session.event_end).getTime() -
167167
new Date(session.event_start).getTime()
168168

169-
// if a session is longer than 3 hourse, we show the time range
169+
// if a session is longer than 3 hours, we show the time range
170170
const formattedTime =
171171
durationMs > 1000 * 60 * 60 * 3
172172
? formatBlockTime(session.event_start, new Date(session.event_end))
@@ -198,7 +198,7 @@ function AddToCalendarLink({
198198
description: stripHtml(session.description).result,
199199
location: session.venue,
200200
organizer: {
201-
name: `GraphQLConf ${new Date().getFullYear()}`,
201+
name: `GraphQL Day ${new Date().getFullYear()}`,
202202
email: "graphql_events@linuxfoundation.org",
203203
},
204204
guests: speakers.map(s => s.name),
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import clsx from "clsx"
2+
import { findBestMatch } from "string-similarity"
3+
4+
import { SchedSpeaker, ScheduleSession } from "@/app/conf/_api/sched-types"
5+
6+
import { getEventTitle } from "../../utils"
7+
8+
export interface SessionVideoProps {
9+
video: {
10+
id: string
11+
title: string
12+
}
13+
className?: string
14+
}
15+
16+
export function SessionVideo({ video, className }: SessionVideoProps) {
17+
return (
18+
<iframe
19+
className={clsx("mx-auto aspect-video w-full", className)}
20+
src={`https://youtube.com/embed/${video.id}`}
21+
title={video.title}
22+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
23+
allowFullScreen
24+
/>
25+
)
26+
}
27+
28+
export function buildVideoIndex(
29+
schedule: ScheduleSession[],
30+
speakers: SchedSpeaker[],
31+
videos: { id: string; title: string }[],
32+
) {
33+
const sessionIdByTitle: Record<string, string> = Object.create(null)
34+
for (const session of schedule) {
35+
const speakerNames = (session.speakers || []).map(speaker => {
36+
const s = speakers.find(s => s.username === speaker.username)
37+
if (!s) {
38+
throw new Error(
39+
`Speaker "${speaker.username}" not found for "${session.name}"`,
40+
)
41+
}
42+
return s.name
43+
})
44+
45+
const eventTitle = getEventTitle(session, speakerNames)
46+
const title = `${eventTitle} ${speakerNames.join(" ")}`
47+
sessionIdByTitle[title] = session.id
48+
}
49+
50+
const videoBySessionId: Record<string, { id: string; title: string }> =
51+
Object.create(null)
52+
if (Object.keys(sessionIdByTitle).length > 0) {
53+
for (const video of videos) {
54+
const result = findBestMatch(video.title, Object.keys(sessionIdByTitle))
55+
if (result.ratings[result.bestMatchIndex].rating < 0.17) {
56+
console.warn(
57+
`Could not find suitable schedule item for video "${video.title}"`,
58+
)
59+
continue
60+
}
61+
const recordingTitle = result.bestMatch.target
62+
const sessionId = sessionIdByTitle[recordingTitle]
63+
videoBySessionId[sessionId] = video
64+
}
65+
}
66+
67+
return (event: ScheduleSession) => videoBySessionId[event.id]
68+
}

0 commit comments

Comments
 (0)