diff --git a/2024/src/components/RoomLegend.tsx b/2024/src/components/RoomLegend.tsx
index 66a66082..87cb480c 100644
--- a/2024/src/components/RoomLegend.tsx
+++ b/2024/src/components/RoomLegend.tsx
@@ -16,11 +16,25 @@ const Box = styled.div`
gap: 1em;
}
`
+
+const RoomBoxContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ gap: 1em;
+
+ ${({ theme }) => theme.breakpoints.mobile} {
+ align-items: flex-start;
+ gap: 0;
+ }
+`
+
const RoomBox = styled(InlineLink)<{ disabled: boolean }>`
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
+
${({ disabled }) =>
disabled
? `
@@ -66,10 +80,32 @@ const SubText = styled.span`
margin-top: -0.1rem;
font-family: ${({ theme }) => theme.fonts.text};
`
+
+const StreamLink = styled.a<{ track: Rooms }>`
+ display: block;
+ font-size: 1.6rem;
+ margin-top: 1rem;
+ text-align: left;
+ font-family: ${({ theme }) => theme.fonts.text};
+
+ color: ${({ theme }) => theme.colors.text};
+ text-decoration-color: ${({ track, theme }) =>
+ theme.colors[`room${track}Border`]};
+
+ &:hover {
+ color: ${({ track, theme }) => theme.colors[`room${track}Border`]};
+ }
+
+ @media print {
+ display: none;
+ }
+`
+
const TextBox = styled.div`
display: flex;
flex-direction: column;
justify-items: center;
+ align-items: center;
`
type RoomProps = {
@@ -78,22 +114,42 @@ type RoomProps = {
}
export const Room = ({ track, "selected-track": selectedTrack }: RoomProps) => {
+ function getTrackLink(track: Rooms) {
+ switch (track) {
+ case "A":
+ return "https://youtube.com/live/ew1zmA7y9q8"
+ case "B":
+ return "https://youtube.com/live/2BXwigWGjWQ"
+ case "C":
+ return "https://youtube.com/live/E3yTtaGr7V8"
+ case "D":
+ return "https://youtube.com/live/5Wt0r5vHOwQ"
+ }
+ }
+
const { t } = useTranslation()
return (
-
-
-
- {t(`room${track}`)}
- {t(`room${track}Sub`)}
-
-
+
+
+
+
+ {t(`room${track}`)}
+ {t(`room${track}Sub`)}
+
+
+
+
+
+ {t("viewStream")}
+
+
)
}
@@ -106,3 +162,4 @@ export const RoomLegend = (props: RoomLegendProps) => (
))}
)
+
diff --git a/2024/src/components/StreamLinks.tsx b/2024/src/components/StreamLinks.tsx
new file mode 100644
index 00000000..46006ee8
--- /dev/null
+++ b/2024/src/components/StreamLinks.tsx
@@ -0,0 +1,71 @@
+import React from "react"
+import { useTranslation } from "react-i18next"
+
+import styled from "styled-components"
+
+const Container = styled.div`
+ width: 100%;
+
+ display: flex;
+ flex-wrap: wrap;
+ gap: 40px 20px;
+ justify-content: center;
+`
+
+const Button = styled.a`
+ display: block;
+ height: min-content;
+
+ color: #000000;
+ font-weight: bold;
+ font-family: ${({ theme }) => theme.fonts.text};
+ padding: 20px 30px 20px 30px;
+ text-decoration: none;
+ border: solid 2px;
+
+ transition: transform 0.2s ease-in-out;
+ &:hover {
+ transform: scale(1.05);
+ }
+`
+
+const ButtonA = styled(Button)`
+ background-color: ${({ theme }) => theme.colors.roomA};
+ border-color: ${({ theme }) => theme.colors.roomABorder};
+`
+const ButtonB = styled(Button)`
+ background-color: ${({ theme }) => theme.colors.roomB};
+ border-color: ${({ theme }) => theme.colors.roomBBorder};
+`
+
+const ButtonC = styled(Button)`
+ background-color: ${({ theme }) => theme.colors.roomC};
+ border-color: ${({ theme }) => theme.colors.roomCBorder};
+`
+
+const ButtonD = styled(Button)`
+ background-color: ${({ theme }) => theme.colors.roomD};
+ border-color: ${({ theme }) => theme.colors.roomDBorder};
+`
+
+export const StreamLinks = () => {
+ const { t } = useTranslation()
+
+ return (
+
+
+ {t("streamLinks.trackA")}
+
+
+ {t("streamLinks.trackB")}
+
+
+ {t("streamLinks.trackC")}
+
+
+ {t("streamLinks.trackD")}
+
+
+ )
+}
+
diff --git a/2024/src/components/Title.tsx b/2024/src/components/Title.tsx
index e82cb4fa..d1aca7a2 100644
--- a/2024/src/components/Title.tsx
+++ b/2024/src/components/Title.tsx
@@ -33,5 +33,26 @@ const H2 = styled.h2`
export function Title(props: Props) {
const { children, heading = "h1" } = props
- return heading === "h1" ?
{children}
: {children}
+ const processedChildren = React.Children.map(children, child => {
+ if (typeof child === "string") {
+ return child.split("
").map((text, i, array) =>
+ i === array.length - 1 ? (
+ text
+ ) : (
+ <>
+ {text}
+
+ >
+ ),
+ )
+ }
+ return child
+ })
+
+ return heading === "h1" ? (
+ {processedChildren}
+ ) : (
+ {processedChildren}
+ )
}
+
diff --git a/2024/src/i18n/en.ts b/2024/src/i18n/en.ts
index 0591ede8..af0afe53 100644
--- a/2024/src/i18n/en.ts
+++ b/2024/src/i18n/en.ts
@@ -61,6 +61,12 @@ export const en = {
callForSponsors: "Call For Sponsors",
becomeASponsor: "Become a sponsor",
sponsors: "Sponsors",
+ "streamLinks.title": "View track live streams",
+ "streamLinks.trackA": "Track A",
+ "streamLinks.trackB": "Track B",
+ "streamLinks.trackC": "Track C",
+ "streamLinks.trackD": "Track D",
+ "viewStream": "View stream",
"sponsor.premium": "Premium Sponsor",
"sponsor.sponsor": "Sponsor",
openMobileMenu: "Open Navigation Menu",
diff --git a/2024/src/i18n/ja.ts b/2024/src/i18n/ja.ts
index 159957bf..faa44749 100644
--- a/2024/src/i18n/ja.ts
+++ b/2024/src/i18n/ja.ts
@@ -47,6 +47,12 @@ export const ja: {
moreDetails: "詳細",
"specified-commercial-transactions-act": "特定商取引法に基づく表示",
"code-of-conduct": "行動規範",
+ "streamLinks.title": "トラックの
ライブ配信を見る",
+ "streamLinks.trackA": "トラックA",
+ "streamLinks.trackB": "トラックB",
+ "streamLinks.trackC": "トラックC",
+ "streamLinks.trackD": "トラックD",
+ "viewStream": "ライブ配信を見る",
// @ts-expect-error It's not defined in en.ts
Japanese: "日本語",
English: "英語",
diff --git a/2024/src/pages/index.tsx b/2024/src/pages/index.tsx
index eef81a7d..1e08b754 100644
--- a/2024/src/pages/index.tsx
+++ b/2024/src/pages/index.tsx
@@ -7,6 +7,7 @@ import { SEO } from "../components/Seo"
import { Hero } from "../components/Hero"
import { Title } from "../components/Title"
import { SpeakerList } from "../components/SpeakerList"
+import { StreamLinks } from "../components/StreamLinks"
import { SponsorList } from "../components/SponsorList"
import { LinkButton } from "../components/LinkButton"
import { Card as _Card } from "../components/Card"
@@ -206,6 +207,18 @@ export default function IndexPage() {
Date.now() > new Date(site.siteMetadata.sponsorDeadline).getTime()
const parts = [
+ {
+ subTitle: t("streamLinks.title"),
+ available: true,
+ render: () => (
+ <>
+
+
+
+ >
+ ),
+ },
+
{
subTitle: t("speakers"),
available: featuredSpeakers.length,
diff --git a/2024/src/templates/schedule.tsx b/2024/src/templates/schedule.tsx
index 8b786ad1..bfe20ee0 100644
--- a/2024/src/templates/schedule.tsx
+++ b/2024/src/templates/schedule.tsx
@@ -459,6 +459,7 @@ export default function SchedulePage({
+