Skip to content

Commit b306a3d

Browse files
jaydestroCopilot
andcommitted
time change update
Co-authored-by: Copilot <copilot@github.com>
1 parent 48b306a commit b306a3d

7 files changed

Lines changed: 209 additions & 44 deletions

File tree

client/src/pages/conf/agenda.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
"title": "One Codebase, Any Cloud: Building a Retail Database with OSS and Azure",
6161
"description": "Build a retail store database once with MongoDB-compatible APIs and deploy it unchanged on-prem via Kubernetes and fully managed on Azure.",
6262
"speakers": [{ "name": "Khelan Modi", "slug": "khelan-modi" }],
63-
"url": ""
63+
"url": "https://youtu.be/O9bxsV9TeiI"
6464
},
6565
{
6666
"time": "12:23 PM",
@@ -74,7 +74,7 @@
7474
"title": "Querying and Indexing in Azure Cosmos DB: The Complete Guide",
7575
"description": "A complete tour of the Azure Cosmos DB query engine and indexing system — routing, index types, query metrics, and cost-saving optimizations.",
7676
"speakers": [{ "name": "James Codella", "slug": "james-codella" }],
77-
"url": ""
77+
"url": "https://youtu.be/fkEDPP1XsKk"
7878
},
7979
{
8080
"time": "12:58 PM",
@@ -169,13 +169,13 @@
169169
"title": "Memory for Your Agents: Building Chat History and Semantic Caching with Azure Cosmos DB",
170170
"description": "Build chat history and semantic cache containers that give AI agents persistent, context-aware memory while cutting tokens and latency.",
171171
"speakers": [{ "name": "Lino Tadros", "slug": "lino-tadros" }],
172-
"url": ""
172+
"url": "https://youtu.be/atbRswDKruY"
173173
},
174174
{
175175
"title": "Importing Data into Azure Cosmos DB: Tools, Tradeoffs, and Capacity Planning",
176176
"description": "A decision framework for choosing between the Data Migration Tool, Azure Data Factory, and the Spark connector — plus capacity planning for bulk loads.",
177177
"speakers": [{ "name": "Rakhi Thejraj", "slug": "rakhi-thejraj" }],
178-
"url": ""
178+
"url": "https://youtu.be/CTeI9N-LK4Y"
179179
}
180180
]
181181
}

client/src/pages/conf/conf.module.css

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3722,6 +3722,25 @@
37223722
color: #bbb;
37233723
}
37243724

3725+
.speakerModalVideo {
3726+
position: relative;
3727+
margin-top: 14px;
3728+
aspect-ratio: 16 / 9;
3729+
width: 100%;
3730+
border-radius: 10px;
3731+
overflow: hidden;
3732+
background: #000;
3733+
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
3734+
}
3735+
3736+
.speakerModalVideo iframe {
3737+
position: absolute;
3738+
inset: 0;
3739+
width: 100%;
3740+
height: 100%;
3741+
border: 0;
3742+
}
3743+
37253744
.speakerModalBio {
37263745
margin-top: 4px;
37273746
}

client/src/pages/conf/index.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import AgendaSection from "./sections/AgendaSection";
1313
import NewsSection from "./sections/NewsSection";
1414
import FaqSection from "./sections/FaqSection";
1515
import SpeakersSection from "./sections/SpeakersSection";
16+
import { useStreamReleased } from "./videoRelease";
1617

1718
const CONF_YEAR = "2026";
1819
const CONF_DATE_DISPLAY = "April 28 - 9:00 AM - 2:00 PM PDT";
@@ -154,6 +155,7 @@ const archiveTimelineData = [
154155
const ConfPage = () => {
155156
const { siteConfig } = useDocusaurusContext();
156157
const { showAgenda, showStream, streamEmbedUrl } = getConfSettings(siteConfig);
158+
const streamLive = useStreamReleased();
157159

158160
return (
159161
<Layout
@@ -240,6 +242,10 @@ const ConfPage = () => {
240242
</div>
241243
</header>
242244

245+
{showStream && streamLive && (
246+
<StreamSection confYear={CONF_YEAR} streamEmbedUrl={streamEmbedUrl} live />
247+
)}
248+
243249
<section className={styles.introSection} aria-labelledby="conf-intro">
244250
<div id="about" className={styles.sectionAnchor} />
245251
<div className={styles.introInner}>
@@ -356,7 +362,7 @@ const ConfPage = () => {
356362
</div>
357363
</section>
358364

359-
{showStream && <StreamSection confYear={CONF_YEAR} streamEmbedUrl={streamEmbedUrl} />}
365+
{showStream && !streamLive && <StreamSection confYear={CONF_YEAR} streamEmbedUrl={streamEmbedUrl} />}
360366

361367
<NewsSection confYear={CONF_YEAR} />
362368

client/src/pages/conf/sections/AgendaSection.tsx

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from "react";
22
import useBaseUrl from "@docusaurus/useBaseUrl";
33
import styles from "../conf.module.css";
44
import agendaData from "../agenda.json";
5+
import { useVideoReleased } from "../videoRelease";
56

67
interface AgendaSectionProps {
78
confYear: string;
@@ -25,6 +26,27 @@ const { live: liveAgenda, onDemand: onDemandAgenda } = agendaData as {
2526
onDemand: AgendaItem[];
2627
};
2728

29+
// Videos become clickable on 2026-04-28 1:30 PM PDT (PDT = UTC-7).
30+
const renderAgendaCta = (url: string | undefined, released: boolean) => {
31+
if (url && released) {
32+
return (
33+
<a
34+
className={styles.agendaCardButton}
35+
href={url}
36+
target="_blank"
37+
rel="noopener noreferrer"
38+
>
39+
Watch on YouTube
40+
</a>
41+
);
42+
}
43+
return (
44+
<span className={styles.agendaCardButton} aria-disabled="true">
45+
Coming soon
46+
</span>
47+
);
48+
};
49+
2850
const handleSpeakerClick = (slug: string) => (e: React.MouseEvent) => {
2951
e.preventDefault();
3052
const target = `#speaker/${slug}`;
@@ -47,6 +69,8 @@ const renderSpeakers = (speakers: AgendaSpeaker[]) =>
4769
));
4870

4971
const AgendaSection = ({ confYear }: AgendaSectionProps) => {
72+
const released = useVideoReleased();
73+
5074
return (
5175
<section className={styles.newsSection} aria-labelledby="agenda-heading">
5276
<div id="agenda" className={styles.sectionAnchor} />
@@ -81,9 +105,7 @@ const AgendaSection = ({ confYear }: AgendaSectionProps) => {
81105
<p className={styles.agendaCardDescription}>{item.description}</p>
82106
)}
83107
</div>
84-
<span className={styles.agendaCardButton} aria-disabled="true">
85-
Coming soon
86-
</span>
108+
{renderAgendaCta(item.url, released)}
87109
</article>
88110
))}
89111
</div>
@@ -102,9 +124,7 @@ const AgendaSection = ({ confYear }: AgendaSectionProps) => {
102124
<p className={styles.agendaCardDescription}>{item.description}</p>
103125
)}
104126
</div>
105-
<span className={styles.agendaCardButton} aria-disabled="true">
106-
Coming soon
107-
</span>
127+
{renderAgendaCta(item.url, released)}
108128
</article>
109129
))}
110130
</div>

client/src/pages/conf/sections/SpeakersSection.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import React, { useEffect, useRef, useState } from "react";
22
import styles from "../conf.module.css";
33
import speakersData from "../speakers2026.json";
4+
import {
5+
getVideoUrlForSpeaker,
6+
getYouTubeEmbedUrl,
7+
useVideoReleased,
8+
} from "../videoRelease";
49

510
interface Session {
611
title?: string;
@@ -57,6 +62,7 @@ const SpeakersSection = ({ confYear }: SpeakersSectionProps) => {
5762
const speakers = allSpeakers.filter((s) => s.confirmed !== false);
5863
const [selected, setSelected] = useState<Speaker | null>(null);
5964
const modalRef = useRef<HTMLDivElement>(null);
65+
const videoReleased = useVideoReleased();
6066

6167
// Open modal when URL hash matches #speaker/<slug> — both on mount and live
6268
// when other sections (e.g. Agenda) link to #speaker/<slug>.
@@ -244,6 +250,21 @@ const SpeakersSection = ({ confYear }: SpeakersSectionProps) => {
244250
{selected.session.abstract && (
245251
<p className={styles.speakerModalAbstract}>{selected.session.abstract}</p>
246252
)}
253+
{videoReleased && (() => {
254+
const embedUrl = getYouTubeEmbedUrl(getVideoUrlForSpeaker(selected.slug));
255+
if (!embedUrl) return null;
256+
return (
257+
<div className={styles.speakerModalVideo}>
258+
<iframe
259+
src={embedUrl}
260+
title={`${selected.name}${selected.session?.title ?? "session video"}`}
261+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
262+
allowFullScreen
263+
loading="lazy"
264+
/>
265+
</div>
266+
);
267+
})()}
247268
</div>
248269
)}
249270

client/src/pages/conf/sections/StreamSection.tsx

Lines changed: 39 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import styles from "../conf.module.css";
44
interface StreamSectionProps {
55
confYear: string;
66
streamEmbedUrl: string | null;
7+
/** When true, hide the countdown + subscribe notice (stream is live). */
8+
live?: boolean;
79
}
810

911
// Azure Cosmos DB Conf 2026: April 28, 2026 · 9:00 AM PDT (UTC−7)
@@ -32,7 +34,7 @@ const computeCountdown = (): Countdown => {
3234

3335
const pad = (value: number) => value.toString().padStart(2, "0");
3436

35-
const StreamSection = ({ confYear, streamEmbedUrl }: StreamSectionProps) => {
37+
const StreamSection = ({ confYear, streamEmbedUrl, live = false }: StreamSectionProps) => {
3638
// Start with a stable value for SSR; update on the client after mount.
3739
const [countdown, setCountdown] = useState<Countdown>(() => computeCountdown());
3840

@@ -83,41 +85,45 @@ const StreamSection = ({ confYear, streamEmbedUrl }: StreamSectionProps) => {
8385
<span data-node-id="6:228">Evaluation available on 4/28/2026</span>
8486
</button>
8587
*/}
86-
<p className={styles.streamCtaNote} data-node-id="6:230">
87-
Subscribe to the channel and click the 🔔 bell to get a reminder when we go live.
88-
</p>
89-
90-
{countdown.done ? (
91-
<p className={styles.streamCountdownLive} aria-live="polite">
92-
🔴 We&apos;re live!
88+
{!live && (
89+
<p className={styles.streamCtaNote} data-node-id="6:230">
90+
Subscribe to the channel and click the 🔔 bell to get a reminder when we go live.
9391
</p>
94-
) : (
95-
<div
96-
className={styles.streamCountdown}
97-
role="timer"
98-
aria-live="off"
99-
aria-label={`Time until Azure Cosmos DB Conf ${confYear} goes live`}
100-
>
101-
<span className={styles.streamCountdownLabel}>Live in</span>
102-
<div className={styles.streamCountdownGrid}>
103-
<div className={styles.streamCountdownUnit}>
104-
<span className={styles.streamCountdownValue}>{pad(countdown.days)}</span>
105-
<span className={styles.streamCountdownName}>days</span>
106-
</div>
107-
<div className={styles.streamCountdownUnit}>
108-
<span className={styles.streamCountdownValue}>{pad(countdown.hours)}</span>
109-
<span className={styles.streamCountdownName}>hrs</span>
110-
</div>
111-
<div className={styles.streamCountdownUnit}>
112-
<span className={styles.streamCountdownValue}>{pad(countdown.minutes)}</span>
113-
<span className={styles.streamCountdownName}>min</span>
114-
</div>
115-
<div className={styles.streamCountdownUnit}>
116-
<span className={styles.streamCountdownValue}>{pad(countdown.seconds)}</span>
117-
<span className={styles.streamCountdownName}>sec</span>
92+
)}
93+
94+
{!live && (
95+
countdown.done ? (
96+
<p className={styles.streamCountdownLive} aria-live="polite">
97+
🔴 We&apos;re live!
98+
</p>
99+
) : (
100+
<div
101+
className={styles.streamCountdown}
102+
role="timer"
103+
aria-live="off"
104+
aria-label={`Time until Azure Cosmos DB Conf ${confYear} goes live`}
105+
>
106+
<span className={styles.streamCountdownLabel}>Live in</span>
107+
<div className={styles.streamCountdownGrid}>
108+
<div className={styles.streamCountdownUnit}>
109+
<span className={styles.streamCountdownValue}>{pad(countdown.days)}</span>
110+
<span className={styles.streamCountdownName}>days</span>
111+
</div>
112+
<div className={styles.streamCountdownUnit}>
113+
<span className={styles.streamCountdownValue}>{pad(countdown.hours)}</span>
114+
<span className={styles.streamCountdownName}>hrs</span>
115+
</div>
116+
<div className={styles.streamCountdownUnit}>
117+
<span className={styles.streamCountdownValue}>{pad(countdown.minutes)}</span>
118+
<span className={styles.streamCountdownName}>min</span>
119+
</div>
120+
<div className={styles.streamCountdownUnit}>
121+
<span className={styles.streamCountdownValue}>{pad(countdown.seconds)}</span>
122+
<span className={styles.streamCountdownName}>sec</span>
123+
</div>
118124
</div>
119125
</div>
120-
</div>
126+
)
121127
)}
122128

123129
<a
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { useEffect, useState } from "react";
2+
import agendaData from "./agenda.json";
3+
4+
// Videos become clickable on 2026-04-28 1:30 PM PDT (PDT = UTC-7 → 20:30 UTC).
5+
export const VIDEO_RELEASE_TIMESTAMP = Date.UTC(2026, 3, 28, 20, 30, 0);
6+
7+
// Live stream embed becomes visible on 2026-04-28 7:00 AM PDT (→ 14:00 UTC).
8+
export const STREAM_RELEASE_TIMESTAMP = Date.UTC(2026, 3, 28, 14, 0, 0);
9+
10+
interface AgendaEntry {
11+
speakers: { slug: string }[];
12+
url?: string;
13+
}
14+
15+
const { live, onDemand } = agendaData as {
16+
live: AgendaEntry[];
17+
onDemand: AgendaEntry[];
18+
};
19+
20+
const allSessions: AgendaEntry[] = [...live, ...onDemand];
21+
22+
/**
23+
* Look up a YouTube URL for a given speaker slug. Returns the first matching
24+
* session URL, or undefined if the speaker has no URL (yet).
25+
*/
26+
export const getVideoUrlForSpeaker = (slug: string): string | undefined => {
27+
for (const session of allSessions) {
28+
if (!session.url) continue;
29+
if (session.speakers.some((s) => s.slug === slug)) {
30+
return session.url;
31+
}
32+
}
33+
return undefined;
34+
};
35+
36+
/**
37+
* Returns true once the shared release time has passed. The value is `false`
38+
* during SSR/initial render to avoid hydration mismatch, then flips to `true`
39+
* on the client if applicable. Re-checked every minute.
40+
*
41+
* Supports `?releaseNow=1` in the URL to force-release for testing/previewing.
42+
*/
43+
export const useVideoReleased = (): boolean => {
44+
const [released, setReleased] = useState(false);
45+
useEffect(() => {
46+
const forceReleased =
47+
typeof window !== "undefined" &&
48+
new URLSearchParams(window.location.search).get("releaseNow") === "1";
49+
const check = () =>
50+
setReleased(forceReleased || Date.now() >= VIDEO_RELEASE_TIMESTAMP);
51+
check();
52+
const interval = window.setInterval(check, 60_000);
53+
return () => window.clearInterval(interval);
54+
}, []);
55+
return released;
56+
};
57+
58+
/**
59+
* Returns true once the live stream release time has passed. SSR-safe.
60+
* Supports `?streamNow=1` (and also `?releaseNow=1`) for preview.
61+
*/
62+
export const useStreamReleased = (): boolean => {
63+
const [released, setReleased] = useState(false);
64+
useEffect(() => {
65+
const params =
66+
typeof window !== "undefined"
67+
? new URLSearchParams(window.location.search)
68+
: null;
69+
const forceReleased =
70+
params?.get("streamNow") === "1" || params?.get("releaseNow") === "1";
71+
const check = () =>
72+
setReleased(!!forceReleased || Date.now() >= STREAM_RELEASE_TIMESTAMP);
73+
check();
74+
const interval = window.setInterval(check, 60_000);
75+
return () => window.clearInterval(interval);
76+
}, []);
77+
return released;
78+
};
79+
80+
/**
81+
* Convert a youtu.be or youtube.com URL into a youtube.com/embed/<id> URL.
82+
* Returns undefined if the URL cannot be parsed.
83+
*/
84+
export const getYouTubeEmbedUrl = (url: string | undefined): string | undefined => {
85+
if (!url) return undefined;
86+
const shortMatch = url.match(/youtu\.be\/([A-Za-z0-9_-]{6,})/);
87+
if (shortMatch) return `https://www.youtube.com/embed/${shortMatch[1]}`;
88+
const longMatch = url.match(/[?&]v=([A-Za-z0-9_-]{6,})/);
89+
if (longMatch) return `https://www.youtube.com/embed/${longMatch[1]}`;
90+
const embedMatch = url.match(/youtube\.com\/embed\/([A-Za-z0-9_-]{6,})/);
91+
if (embedMatch) return url;
92+
return undefined;
93+
};

0 commit comments

Comments
 (0)