Skip to content

Commit 8947570

Browse files
authored
website: Add a new kubecon event page (#8311)
Signed-off-by: Charlie Egan <charlie_egan@apple.com>
1 parent 637b0b5 commit 8947570

File tree

12 files changed

+617
-0
lines changed

12 files changed

+617
-0
lines changed

docs/docusaurus.config.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import fs from "fs/promises";
66
const path = require("path");
77

88
import { loadPages } from "./src/lib/ecosystem/loadPages.js";
9+
import { loadEvents } from "./src/lib/events/loadEvents.js";
910
import { loadRules } from "./src/lib/projects/regal/loadRules.js";
1011
import { loadSurveyEventData, loadSurveyEventMetadata, loadSurveyQuestions } from "./src/lib/surveys/loadSurveyData.js";
1112

@@ -452,6 +453,51 @@ The Linux Foundation has registered trademarks and uses trademarks. For a list o
452453
};
453454
},
454455

456+
async function eventsData(context, _options) {
457+
return {
458+
name: "events-data",
459+
460+
async loadContent() {
461+
const events = await loadEvents(path.join(context.siteDir, "src/data/events/*.json"));
462+
return { events };
463+
},
464+
465+
async contentLoaded({ content, actions }) {
466+
const { createData } = actions;
467+
const { events } = content;
468+
469+
await createData("events.json", JSON.stringify(events, null, 2));
470+
},
471+
};
472+
},
473+
474+
async function eventsPagesGen(context, _options) {
475+
return {
476+
name: "events-pages-gen",
477+
async loadContent() {
478+
const events = await loadEvents(path.join(context.siteDir, "src/data/events/*.json"));
479+
return { events };
480+
},
481+
482+
async contentLoaded({ content, actions }) {
483+
const { events } = content;
484+
485+
await Promise.all(
486+
Object.values(events).map(async (event) => {
487+
const routePath = path.join(baseUrl, `/events/${event.id}`);
488+
return actions.addRoute({
489+
path: routePath,
490+
component: require.resolve("./src/EventPage.jsx"),
491+
exact: true,
492+
modules: {},
493+
customData: { id: event.id },
494+
});
495+
}),
496+
);
497+
},
498+
};
499+
},
500+
455501
async function versionsData(context, _options) {
456502
return {
457503
name: "versions-data",

docs/src/EventPage.jsx

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import Heading from "@theme/Heading";
2+
import Layout from "@theme/Layout";
3+
import React, { useEffect, useState } from "react";
4+
5+
import AgendaItem from "./components/Event/AgendaItem";
6+
import Countdown from "./components/Event/Countdown";
7+
8+
import eventsData from "@generated/events-data/default/events.json";
9+
10+
import styles from "./EventPage.module.css";
11+
12+
const EventPage = (props) => {
13+
const eventId = props.route.customData.id;
14+
const event = eventsData[eventId];
15+
16+
// Pages are only created for events that exist in data
17+
if (!event || !event.agenda) {
18+
return null;
19+
}
20+
21+
const [eventStatus, setEventStatus] = useState("before");
22+
23+
const eventStart = new Date(event.startDate);
24+
const MS_PER_DAY = 1000 * 60 * 60 * 24;
25+
const eventEnd = new Date(new Date(event.endDate).getTime() + MS_PER_DAY);
26+
27+
useEffect(() => {
28+
const updateStatus = () => {
29+
const now = new Date();
30+
31+
if (now >= eventStart && now < eventEnd) {
32+
setEventStatus("during");
33+
} else if (now >= eventEnd) {
34+
setEventStatus("after");
35+
} else {
36+
setEventStatus("before");
37+
}
38+
};
39+
40+
updateStatus();
41+
const interval = setInterval(updateStatus, 1000);
42+
return () => clearInterval(interval);
43+
}, [eventStart, eventEnd]);
44+
45+
const bannerUrl = `/img/event-banners/${event.banner}`;
46+
47+
return (
48+
<Layout title={event.title}>
49+
<div className={styles.pageLayout}>
50+
<div className={styles.sidebar}>
51+
<img
52+
src={bannerUrl}
53+
alt={event.title}
54+
className={styles.banner}
55+
/>
56+
57+
{event.location && (
58+
<div className={styles.eventLocation}>
59+
{event.location}
60+
</div>
61+
)}
62+
63+
{eventStatus === "before" && <Countdown targetDate={eventStart} title={event.title} />}
64+
65+
{eventStatus === "during" && (
66+
<Heading as="h1" className={styles.eventMessage}>
67+
{event.title} is on now
68+
</Heading>
69+
)}
70+
71+
{eventStatus === "after" && (
72+
<Heading as="h1" className={styles.eventMessage}>
73+
{event.title} has now passed
74+
</Heading>
75+
)}
76+
</div>
77+
78+
<div className={styles.mainContent}>
79+
<div className={styles.section}>
80+
<Heading as="h2" className={styles.sectionTitle}>
81+
Agenda
82+
</Heading>
83+
{event.agenda.map((dayAgenda, dayIndex) => (
84+
<div key={dayIndex} className={styles.daySection}>
85+
<h3 className={styles.dayTitle}>
86+
{dayAgenda.day}, {dayAgenda.date}
87+
</h3>
88+
<div className={styles.dayItems}>
89+
{dayAgenda.items.map((item, itemIndex) => (
90+
<AgendaItem
91+
key={itemIndex}
92+
item={item}
93+
/>
94+
))}
95+
</div>
96+
</div>
97+
))}
98+
</div>
99+
</div>
100+
</div>
101+
</Layout>
102+
);
103+
};
104+
105+
export default EventPage;

docs/src/EventPage.module.css

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
.pageLayout {
2+
display: flex;
3+
gap: 2rem;
4+
max-width: 100%;
5+
padding: 2rem;
6+
}
7+
8+
@media (max-width: 996px) {
9+
.pageLayout {
10+
flex-direction: column;
11+
padding: 1rem;
12+
}
13+
}
14+
15+
.sidebar {
16+
flex: 0 0 350px;
17+
position: sticky;
18+
top: 2rem;
19+
align-self: flex-start;
20+
}
21+
22+
@media (max-width: 996px) {
23+
.sidebar {
24+
position: relative;
25+
flex: 1;
26+
top: 0;
27+
}
28+
}
29+
30+
.mainContent {
31+
flex: 1;
32+
min-width: 0;
33+
}
34+
35+
.banner {
36+
width: 100%;
37+
height: auto;
38+
border-radius: 0.5rem;
39+
margin-bottom: 1.5rem;
40+
}
41+
42+
.eventLocation {
43+
text-align: center;
44+
font-size: 1.1rem;
45+
color: var(--ifm-font-color-secondary);
46+
margin-bottom: 1.5rem;
47+
}
48+
49+
.eventMessage {
50+
font-size: 1.5rem;
51+
color: var(--ifm-font-color-base);
52+
margin: 1rem 0;
53+
text-align: center;
54+
}
55+
56+
@media (max-width: 996px) {
57+
.eventMessage {
58+
font-size: 2rem;
59+
}
60+
}
61+
62+
.section {
63+
margin: 0;
64+
}
65+
66+
.sectionTitle {
67+
margin-bottom: 1.5rem;
68+
font-size: 1.8rem;
69+
}
70+
71+
.daySection {
72+
margin-bottom: 2rem;
73+
}
74+
75+
.dayTitle {
76+
font-size: 1.3rem;
77+
margin-bottom: 1rem;
78+
color: var(--ifm-font-color-base);
79+
border-bottom: 2px solid var(--ifm-color-emphasis-300);
80+
padding-bottom: 0.5rem;
81+
}
82+
83+
.dayItems {
84+
display: flex;
85+
flex-direction: column;
86+
gap: 1rem;
87+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import React from "react";
2+
import SessionCard from "../SessionCard";
3+
import styles from "./styles.module.css";
4+
5+
const AgendaItem = ({ item }) => {
6+
if (item.type === "session") {
7+
return <SessionCard {...item} />;
8+
}
9+
10+
if (item.type === "booth") {
11+
return (
12+
<div className={styles.boothItem}>
13+
<div className={styles.boothDetails}>
14+
<div className={styles.boothDetailItem}>
15+
<strong>Booth:</strong> {item.location}
16+
</div>
17+
<div className={styles.boothDetailItem}>
18+
<strong>Hours:</strong> {item.hours}
19+
</div>
20+
</div>
21+
</div>
22+
);
23+
}
24+
25+
return null;
26+
};
27+
28+
export default AgendaItem;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
.boothItem {
2+
background: var(--ifm-card-background-color, var(--ifm-background-surface-color));
3+
border: 1px solid var(--ifm-color-emphasis-300);
4+
border-radius: 0.5rem;
5+
padding: 1.5rem;
6+
}
7+
8+
.boothDetails {
9+
display: flex;
10+
flex-direction: column;
11+
gap: 0.5rem;
12+
}
13+
14+
.boothDetailItem {
15+
font-size: 0.95rem;
16+
}
17+
18+
.boothDetailItem strong {
19+
margin-right: 0.5rem;
20+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import Heading from "@theme/Heading";
2+
import React, { useEffect, useState } from "react";
3+
4+
import styles from "./styles.module.css";
5+
6+
const Countdown = ({ targetDate, title }) => {
7+
const [timeRemaining, setTimeRemaining] = useState(null);
8+
9+
useEffect(() => {
10+
const MS_PER_SECOND = 1000;
11+
const MS_PER_MINUTE = MS_PER_SECOND * 60;
12+
const MS_PER_HOUR = MS_PER_MINUTE * 60;
13+
const MS_PER_DAY = MS_PER_HOUR * 24;
14+
15+
const updateCountdown = () => {
16+
const now = new Date();
17+
const diff = targetDate - now;
18+
19+
if (diff <= 0) {
20+
setTimeRemaining(null);
21+
return;
22+
}
23+
24+
const days = Math.floor(diff / MS_PER_DAY);
25+
const hours = Math.floor((diff % MS_PER_DAY) / MS_PER_HOUR);
26+
const minutes = Math.floor((diff % MS_PER_HOUR) / MS_PER_MINUTE);
27+
const seconds = Math.floor((diff % MS_PER_MINUTE) / MS_PER_SECOND);
28+
29+
setTimeRemaining({ days, hours, minutes, seconds });
30+
};
31+
32+
updateCountdown();
33+
const interval = setInterval(updateCountdown, MS_PER_SECOND);
34+
return () => clearInterval(interval);
35+
}, [targetDate]);
36+
37+
if (!timeRemaining) {
38+
return null;
39+
}
40+
41+
return (
42+
<>
43+
<Heading as="h1" className={styles.countdownTitle}>
44+
{title}
45+
</Heading>
46+
<div className={styles.countdown}>
47+
<div className={styles.countdownItem}>
48+
<div className={styles.countdownNumber}>{timeRemaining.days}</div>
49+
<div className={styles.countdownLabel}>Days</div>
50+
</div>
51+
<div className={styles.countdownItem}>
52+
<div className={styles.countdownNumber}>{timeRemaining.hours}</div>
53+
<div className={styles.countdownLabel}>Hours</div>
54+
</div>
55+
<div className={styles.countdownItem}>
56+
<div className={styles.countdownNumber}>{timeRemaining.minutes}</div>
57+
<div className={styles.countdownLabel}>Minutes</div>
58+
</div>
59+
<div className={styles.countdownItem}>
60+
<div className={styles.countdownNumber}>{timeRemaining.seconds}</div>
61+
<div className={styles.countdownLabel}>Seconds</div>
62+
</div>
63+
</div>
64+
</>
65+
);
66+
};
67+
68+
export default Countdown;

0 commit comments

Comments
 (0)