diff --git a/src/app/[locale]/kodukord/page.tsx b/src/app/[locale]/kodukord/page.tsx
index a1e2456..5397bbf 100644
--- a/src/app/[locale]/kodukord/page.tsx
+++ b/src/app/[locale]/kodukord/page.tsx
@@ -4,7 +4,7 @@ import { vipnagorgialla } from "@/components/Vipnagorgialla";
import RulesLastChanged from "@/components/RulesLastChanged";
import RuleNav from "@/components/RuleNav";
import { getTranslations, setRequestLocale } from "next-intl/server";
-import { loadRulesBun } from "@/lib/loadRules";
+import { loadRules } from "@/lib/loadRules";
const sectionKeys = [
{ id: "reminder", labelKey: "rules.reminder" },
@@ -19,7 +19,7 @@ export default async function Page({
const { locale } = await params;
setRequestLocale(locale);
const t = await getTranslations({ locale });
- const content = await loadRulesBun("kodukord", locale as "et" | "en");
+ const content = await loadRules("kodukord", locale as "et" | "en");
const sections = sectionKeys.map((section) => ({
id: section.id,
diff --git a/src/app/[locale]/page.tsx b/src/app/[locale]/page.tsx
index cf56c6f..2a0b3e8 100644
--- a/src/app/[locale]/page.tsx
+++ b/src/app/[locale]/page.tsx
@@ -2,6 +2,7 @@ import { vipnagorgialla } from "@/components/Vipnagorgialla";
import Sponsors from "@/components/Sponsors";
import HeroSection from "@/components/HeroSection";
import TeaserCarousel from "@/components/TeaserCarousel";
+import Livestreams from "@/components/Livestreams";
import { Link } from "@/i18n/routing";
import { getTranslations, setRequestLocale } from "next-intl/server";
@@ -21,8 +22,11 @@ export default async function Home({
- {/* Nav cards: Piletid + Turniirid */}
-
+ {/* Livestreams — shown automatically when the event begins */}
+
+
+ {/* Nav cards: Piletid + Ajakava + Turniirid */}
+
+
+
+
+ calendar_month
+
+
+
+
+ {t("navigation.schedule")}
+
+
+ arrow_right_alt
+
+
+
+
{backgroundImage && (
@@ -83,14 +98,19 @@ function TicketCard({
))}
-
-
-
+
+ )}
+
+ {soldOut &&
}
);
}
@@ -129,6 +149,9 @@ export default async function Tickets({
backgroundImageB="/images/landing/main_supporter.jpg"
backgroundOpacityA={30}
backgroundOpacityB={40}
+ soldOutA={ticketSoldOut.visitor}
+ soldOutB={ticketSoldOut.supporter}
+ soldOutText={t("tickets.soldOut")}
className="border-b-[3px] lg:border-r-[3px]"
/>
@@ -142,6 +165,8 @@ export default async function Tickets({
buttonHref="https://fienta.com/et/tipilan"
backgroundImage="/images/landing/explore_teaser.png"
backgroundOpacity={100}
+ soldOut={ticketSoldOut.lan}
+ soldOutText={t("tickets.soldOut")}
className="border-b-[3px]"
/>
@@ -154,6 +179,8 @@ export default async function Tickets({
buttonText={t("tickets.buyButton")}
buttonHref="https://fienta.com/et/tipilan"
backgroundImage="/images/landing/league_ticket.jpg"
+ soldOut={ticketSoldOut.lol}
+ soldOutText={t("tickets.soldOut")}
className="border-b-[3px] lg:border-b-0 lg:border-r-[3px]"
/>
@@ -167,6 +194,8 @@ export default async function Tickets({
buttonHref="https://fienta.com/et/tipilan"
backgroundImage="/images/landing/compete_teaser.jpg"
backgroundOpacity={30}
+ soldOut={ticketSoldOut.cs2}
+ soldOutText={t("tickets.soldOut")}
className="border-b-0"
/>
diff --git a/src/app/[locale]/turniirid/cs2/page.tsx b/src/app/[locale]/turniirid/cs2/page.tsx
index 4ed6035..d7a7e2b 100644
--- a/src/app/[locale]/turniirid/cs2/page.tsx
+++ b/src/app/[locale]/turniirid/cs2/page.tsx
@@ -45,13 +45,6 @@ export default async function CS2Tournament({
{/* Buttons */}
-
- {t("cs2page.buyTicket")}
-
- {/* Buttons */}
-
-
- {t("lolpage.buyTicket")}
-
-
-
{/* SISSEJUHATUS */}
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
index d8bfdd3..9e619b4 100644
--- a/src/components/Header.tsx
+++ b/src/components/Header.tsx
@@ -89,8 +89,8 @@ const Header = ({ navItems }: HeaderProps) => {
diff --git a/src/components/HeroSection.tsx b/src/components/HeroSection.tsx
index b7fe1cf..b0f19af 100644
--- a/src/components/HeroSection.tsx
+++ b/src/components/HeroSection.tsx
@@ -14,6 +14,7 @@ export default function HeroSection() {
src="/images/landing/main_teaser.jpg"
alt=""
fill
+ sizes="100vw"
className="object-cover object-center"
priority
/>
@@ -56,10 +57,10 @@ export default function HeroSection() {
{
+ let timeoutId: number | undefined;
+
+ const revealWhenReady = () => {
+ const timeUntilStart = livestreamsStartAt - Date.now();
+
+ if (timeUntilStart <= 0) {
+ setTwitchParent(window.location.hostname);
+ setIsVisible(true);
+ return;
+ }
+
+ timeoutId = window.setTimeout(
+ revealWhenReady,
+ Math.min(timeUntilStart, maximumTimeout),
+ );
+ };
+
+ revealWhenReady();
+
+ return () => {
+ if (timeoutId !== undefined) {
+ window.clearTimeout(timeoutId);
+ }
+ };
+ }, []);
+
+ if (!isVisible || !twitchParent) {
+ return null;
+ }
+
+ return (
+
+
+ {t("title")}
+
+
+
+ {streams.map((stream) => {
+ const query = new URLSearchParams({
+ channel: stream.channel,
+ parent: twitchParent,
+ muted: "true",
+ });
+
+ return (
+
+
+ {t(stream.translationKey)}
+
+
+
+
+
+ );
+ })}
+
+
+ );
+}
diff --git a/src/components/SoldOutOverlay.tsx b/src/components/SoldOutOverlay.tsx
new file mode 100644
index 0000000..6f03370
--- /dev/null
+++ b/src/components/SoldOutOverlay.tsx
@@ -0,0 +1,17 @@
+import { vipnagorgialla } from "@/components/Vipnagorgialla";
+
+interface SoldOutOverlayProps {
+ text: string;
+}
+
+export default function SoldOutOverlay({ text }: SoldOutOverlayProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Sponsors.tsx b/src/components/Sponsors.tsx
index 5e39704..218a79f 100644
--- a/src/components/Sponsors.tsx
+++ b/src/components/Sponsors.tsx
@@ -310,7 +310,7 @@ export default function Sponsors({
return (
{showTitle && (
diff --git a/src/components/SwitchableTicketCard.tsx b/src/components/SwitchableTicketCard.tsx
index 62d210c..3895353 100644
--- a/src/components/SwitchableTicketCard.tsx
+++ b/src/components/SwitchableTicketCard.tsx
@@ -3,6 +3,7 @@
import { useState } from "react";
import Image from "next/image";
import Link from "next/link";
+import SoldOutOverlay from "@/components/SoldOutOverlay";
import { vipnagorgialla } from "@/components/Vipnagorgialla";
interface TicketData {
@@ -22,6 +23,9 @@ interface SwitchableTicketCardProps {
backgroundOpacity?: number;
backgroundOpacityA?: number;
backgroundOpacityB?: number;
+ soldOutA?: boolean;
+ soldOutB?: boolean;
+ soldOutText: string;
className?: string;
}
@@ -35,11 +39,15 @@ export default function SwitchableTicketCard({
backgroundOpacity = 40,
backgroundOpacityA,
backgroundOpacityB,
+ soldOutA = false,
+ soldOutB = false,
+ soldOutText,
className = "",
}: SwitchableTicketCardProps) {
const [isA, setIsA] = useState(true);
const titleOnRight = !isA;
+ const soldOut = isA ? soldOutA : soldOutB;
function renderPrice(price: string) {
return Array.from(price).map((char, index) => (
@@ -97,6 +105,7 @@ export default function SwitchableTicketCard({
src={backgroundImage}
alt=""
fill
+ sizes="(min-width: 1024px) 50vw, 100vw"
className="object-cover object-center transition-opacity duration-700 ease-out"
style={{ opacity: isA ? (backgroundOpacityA ?? backgroundOpacity) / 100 : 0 }}
/>
@@ -106,6 +115,7 @@ export default function SwitchableTicketCard({
src={backgroundImageB}
alt=""
fill
+ sizes="(min-width: 1024px) 50vw, 100vw"
className="object-cover object-center transition-opacity duration-700 ease-out"
style={{ opacity: isA ? 0 : (backgroundOpacityB ?? backgroundOpacity) / 100 }}
/>
@@ -163,13 +173,16 @@ export default function SwitchableTicketCard({
-
-
-
+
+ )}
@@ -275,33 +288,38 @@ export default function SwitchableTicketCard({
-
-
-
+
+ )}
+ {soldOut && }
+
{/* Chevron buttons on left/right sides */}
-
+
diff --git a/src/lib/loadRules.ts b/src/lib/loadRules.ts
index 7cde9de..7cc2f87 100644
--- a/src/lib/loadRules.ts
+++ b/src/lib/loadRules.ts
@@ -68,40 +68,15 @@ export async function loadRules(
}
/**
- * Loads rules using Bun.file (for Bun runtime environments)
- * @param ruleType - The type of rules to load
- * @param locale - The locale to load rules for
- * @returns Promise
The markdown content
+ * Backwards-compatible alias for callers that still use the old Bun-specific
+ * function name. Rule files are loaded through Node so this also works in the
+ * Next.js runtime.
*/
export async function loadRulesBun(
ruleType: RuleType,
locale: Locale,
): Promise {
- // Try to load the file for the current locale first
- let filePath = `src/data/rules/${locale}/${ruleType}.md`;
- let file = Bun.file(filePath);
-
- // Check if file exists, if not fallback to Estonian
- if (!(await file.exists()) && locale !== "et") {
- console.warn(
- `Rules file not found for ${ruleType} in ${locale}, falling back to Estonian`,
- );
- filePath = `src/data/rules/et/${ruleType}.md`;
- file = Bun.file(filePath);
- }
-
- try {
- const content = await file.text();
- return content;
- } catch (error) {
- console.error(
- `Error loading rules for ${ruleType} in locale ${locale}:`,
- error,
- );
- throw new Error(
- `Failed to load rules for ${ruleType}: ${error instanceof Error ? error.message : "Unknown error"}`,
- );
- }
+ return loadRules(ruleType, locale);
}
// TODO review unused method
diff --git a/translations/en.json b/translations/en.json
index 2c478d1..3e673be 100644
--- a/translations/en.json
+++ b/translations/en.json
@@ -67,6 +67,11 @@
"link": "/messiala"
}
},
+ "livestreams": {
+ "title": "TipiLAN livestreams",
+ "cs2": "Counter-Strike 2",
+ "lol": "League of Legends"
+ },
"sections": {
"schedule": {
"description": "TipiLAN is packed with exciting tournaments, mini-competitions and much more."
@@ -87,6 +92,7 @@
"title": "TICKETS",
"subtitle": "TICKET",
"buyButton": "BUY TICKET FROM FIENTA",
+ "soldOut": "SOLD OUT",
"visitor": {
"name": "VISITOR",
"price": "10€",
diff --git a/translations/et.json b/translations/et.json
index 0243e5f..254754c 100644
--- a/translations/et.json
+++ b/translations/et.json
@@ -67,6 +67,11 @@
"link": "/messiala"
}
},
+ "livestreams": {
+ "title": "TipiLANi otseülekanded",
+ "cs2": "Counter-Strike 2",
+ "lol": "League of Legends"
+ },
"sections": {
"schedule": {
"description": "TipiLAN on pungil põnevatest turniiridest, mini-võistlustest ja paljust muust."
@@ -87,6 +92,7 @@
"title": "PILETID",
"subtitle": "PILET",
"buyButton": "OSTA PILET FIENTAST",
+ "soldOut": "VÄLJA MÜÜDUD",
"visitor": {
"name": "KÜLASTAJA",
"price": "10€",
diff --git a/tsconfig.json b/tsconfig.json
index 9a6e0de..541fdd6 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -24,7 +24,7 @@
],
"types": [
"node",
- "bun-types"
+ "bun"
],
"paths": {
"@/*": [