this repo has no description atmosphereconf-vods.wisp.place/
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

add schedule page

+530 -68
+23
src/lib/conference.ts
··· 69 69 .filter((entry) => entry.sessions.length > 0); 70 70 } 71 71 72 + export function getScheduleByDay(): Array<{ 73 + day: ConferenceDay; 74 + rooms: Array<{ track: Track; sessions: SessionWithDerived[] }>; 75 + }> { 76 + return conferenceDays 77 + .map((day) => ({ 78 + day, 79 + rooms: tracks 80 + .map((track) => ({ 81 + track, 82 + sessions: sessions 83 + .filter( 84 + (session) => 85 + session.dayId === day.id && session.trackSlug === track.slug, 86 + ) 87 + .sort((a, b) => a.startsAt.localeCompare(b.startsAt)) 88 + .map(withDerivedFields), 89 + })) 90 + .filter((entry) => entry.sessions.length > 0), 91 + })) 92 + .filter((entry) => entry.rooms.length > 0); 93 + } 94 + 72 95 export function buildPlaylistUrl(recordUri: string): string { 73 96 return `https://vod-beta.stream.place/xrpc/place.stream.playback.getVideoPlaylist?uri=${encodeURIComponent(recordUri)}`; 74 97 }
+26 -3
src/routeTree.gen.ts
··· 9 9 // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. 10 10 11 11 import { Route as rootRouteImport } from './routes/__root' 12 + import { Route as ScheduleRouteImport } from './routes/schedule' 12 13 import { Route as IndexRouteImport } from './routes/index' 13 14 import { Route as VideosVideoSlugRouteImport } from './routes/videos.$videoSlug' 14 15 import { Route as TracksTrackSlugRouteImport } from './routes/tracks.$trackSlug' 15 16 17 + const ScheduleRoute = ScheduleRouteImport.update({ 18 + id: '/schedule', 19 + path: '/schedule', 20 + getParentRoute: () => rootRouteImport, 21 + } as any) 16 22 const IndexRoute = IndexRouteImport.update({ 17 23 id: '/', 18 24 path: '/', ··· 31 37 32 38 export interface FileRoutesByFullPath { 33 39 '/': typeof IndexRoute 40 + '/schedule': typeof ScheduleRoute 34 41 '/tracks/$trackSlug': typeof TracksTrackSlugRoute 35 42 '/videos/$videoSlug': typeof VideosVideoSlugRoute 36 43 } 37 44 export interface FileRoutesByTo { 38 45 '/': typeof IndexRoute 46 + '/schedule': typeof ScheduleRoute 39 47 '/tracks/$trackSlug': typeof TracksTrackSlugRoute 40 48 '/videos/$videoSlug': typeof VideosVideoSlugRoute 41 49 } 42 50 export interface FileRoutesById { 43 51 __root__: typeof rootRouteImport 44 52 '/': typeof IndexRoute 53 + '/schedule': typeof ScheduleRoute 45 54 '/tracks/$trackSlug': typeof TracksTrackSlugRoute 46 55 '/videos/$videoSlug': typeof VideosVideoSlugRoute 47 56 } 48 57 export interface FileRouteTypes { 49 58 fileRoutesByFullPath: FileRoutesByFullPath 50 - fullPaths: '/' | '/tracks/$trackSlug' | '/videos/$videoSlug' 59 + fullPaths: '/' | '/schedule' | '/tracks/$trackSlug' | '/videos/$videoSlug' 51 60 fileRoutesByTo: FileRoutesByTo 52 - to: '/' | '/tracks/$trackSlug' | '/videos/$videoSlug' 53 - id: '__root__' | '/' | '/tracks/$trackSlug' | '/videos/$videoSlug' 61 + to: '/' | '/schedule' | '/tracks/$trackSlug' | '/videos/$videoSlug' 62 + id: 63 + | '__root__' 64 + | '/' 65 + | '/schedule' 66 + | '/tracks/$trackSlug' 67 + | '/videos/$videoSlug' 54 68 fileRoutesById: FileRoutesById 55 69 } 56 70 export interface RootRouteChildren { 57 71 IndexRoute: typeof IndexRoute 72 + ScheduleRoute: typeof ScheduleRoute 58 73 TracksTrackSlugRoute: typeof TracksTrackSlugRoute 59 74 VideosVideoSlugRoute: typeof VideosVideoSlugRoute 60 75 } 61 76 62 77 declare module '@tanstack/react-router' { 63 78 interface FileRoutesByPath { 79 + '/schedule': { 80 + id: '/schedule' 81 + path: '/schedule' 82 + fullPath: '/schedule' 83 + preLoaderRoute: typeof ScheduleRouteImport 84 + parentRoute: typeof rootRouteImport 85 + } 64 86 '/': { 65 87 id: '/' 66 88 path: '/' ··· 87 109 88 110 const rootRouteChildren: RootRouteChildren = { 89 111 IndexRoute: IndexRoute, 112 + ScheduleRoute: ScheduleRoute, 90 113 TracksTrackSlugRoute: TracksTrackSlugRoute, 91 114 VideosVideoSlugRoute: VideosVideoSlugRoute, 92 115 }
+3 -6
src/routes/__root.tsx
··· 19 19 NavbarLogo, 20 20 NavbarNavigation, 21 21 } from "#/components/navbar"; 22 - import { 23 - horizontalSpace, 24 - verticalSpace, 25 - } from "../components/theme/semantic-spacing.stylex"; 26 22 import { fontFamily, fontWeight } from "../components/theme/typography.stylex"; 27 - import { primaryColor, uiColor } from "../components/theme/color.stylex"; 23 + import { primaryColor } from "../components/theme/color.stylex"; 28 24 import { blue } from "../components/theme/colors/blue.stylex"; 29 25 import { Footer } from "#/components/footer"; 30 26 import { Link } from "#/components/link"; 31 - import { Body, SmallBody } from "#/components/typography"; 27 + import { SmallBody } from "#/components/typography"; 32 28 33 29 interface MyRouterContext { 34 30 queryClient: QueryClient; ··· 136 132 </NavbarLink> 137 133 </NavbarLogo> 138 134 <NavbarNavigation justify="right"> 135 + <NavbarLink href="/schedule">Schedule</NavbarLink> 139 136 <NavbarLink href="/tracks/great-hall-south"> 140 137 Great Hall South 141 138 </NavbarLink>
+27 -59
src/routes/index.tsx
··· 2 2 import * as stylex from "@stylexjs/stylex"; 3 3 4 4 import { AspectRatio, AspectRatioImage } from "#/components/aspect-ratio"; 5 + import { Badge } from "#/components/badge"; 5 6 import { Flex } from "#/components/flex"; 6 7 import { Page } from "#/components/page"; 8 + import { Blockquote } from "#/components/typography"; 9 + import { Text } from "#/components/typography/text"; 10 + import { getSessionsForTrack, getTracks } from "#/lib/conference"; 7 11 import { primaryColor, uiColor } from "../components/theme/color.stylex"; 12 + import { breakpoints } from "../components/theme/media-queries.stylex"; 8 13 import { ui } from "../components/theme/semantic-color.stylex"; 9 14 import { 10 15 gap, 11 16 horizontalSpace, 12 17 verticalSpace, 13 18 } from "../components/theme/semantic-spacing.stylex"; 14 - import { Text } from "#/components/typography/text"; 15 - import { getSessionsForTrack, getTracks } from "#/lib/conference"; 16 - import { Blockquote } from "#/components/typography"; 17 - import { Badge } from "#/components/badge"; 18 - import { breakpoints } from "../components/theme/media-queries.stylex"; 19 19 20 20 export const Route = createFileRoute("/")({ component: App }); 21 21 ··· 39 39 gap: gap["3xl"], 40 40 gridTemplateColumns: { 41 41 default: "1fr", 42 - "@media (min-width: 48rem)": "1fr 1fr", 42 + [breakpoints.md]: "1fr 1fr", 43 43 }, 44 44 }, 45 45 roomLink: { ··· 57 57 roomTileFeatured: { 58 58 gridColumn: { 59 59 default: "auto", 60 - "@media (min-width: 48rem)": "1 / -1", 60 + [breakpoints.md]: "1 / -1", 61 61 }, 62 62 }, 63 63 roomMedia: { ··· 94 94 flexWrap: "wrap", 95 95 gap: gap["sm"], 96 96 alignItems: "center", 97 - marginLeft: `calc(${horizontalSpace["md"]} * -1)`, 98 97 }, 99 - roomChip: { 98 + roomChipBase: { 100 99 borderRadius: "999px", 101 - borderColor: "rgba(255, 255, 255, 0.24)", 102 100 borderStyle: "solid", 103 101 borderWidth: 1, 104 - backgroundColor: "rgba(0, 0, 0, 0.28)", 105 102 paddingLeft: horizontalSpace["md"], 106 103 paddingRight: horizontalSpace["md"], 107 104 paddingTop: verticalSpace["xs"], 108 105 paddingBottom: verticalSpace["xs"], 109 106 width: "fit-content", 110 107 }, 108 + roomChipContrast: { 109 + borderColor: "rgba(255, 255, 255, 0.24)", 110 + backgroundColor: "rgba(0, 0, 0, 0.28)", 111 + }, 111 112 roomDescription: { 112 - maxWidth: "34rem", 113 - opacity: 0.8, 114 - }, 115 - badge: { 116 - borderColor: uiColor.border2, 117 - borderStyle: "solid", 118 - borderWidth: 1, 119 - borderRadius: "999px", 120 - paddingLeft: horizontalSpace["md"], 121 - paddingRight: horizontalSpace["md"], 122 - paddingTop: verticalSpace["xs"], 123 - paddingBottom: verticalSpace["xs"], 124 - width: "fit-content", 113 + lineHeight: 1.5, 125 114 }, 126 115 sectionSpacing: { 127 116 marginTop: verticalSpace["6xl"], 128 117 }, 129 - sectionHeader: { 130 - display: "flex", 131 - flexDirection: { 132 - default: "column", 133 - "@media (min-width: 48rem)": "row", 134 - }, 135 - alignItems: { 136 - default: "flex-start", 137 - "@media (min-width: 48rem)": "center", 138 - }, 139 - justifyContent: "space-between", 140 - gap: gap["xl"], 141 - }, 142 - sectionTitleGroup: { 143 - display: "flex", 144 - flexDirection: "column", 145 - gap: gap["sm"], 146 - }, 147 - gooseBadge: { 148 - display: "flex", 149 - alignItems: "center", 150 - gap: gap["md"], 151 - borderRadius: "999px", 152 - backgroundColor: uiColor.bgSubtle, 153 - paddingLeft: horizontalSpace["lg"], 154 - paddingRight: horizontalSpace["lg"], 155 - paddingTop: verticalSpace["sm"], 156 - paddingBottom: verticalSpace["sm"], 157 - }, 158 118 gooseImage: { 159 119 width: { 160 120 default: "8rem", ··· 201 161 202 162 <Flex direction="column" gap="5xl" style={styles.intro}> 203 163 <Text size="lg" leading="base"> 204 - Enter each room to browse talks in schedule order. Top of each 205 - page is the start of the day, and the bottom is where final 206 - sessions land. 164 + Enter each room to browse talks in schedule order, or use the 165 + schedule page in the navbar to compare events across rooms. 207 166 </Text> 208 167 <Blockquote> 209 168 This app is focused on recorded talks and workshops. Some sessions ··· 220 179 {...stylex.props(styles.gooseImage)} 221 180 /> 222 181 </Flex> 223 - <Flex direction="column" gap="xl" style={styles.sectionSpacing}> 182 + 183 + <Flex direction="column" gap="8xl" style={styles.sectionSpacing}> 224 184 <div {...stylex.props(styles.roomGrid)}> 225 185 {tracks.map((track, index) => { 226 186 const sessionCount = getSessionsForTrack(track.slug).length; ··· 255 215 <div {...stylex.props(styles.roomMeta)}> 256 216 <Text 257 217 size="sm" 258 - style={[styles.roomChip, ui.textContrast]} 218 + style={[ 219 + styles.roomChipBase, 220 + styles.roomChipContrast, 221 + ui.textContrast, 222 + ]} 259 223 > 260 224 {track.stageLabel} 261 225 </Text> 262 226 <Text 263 227 size="sm" 264 - style={[styles.roomChip, ui.textContrast]} 228 + style={[ 229 + styles.roomChipBase, 230 + styles.roomChipContrast, 231 + ui.textContrast, 232 + ]} 265 233 > 266 234 {sessionCount} scheduled session 267 235 {sessionCount === 1 ? "" : "s"}
+451
src/routes/schedule.tsx
··· 1 + import { Link, createFileRoute } from "@tanstack/react-router"; 2 + import * as stylex from "@stylexjs/stylex"; 3 + 4 + import { Flex } from "#/components/flex"; 5 + import { Page } from "#/components/page"; 6 + import { Text } from "#/components/typography/text"; 7 + import { formatLocalTime, getScheduleByDay } from "#/lib/conference"; 8 + import { uiColor, warningColor } from "../components/theme/color.stylex"; 9 + import { 10 + gap, 11 + horizontalSpace, 12 + verticalSpace, 13 + } from "../components/theme/semantic-spacing.stylex"; 14 + 15 + export const Route = createFileRoute("/schedule")({ component: SchedulePage }); 16 + 17 + const CALENDAR_PIXELS_PER_MINUTE = 5; 18 + const CALENDAR_SLOT_MINUTES = 30; 19 + const CALENDAR_TIME_COLUMN_REM = 5.5; 20 + const CALENDAR_ROOM_COLUMN_REM = 16; 21 + 22 + const styles = stylex.create({ 23 + root: { 24 + paddingBottom: verticalSpace["10xl"], 25 + }, 26 + scheduleStack: { 27 + display: "flex", 28 + flexDirection: "column", 29 + gap: gap["7xl"], 30 + }, 31 + daySection: { 32 + display: "flex", 33 + flexDirection: "column", 34 + gap: gap["6xl"], 35 + }, 36 + dayHeader: { 37 + paddingBottom: verticalSpace["4xl"], 38 + borderBottomColor: uiColor.border2, 39 + borderBottomStyle: "solid", 40 + borderBottomWidth: 1, 41 + }, 42 + roomChipBase: { 43 + borderRadius: "999px", 44 + borderStyle: "solid", 45 + borderWidth: 1, 46 + paddingLeft: horizontalSpace["md"], 47 + paddingRight: horizontalSpace["md"], 48 + paddingTop: verticalSpace["xs"], 49 + paddingBottom: verticalSpace["xs"], 50 + width: "fit-content", 51 + }, 52 + roomChipSurface: { 53 + borderColor: uiColor.border2, 54 + backgroundColor: uiColor.bg, 55 + }, 56 + roomTitleLink: { 57 + color: "inherit", 58 + textDecoration: "none", 59 + }, 60 + scheduleScroller: { 61 + overflowX: "auto", 62 + paddingBottom: verticalSpace["md"], 63 + }, 64 + scheduleBoard: { 65 + display: "grid", 66 + gap: gap["lg"], 67 + alignItems: "start", 68 + minWidth: "fit-content", 69 + }, 70 + timeRailHeader: { 71 + display: "flex", 72 + alignItems: "flex-end", 73 + justifyContent: "flex-end", 74 + paddingRight: horizontalSpace["lg"], 75 + paddingBottom: verticalSpace["lg"], 76 + }, 77 + roomLaneHeader: { 78 + display: "flex", 79 + flexDirection: "column", 80 + gap: gap["sm"], 81 + minWidth: `${CALENDAR_ROOM_COLUMN_REM}rem`, 82 + paddingBottom: verticalSpace["lg"], 83 + }, 84 + roomLaneHeaderMeta: { 85 + display: "flex", 86 + flexWrap: "wrap", 87 + gap: gap["lg"], 88 + alignItems: "center", 89 + }, 90 + timeRail: { 91 + position: "relative", 92 + paddingRight: horizontalSpace["lg"], 93 + }, 94 + timeLabel: { 95 + position: "absolute", 96 + right: horizontalSpace["lg"], 97 + transform: "translateY(-50%)", 98 + whiteSpace: "nowrap", 99 + }, 100 + timeRailTick: { 101 + position: "absolute", 102 + left: 0, 103 + right: 0, 104 + borderTopColor: uiColor.border2, 105 + borderTopStyle: "solid", 106 + borderTopWidth: 1, 107 + }, 108 + roomLane: { 109 + position: "relative", 110 + minWidth: `${CALENDAR_ROOM_COLUMN_REM}rem`, 111 + borderLeftColor: uiColor.border2, 112 + borderLeftStyle: "solid", 113 + borderLeftWidth: 1, 114 + borderTopColor: uiColor.border2, 115 + borderTopStyle: "solid", 116 + borderTopWidth: 1, 117 + backgroundColor: uiColor.bgSubtle, 118 + }, 119 + roomLaneTick: { 120 + position: "absolute", 121 + left: 0, 122 + right: 0, 123 + borderTopColor: uiColor.border2, 124 + borderTopStyle: "solid", 125 + borderTopWidth: 1, 126 + }, 127 + calendarEventShell: { 128 + position: "absolute", 129 + left: horizontalSpace["sm"], 130 + right: horizontalSpace["sm"], 131 + }, 132 + calendarEventLink: { 133 + color: "inherit", 134 + textDecoration: "none", 135 + display: "block", 136 + height: "100%", 137 + position: "relative", 138 + zIndex: { 139 + ":hover": 10, 140 + }, 141 + }, 142 + calendarEvent: { 143 + boxSizing: "border-box", 144 + height: "100%", 145 + overflow: "hidden", 146 + borderRadius: "1rem", 147 + borderColor: uiColor.border2, 148 + borderStyle: "solid", 149 + borderWidth: 1, 150 + backgroundColor: uiColor.bg, 151 + paddingLeft: horizontalSpace["md"], 152 + paddingRight: horizontalSpace["md"], 153 + paddingTop: verticalSpace["md"], 154 + paddingBottom: verticalSpace["md"], 155 + display: "flex", 156 + flexDirection: "column", 157 + gap: gap["xl"], 158 + boxShadow: "0 12px 32px rgba(0, 0, 0, 0.08)", 159 + }, 160 + calendarEventMeta: { 161 + display: "flex", 162 + justifyContent: "space-between", 163 + gap: gap["sm"], 164 + alignItems: "baseline", 165 + }, 166 + calendarEventTitle: { 167 + overflow: "hidden", 168 + }, 169 + calendarEventStatus: { 170 + flexShrink: 0, 171 + color: warningColor.text2, 172 + }, 173 + }); 174 + 175 + function getMinutesInDay(isoDateTime: string): number { 176 + const [hours, minutes] = isoDateTime.slice(11, 16).split(":").map(Number); 177 + 178 + return hours * 60 + minutes; 179 + } 180 + 181 + function floorToSlot(minutes: number, slotMinutes: number): number { 182 + return Math.floor(minutes / slotMinutes) * slotMinutes; 183 + } 184 + 185 + function ceilToSlot(minutes: number, slotMinutes: number): number { 186 + return Math.ceil(minutes / slotMinutes) * slotMinutes; 187 + } 188 + 189 + function formatScheduleAxisTime(totalMinutes: number): string { 190 + const hours24 = Math.floor(totalMinutes / 60); 191 + const minutes = totalMinutes % 60; 192 + const suffix = hours24 >= 12 ? "PM" : "AM"; 193 + const hours12 = hours24 % 12 || 12; 194 + 195 + return `${hours12}:${minutes.toString().padStart(2, "0")} ${suffix}`; 196 + } 197 + 198 + function buildTimeSlots(startMinutes: number, endMinutes: number): number[] { 199 + const slots: number[] = []; 200 + 201 + for ( 202 + let minutes = startMinutes; 203 + minutes <= endMinutes; 204 + minutes += CALENDAR_SLOT_MINUTES 205 + ) { 206 + slots.push(minutes); 207 + } 208 + 209 + return slots; 210 + } 211 + 212 + function SchedulePage() { 213 + const schedule = getScheduleByDay(); 214 + 215 + return ( 216 + <Page.Root style={styles.root}> 217 + <Page.Header> 218 + <Flex direction="column" gap="4xl"> 219 + <Page.Title>Schedule</Page.Title> 220 + <Page.Description> 221 + Compare the conference timeline across rooms on a shared time grid. 222 + </Page.Description> 223 + <Text size="sm" variant="secondary"> 224 + Vancouver time (PDT) 225 + </Text> 226 + </Flex> 227 + </Page.Header> 228 + 229 + <div {...stylex.props(styles.scheduleStack)}> 230 + {schedule.map(({ day, rooms }) => 231 + (() => { 232 + const allSessions = rooms.flatMap(({ sessions }) => sessions); 233 + const dayStartMinutes = floorToSlot( 234 + Math.min( 235 + ...allSessions.map((session) => 236 + getMinutesInDay(session.startsAt), 237 + ), 238 + ), 239 + CALENDAR_SLOT_MINUTES, 240 + ); 241 + const dayEndMinutes = ceilToSlot( 242 + Math.max( 243 + ...allSessions.map((session) => 244 + getMinutesInDay(session.endsAt), 245 + ), 246 + ), 247 + CALENDAR_SLOT_MINUTES, 248 + ); 249 + const timelineHeight = 250 + (dayEndMinutes - dayStartMinutes) * CALENDAR_PIXELS_PER_MINUTE; 251 + const timeSlots = buildTimeSlots(dayStartMinutes, dayEndMinutes); 252 + 253 + return ( 254 + <section key={day.id} {...stylex.props(styles.daySection)}> 255 + <Flex direction="column" gap="4xl" style={styles.dayHeader}> 256 + <Text 257 + font="title" 258 + size={{ default: "lg", sm: "xl" }} 259 + weight="semibold" 260 + > 261 + {day.label} 262 + </Text> 263 + <Text size="sm" variant="secondary"> 264 + {day.phase === "preshow" ? "Preshow" : "Conference"} · 265 + Vancouver time (PDT) 266 + </Text> 267 + </Flex> 268 + 269 + <div {...stylex.props(styles.scheduleScroller)}> 270 + <div 271 + {...stylex.props(styles.scheduleBoard)} 272 + style={{ 273 + gridTemplateColumns: `${CALENDAR_TIME_COLUMN_REM}rem repeat(${rooms.length}, minmax(${CALENDAR_ROOM_COLUMN_REM}rem, 1fr))`, 274 + }} 275 + > 276 + <div {...stylex.props(styles.timeRailHeader)}> 277 + <Text size="xs" variant="secondary"> 278 + PDT 279 + </Text> 280 + </div> 281 + 282 + {rooms.map(({ track, sessions }) => ( 283 + <div 284 + key={`${day.id}-${track.slug}-header`} 285 + {...stylex.props(styles.roomLaneHeader)} 286 + > 287 + <div {...stylex.props(styles.roomLaneHeaderMeta)}> 288 + <Text 289 + size="xs" 290 + style={[ 291 + styles.roomChipBase, 292 + styles.roomChipSurface, 293 + ]} 294 + > 295 + {track.stageLabel} 296 + </Text> 297 + <Text 298 + size="xs" 299 + style={[ 300 + styles.roomChipBase, 301 + styles.roomChipSurface, 302 + ]} 303 + > 304 + {sessions.length} session 305 + {sessions.length === 1 ? "" : "s"} 306 + </Text> 307 + </div> 308 + <Link 309 + to="/tracks/$trackSlug" 310 + params={{ trackSlug: track.slug }} 311 + {...stylex.props(styles.roomTitleLink)} 312 + > 313 + <Text font="title" size="lg" weight="bold"> 314 + {track.name} 315 + </Text> 316 + </Link> 317 + </div> 318 + ))} 319 + 320 + <div 321 + {...stylex.props(styles.timeRail)} 322 + style={{ height: `${timelineHeight}px` }} 323 + > 324 + {timeSlots.map((slot) => { 325 + const top = 326 + (slot - dayStartMinutes) * CALENDAR_PIXELS_PER_MINUTE; 327 + 328 + return ( 329 + <div key={`${day.id}-time-${slot}`}> 330 + <div 331 + {...stylex.props(styles.timeRailTick)} 332 + style={{ top: `${top}px` }} 333 + /> 334 + <div 335 + {...stylex.props(styles.timeLabel)} 336 + style={{ top: `${top}px` }} 337 + > 338 + <Text size="xs" variant="secondary"> 339 + {formatScheduleAxisTime(slot)} 340 + </Text> 341 + </div> 342 + </div> 343 + ); 344 + })} 345 + </div> 346 + 347 + {rooms.map(({ track, sessions }) => ( 348 + <div 349 + key={`${day.id}-${track.slug}-lane`} 350 + {...stylex.props(styles.roomLane)} 351 + style={{ height: `${timelineHeight}px` }} 352 + > 353 + {timeSlots.map((slot) => { 354 + const top = 355 + (slot - dayStartMinutes) * 356 + CALENDAR_PIXELS_PER_MINUTE; 357 + 358 + return ( 359 + <div 360 + key={`${day.id}-${track.slug}-${slot}`} 361 + {...stylex.props(styles.roomLaneTick)} 362 + style={{ top: `${top}px` }} 363 + /> 364 + ); 365 + })} 366 + 367 + {sessions.map((session) => { 368 + const startMinutes = getMinutesInDay( 369 + session.startsAt, 370 + ); 371 + const endMinutes = getMinutesInDay(session.endsAt); 372 + const durationMinutes = endMinutes - startMinutes; 373 + const top = 374 + (startMinutes - dayStartMinutes) * 375 + CALENDAR_PIXELS_PER_MINUTE; 376 + const height = Math.max( 377 + durationMinutes * CALENDAR_PIXELS_PER_MINUTE, 378 + 64, 379 + ); 380 + const isCompact = durationMinutes <= 10; 381 + const showSpeakers = durationMinutes >= 35; 382 + const showStatus = durationMinutes >= 45; 383 + 384 + return ( 385 + <div 386 + key={session.id} 387 + {...stylex.props(styles.calendarEventShell)} 388 + style={{ 389 + top: `${top}px`, 390 + height: `${height}px`, 391 + }} 392 + > 393 + <Link 394 + to="/videos/$videoSlug" 395 + params={{ videoSlug: session.slug }} 396 + search={{ autoplay: false }} 397 + title={`${session.title} · ${formatLocalTime(session.startsAt)} - ${formatLocalTime(session.endsAt)}`} 398 + {...stylex.props(styles.calendarEventLink)} 399 + > 400 + <div {...stylex.props(styles.calendarEvent)}> 401 + <div 402 + {...stylex.props(styles.calendarEventMeta)} 403 + > 404 + <Text size="xs" variant="secondary"> 405 + {formatLocalTime(session.startsAt)} 406 + </Text> 407 + {!session.recordUri && showStatus ? ( 408 + <Text 409 + size="xs" 410 + variant="secondary" 411 + style={styles.calendarEventStatus} 412 + > 413 + No recording 414 + </Text> 415 + ) : null} 416 + </div> 417 + <Text 418 + size={isCompact ? "xs" : "sm"} 419 + weight="medium" 420 + style={styles.calendarEventTitle} 421 + > 422 + {session.title} 423 + </Text> 424 + {showSpeakers && 425 + session.speakerProfiles.length > 0 ? ( 426 + <Text size="xs" variant="secondary"> 427 + {session.speakerProfiles 428 + .map( 429 + (speaker) => 430 + speaker.displayName ?? speaker.name, 431 + ) 432 + .join(", ")} 433 + </Text> 434 + ) : null} 435 + </div> 436 + </Link> 437 + </div> 438 + ); 439 + })} 440 + </div> 441 + ))} 442 + </div> 443 + </div> 444 + </section> 445 + ); 446 + })(), 447 + )} 448 + </div> 449 + </Page.Root> 450 + ); 451 + }