Skip to content

Commit 4ad6aba

Browse files
benjaiebenjiejemgillam
authored
Fix the way that lightning talks render on the schedule (#2444)
Really ought to have done this before the conference... --------- Co-authored-by: Benjie Gillam <benjie@jemjie.com> Co-authored-by: Jem Gillam <6413628+jemgillam@users.noreply.github.com>
1 parent 98e0eb4 commit 4ad6aba

2 files changed

Lines changed: 230 additions & 160 deletions

File tree

src/app/conf/2026/schedule/_components/schedule-list.tsx

Lines changed: 183 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,6 @@ import { format, parseISO, compareAsc } from "date-fns"
44
import { ReactElement, useMemo, useState } from "react"
55

66
import { ScheduleSession } from "@/app/conf/_api/sched-types"
7-
import {
8-
ConcurrentSessions,
9-
ScheduleSessionsByDay,
10-
} from "@/app/conf/_components/schedule/session-list"
117

128
import { ScheduleSessionCard } from "./schedule-session-card"
139
import {
@@ -25,6 +21,17 @@ export interface FiltersConfig
2521
Record<keyof ScheduleSession /* key */, string /* label */>
2622
> {}
2723

24+
interface ScheduleBlock {
25+
start: string
26+
end: Date
27+
sessions: ScheduleSession[]
28+
isMergeable: boolean
29+
}
30+
31+
interface ScheduleBlocksByDay {
32+
[date: string]: ScheduleBlock[]
33+
}
34+
2835
function getSessionsByDay(
2936
scheduleData: ScheduleSession[],
3037
filterStates: FilterStates,
@@ -34,7 +41,7 @@ function getSessionsByDay(
3441
)
3542

3643
const states = Object.entries<FilterStates[keyof FilterStates]>(filterStates)
37-
const concurrentSessions: ConcurrentSessions = {}
44+
const filteredSessions: ScheduleSession[] = []
3845

3946
filteredSortedSchedule.forEach(session => {
4047
for (const [property, filterState] of states) {
@@ -49,21 +56,52 @@ function getSessionsByDay(
4956
}
5057
}
5158

59+
filteredSessions.push(session)
60+
})
61+
62+
const concurrentSessions: Record<string, ScheduleSession[]> = {}
63+
filteredSessions.forEach(session => {
5264
;(concurrentSessions[session.event_start] ||= []).push(session)
5365
})
5466

55-
const sessionsByDay: ScheduleSessionsByDay = {}
67+
const sessionsByDay: ScheduleBlocksByDay = {}
5668
Object.entries(concurrentSessions).forEach(([date, sessions]) => {
5769
const day = date.split(" ")[0]
58-
if (!sessionsByDay[day]) {
59-
sessionsByDay[day] = {}
60-
}
61-
sessionsByDay[day] = {
62-
...sessionsByDay[day],
63-
[date]: sessions.sort((a, b) =>
64-
(a?.venue ?? "").localeCompare(b?.venue ?? ""),
70+
const dayBlocks = (sessionsByDay[day] ||= [])
71+
const sortedSessions = sessions.sort((a, b) =>
72+
(a?.venue ?? "").localeCompare(b?.venue ?? ""),
73+
)
74+
const blockEnd = new Date(
75+
Math.max(
76+
...sortedSessions.map(session => new Date(session.event_end).getTime()),
6577
),
78+
)
79+
const isMergeable = sortedSessions.every(session => {
80+
const durationMs =
81+
new Date(session.event_end).getTime() -
82+
new Date(session.event_start).getTime()
83+
return durationMs < 55 * 60 * 1000
84+
})
85+
const previousBlock = dayBlocks.at(-1)
86+
87+
if (
88+
isMergeable &&
89+
previousBlock?.isMergeable &&
90+
new Date(date).getTime() < previousBlock.end.getTime()
91+
) {
92+
previousBlock.sessions.push(...sortedSessions)
93+
if (blockEnd.getTime() > previousBlock.end.getTime()) {
94+
previousBlock.end = blockEnd
95+
}
96+
return
6697
}
98+
99+
dayBlocks.push({
100+
start: date,
101+
end: blockEnd,
102+
sessions: sortedSessions,
103+
isMergeable,
104+
})
67105
})
68106

69107
return {
@@ -149,7 +187,8 @@ export function ScheduleList({
149187
)
150188
}
151189
// if the first day has less than 3 sessions, it's probably a "day zero" with extra events or workshops
152-
const firstDayIsDayZero = Object.keys(firstDay).length < 3
190+
const firstDayIsDayZero = firstDay.length < 3
191+
const sortedDays = Object.entries(filteredSessions)
153192
const startIndex = firstDayIsDayZero ? 0 : 1
154193

155194
return (
@@ -197,7 +236,7 @@ export function ScheduleList({
197236
) : (
198237
<>
199238
<div className="mb-4 flex space-x-4">
200-
{Object.keys(filteredSessions).map((date, index) => (
239+
{sortedDays.map(([date], index) => (
201240
<a
202241
href={`#day-${index + startIndex}`}
203242
key={date}
@@ -207,126 +246,146 @@ export function ScheduleList({
207246
</a>
208247
))}
209248
</div>
210-
{Object.entries(filteredSessions).map(
211-
([date, concurrentSessionsGroup], index) => (
212-
<div
213-
key={date}
214-
className="typography-body-sm bg-neu-200 pb-px dark:bg-neu-50"
249+
{sortedDays.map(([date, scheduleBlocks], index) => (
250+
<div
251+
key={date}
252+
className="typography-body-sm bg-neu-200 pb-px dark:bg-neu-50"
253+
>
254+
<h3
255+
className="bg-neu-50 py-4 dark:bg-neu-0"
256+
id={`day-${index + startIndex}`}
215257
>
216-
<h3
217-
className="bg-neu-50 py-4 dark:bg-neu-0"
218-
id={`day-${index + startIndex}`}
219-
>
220-
{format(parseISO(date), "EEEE, MMMM d")}
221-
</h3>
222-
{Object.entries(concurrentSessionsGroup).map(
223-
([sessionDate, sessions], i, blocks) => {
224-
const blockEnd = new Date(
225-
Math.max(
226-
...sessions.map(session =>
227-
new Date(session.event_end).getTime(),
258+
{format(parseISO(date), "EEEE, MMMM d")}
259+
</h3>
260+
{scheduleBlocks.map((block, i, blocks) => {
261+
const sessionsByVenue = Object.entries(
262+
block.sessions.reduce<Record<string, ScheduleSession[]>>(
263+
(groupedSessions, session) => {
264+
const venue = session.venue || ""
265+
;(groupedSessions[venue] ||= []).push(session)
266+
return groupedSessions
267+
},
268+
{},
269+
),
270+
)
271+
.sort(([venueA], [venueB]) => venueA.localeCompare(venueB))
272+
.map(
273+
([venue, venueSessions]) =>
274+
[
275+
venue,
276+
venueSessions.sort(
277+
(a, b) =>
278+
compareAsc(
279+
new Date(a.event_start),
280+
new Date(b.event_start),
281+
) ||
282+
compareAsc(
283+
new Date(a.event_end),
284+
new Date(b.event_end),
285+
),
228286
),
229-
),
230-
)
287+
] as const,
288+
)
231289

232-
const nextBlock = blocks[i + 1]
233-
const nextBlockStart = nextBlock?.[0]
234-
? new Date(nextBlock[0])
235-
: undefined
290+
const nextBlockStart = blocks[i + 1]
291+
? new Date(blocks[i + 1].start)
292+
: undefined
236293

237-
const isBreak =
238-
sessions[0]?.event_type
239-
?.toLowerCase()
240-
.includes("break") ||
241-
blocks[i + 1]?.[1]?.[0]?.event_type
242-
?.toLowerCase()
243-
.includes("break")
244-
const hasDashedBorder =
245-
blockEnd &&
246-
blockEnd.getTime() === nextBlockStart?.getTime() &&
247-
!isBreak
294+
const isBreak =
295+
block.sessions.some(session =>
296+
session.event_type?.toLowerCase().includes("break"),
297+
) ||
298+
blocks[i + 1]?.sessions.some(session =>
299+
session.event_type?.toLowerCase().includes("break"),
300+
)
301+
const hasDashedBorder =
302+
block.end.getTime() === nextBlockStart?.getTime() && !isBreak
248303

249-
const endTimesDiffer = sessions.some(
250-
session =>
251-
new Date(session.event_end).getTime() !==
252-
blockEnd.getTime(),
253-
)
304+
const endTimesDiffer = block.sessions.some(
305+
session =>
306+
new Date(session.event_end).getTime() !==
307+
block.end.getTime(),
308+
)
254309

255-
let timeMarker = getTimeMarker(sessionDate, blockEnd)
256-
// if end times differ and blockEnd is far from start, we treat this as a long event, like "solutions showcase"
257-
if (
258-
endTimesDiffer &&
259-
blockEnd.getTime() - new Date(sessionDate).getTime() >
260-
1000 * 60 * 60 * 2
261-
) {
262-
timeMarker = null
263-
}
310+
let timeMarker = getTimeMarker(block.start, block.end)
311+
// if end times differ and blockEnd is far from start, we treat this as a long event, like "solutions showcase"
312+
if (
313+
endTimesDiffer &&
314+
block.end.getTime() - new Date(block.start).getTime() >
315+
1000 * 60 * 60 * 2
316+
) {
317+
timeMarker = null
318+
}
264319

265-
return (
266-
<div
267-
key={`concurrent sessions on ${sessionDate}`}
268-
className="relative lg:mt-px [&_div:has(a:hover)]:z-[1]"
269-
>
270-
<div className="mr-px flex flex-col max-lg:ml-px lg:flex-row">
271-
<div className="relative border-neu-50 bg-neu-50 dark:bg-neu-0 max-lg:-mx-px max-lg:my-px max-lg:border-x lg:mr-px">
272-
<span className="typography-body-sm mt-3 inline-block w-28 whitespace-nowrap pb-0.5 pl-4 lg:mr-6 lg:pb-4 lg:pl-0">
273-
{formatBlockTime(
274-
sessionDate,
275-
endTimesDiffer ? undefined : blockEnd,
276-
)}
277-
</span>
278-
</div>
279-
<div className="relative flex w-full flex-col items-end gap-px lg:flex-row lg:items-start">
280-
{sessions.map(session => (
281-
<ScheduleSessionCard
282-
key={session.id}
283-
session={session}
284-
year={year}
285-
eventsColors={eventsColors}
286-
blockEnd={blockEnd}
287-
durationVisible={endTimesDiffer}
288-
/>
289-
))}
290-
</div>
291-
</div>
292-
{timeMarker && (
320+
return (
321+
<div
322+
key={`schedule block on ${block.start}`}
323+
className="relative lg:mt-px [&_div:has(a:hover)]:z-[1]"
324+
>
325+
<div className="mr-px flex flex-col max-lg:ml-px lg:flex-row">
326+
<div className="relative border-neu-50 bg-neu-50 dark:bg-neu-0 max-lg:-mx-px max-lg:my-px max-lg:border-x lg:mr-px">
327+
<span className="typography-body-sm mt-3 inline-block w-28 whitespace-nowrap pb-0.5 pl-4 lg:mr-6 lg:pb-4 lg:pl-0">
328+
{formatBlockTime(block.start, block.end)}
329+
</span>
330+
</div>
331+
<div className="relative flex w-full flex-col items-end gap-px lg:flex-row lg:items-stretch">
332+
{sessionsByVenue.map(([venue, venueSessions]) => (
293333
<div
294-
id="current-time-marker"
295-
className="typography-body-xs pointer-events-none absolute -right-1 z-10 -translate-y-full font-mono tabular-nums text-pri-base before:absolute before:inset-x-0 before:bottom-0 before:border-b before:border-pri-base before:opacity-80 after:absolute after:bottom-0 after:left-[-100vw] after:w-screen after:border-t after:border-pri-base after:opacity-20 dark:text-pri-light dark:before:border-pri-light dark:after:border-pri-light max-xl:bg-neu-0 xl:translate-x-full"
296-
style={{
297-
top: `${timeMarker.positionPercentage}%`,
298-
}}
334+
key={venue || venueSessions[0]?.id}
335+
className="flex w-full flex-col gap-px lg:w-0 lg:flex-1 lg:self-stretch"
299336
>
300-
<span className="max-2xl:hidden">now: </span>
301-
{timeMarker.currentTime}
337+
{venueSessions.map((session, sessionIndex) => (
338+
<div key={session.id} className="min-h-0 flex-1">
339+
<ScheduleSessionCard
340+
session={session}
341+
year={year}
342+
eventsColors={eventsColors}
343+
blockEnd={block.end}
344+
durationVisible={endTimesDiffer}
345+
startTimeVisible={sessionIndex > 0}
346+
/>
347+
</div>
348+
))}
302349
</div>
303-
)}
304-
{hasDashedBorder && (
305-
<svg
306-
className="absolute -bottom-px left-0 h-px w-full text-neu-50"
307-
viewBox="0 0 100 1"
308-
preserveAspectRatio="none"
309-
>
310-
<line
311-
x1="0"
312-
y1="0.5"
313-
x2="100"
314-
y2="0.5"
315-
stroke="currentColor"
316-
strokeWidth="1"
317-
strokeDasharray="4,4"
318-
strokeDashoffset="4"
319-
vectorEffect="non-scaling-stroke"
320-
/>
321-
</svg>
322-
)}
350+
))}
351+
</div>
352+
</div>
353+
{timeMarker && (
354+
<div
355+
id="current-time-marker"
356+
className="typography-body-xs pointer-events-none absolute -right-1 z-10 -translate-y-full font-mono tabular-nums text-pri-base before:absolute before:inset-x-0 before:bottom-0 before:border-b before:border-pri-base before:opacity-80 after:absolute after:bottom-0 after:left-[-100vw] after:w-screen after:border-t after:border-pri-base after:opacity-20 dark:text-pri-light dark:before:border-pri-light dark:after:border-pri-light max-xl:bg-neu-0 xl:translate-x-full"
357+
style={{
358+
top: `${timeMarker.positionPercentage}%`,
359+
}}
360+
>
361+
<span className="max-2xl:hidden">now: </span>
362+
{timeMarker.currentTime}
323363
</div>
324-
)
325-
},
326-
)}
327-
</div>
328-
),
329-
)}
364+
)}
365+
{hasDashedBorder && (
366+
<svg
367+
className="absolute -bottom-px left-0 h-px w-full text-neu-50"
368+
viewBox="0 0 100 1"
369+
preserveAspectRatio="none"
370+
>
371+
<line
372+
x1="0"
373+
y1="0.5"
374+
x2="100"
375+
y2="0.5"
376+
stroke="currentColor"
377+
strokeWidth="1"
378+
strokeDasharray="4,4"
379+
strokeDashoffset="4"
380+
vectorEffect="non-scaling-stroke"
381+
/>
382+
</svg>
383+
)}
384+
</div>
385+
)
386+
})}
387+
</div>
388+
))}
330389
</>
331390
)}
332391
</>

0 commit comments

Comments
 (0)