mirror of
https://github.com/Lapikud/tipilan.git
synced 2026-09-22 15:22:59 +00:00
Merge pull request #148 from Lapikud/development
add scheduled livestreams and ticket availability
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
<HeroSection />
|
||||
</div>
|
||||
|
||||
{/* Nav cards: Piletid + Turniirid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 md:h-65 border-b-3 border-[#1F5673]">
|
||||
{/* Livestreams — shown automatically when the event begins */}
|
||||
<Livestreams />
|
||||
|
||||
{/* Nav cards: Piletid + Ajakava + Turniirid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 md:h-65 border-b-3 border-[#1F5673]">
|
||||
<Link
|
||||
href="/piletid"
|
||||
className="px-8 md:px-12 py-8 flex flex-col justify-center gap-4 border-b-3 md:border-b-0 md:border-r-3 group border-[#1F5673] hover:bg-[#007CAB] dark:hover:bg-[#00A3E0] transition"
|
||||
@@ -50,6 +54,33 @@ export default async function Home({
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/ajakava"
|
||||
className="px-8 md:px-12 py-8 flex flex-col justify-center gap-4 border-b-3 md:border-b-0 md:border-r-3 group border-[#1F5673] hover:bg-[#007CAB] dark:hover:bg-[#00A3E0] transition"
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<span
|
||||
className="material-symbols-outlined text-[clamp(2rem,1.5rem+1.5vw,3.5rem)]! text-[#007CAB] dark:text-[#00A3E0] dark:group-hover:text-[#EEE5E5] group-hover:text-[#EEE5E5]"
|
||||
style={{
|
||||
fontVariationSettings:
|
||||
'"FILL" 0, "wght" 700, "GRAD" 0, "opsz" 24',
|
||||
}}
|
||||
>
|
||||
calendar_month
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-row justify-between gap-4 items-center">
|
||||
<h2
|
||||
className={`text-[clamp(2rem,1.8rem+1vw,3rem)] ${vipnagorgialla.className} font-bold italic uppercase dark:text-[#EEE5E5] text-[#2A2C3F] group-hover:text-black dark:group-hover:text-[#2A2C3F]`}
|
||||
>
|
||||
{t("navigation.schedule")}
|
||||
</h2>
|
||||
<span className="material-symbols-outlined text-[clamp(2rem,1.5rem+1.5vw,3.5rem)]! font-bold! text-[#007CAB] dark:text-[#00A3E0] group-hover:translate-x-2 dark:group-hover:text-[#EEE5E5] group-hover:text-[#EEE5E5] transition">
|
||||
arrow_right_alt
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/turniirid"
|
||||
className="px-8 md:px-12 py-8 flex flex-col justify-center gap-4 group border-[#1F5673] hover:bg-[#007CAB] dark:hover:bg-[#00A3E0] transition"
|
||||
|
||||
@@ -2,8 +2,17 @@ import { vipnagorgialla } from "@/components/Vipnagorgialla";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { getTranslations, setRequestLocale } from "next-intl/server";
|
||||
import SoldOutOverlay from "@/components/SoldOutOverlay";
|
||||
import SwitchableTicketCard from "@/components/SwitchableTicketCard";
|
||||
|
||||
const ticketSoldOut = {
|
||||
visitor: false,
|
||||
supporter: false,
|
||||
lan: false,
|
||||
lol: true,
|
||||
cs2: true,
|
||||
} as const;
|
||||
|
||||
interface TicketCardProps {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
@@ -14,6 +23,8 @@ interface TicketCardProps {
|
||||
backgroundImage?: string;
|
||||
backgroundOpacity?: number;
|
||||
blueTinge?: boolean;
|
||||
soldOut?: boolean;
|
||||
soldOutText?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
@@ -27,17 +38,21 @@ function TicketCard({
|
||||
backgroundImage,
|
||||
backgroundOpacity = 40,
|
||||
blueTinge = false,
|
||||
soldOut = false,
|
||||
soldOutText = "",
|
||||
className = "",
|
||||
}: TicketCardProps) {
|
||||
return (
|
||||
<div
|
||||
className={`relative bg-[#0E0F19] border-[#1F5673] px-12 py-16 flex flex-col min-h-87.5 h-full ${className}`}
|
||||
className={`relative bg-[#0E0F19] border-[#1F5673] px-12 py-16 flex flex-col min-h-87.5 h-full overflow-hidden ${className}`}
|
||||
aria-disabled={soldOut || undefined}
|
||||
>
|
||||
{backgroundImage && (
|
||||
<Image
|
||||
src={backgroundImage}
|
||||
alt=""
|
||||
fill
|
||||
sizes="(min-width: 1024px) 50vw, 100vw"
|
||||
className="object-cover object-center"
|
||||
style={{ opacity: backgroundOpacity / 100 }}
|
||||
/>
|
||||
@@ -83,14 +98,19 @@ function TicketCard({
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Link href={buttonHref} target="_blank" className="w-fit">
|
||||
<button
|
||||
className={`px-4 py-2 border-4 border-transparent bg-[#00A3E0] hover:bg-[#E5E5EE] text-[#0A121F] cursor-pointer ${vipnagorgialla.className} font-bold italic leading-none uppercase transition`}
|
||||
{!soldOut && (
|
||||
<Link
|
||||
href={buttonHref}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={`w-fit px-4 py-2 border-4 border-transparent bg-[#00A3E0] hover:bg-[#E5E5EE] text-[#0A121F] cursor-pointer ${vipnagorgialla.className} font-bold italic leading-none uppercase transition`}
|
||||
>
|
||||
{buttonText}
|
||||
</button>
|
||||
</Link>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{soldOut && <SoldOutOverlay text={soldOutText} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -45,13 +45,6 @@ export default async function CS2Tournament({
|
||||
|
||||
{/* Buttons */}
|
||||
<div className="flex flex-wrap gap-4 mb-12">
|
||||
<Link
|
||||
href="https://fienta.com/et/tipilan"
|
||||
target="_blank"
|
||||
className={`${vipnagorgialla.className} font-bold italic leading-none text-lg px-4 py-3 border-4 border-transparent bg-[#00A3E0] hover:bg-[#E5E5EE] text-[#0A121F] uppercase transition`}
|
||||
>
|
||||
{t("cs2page.buyTicket")}
|
||||
</Link>
|
||||
<Link
|
||||
href="https://git.edunaut.ee/slunk/TipiLAN_reeglistik_ruleset/src/branch/main/CS2%20tournament"
|
||||
target="_blank"
|
||||
|
||||
@@ -2,7 +2,6 @@ import { vipnagorgialla } from "@/components/Vipnagorgialla";
|
||||
import RuleNav from "@/components/RuleNav";
|
||||
import RulesContent from "@/components/RulesContent";
|
||||
import RulesLastChanged from "@/components/RulesLastChanged";
|
||||
import Link from "next/link";
|
||||
import { getTranslations, setRequestLocale } from "next-intl/server";
|
||||
|
||||
const sectionKeys = [
|
||||
@@ -42,17 +41,6 @@ export default async function LoLTournament({
|
||||
</h1>
|
||||
<RulesLastChanged locale={locale as "et" | "en"} ruleType="lol" />
|
||||
|
||||
{/* Buttons */}
|
||||
<div className="flex flex-wrap gap-4 mb-12">
|
||||
<Link
|
||||
href="https://fienta.com/et/tipilan"
|
||||
target="_blank"
|
||||
className={`${vipnagorgialla.className} font-bold italic leading-none text-lg px-4 py-3 border-4 border-transparent bg-[#00A3E0] hover:bg-[#E5E5EE] text-[#0A121F] uppercase transition`}
|
||||
>
|
||||
{t("lolpage.buyTicket")}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* SISSEJUHATUS */}
|
||||
<section id="intro" className="mb-12 scroll-mt-24 md:scroll-mt-28 text-justify">
|
||||
<h2
|
||||
|
||||
@@ -28,6 +28,7 @@ function TournamentCard({
|
||||
src={backgroundImage}
|
||||
alt=""
|
||||
fill
|
||||
sizes="(min-width: 768px) 50vw, 100vw"
|
||||
className="object-cover opacity-50 transition-opacity duration-200 group-hover:opacity-40 group-focus-visible:opacity-40"
|
||||
style={{ objectPosition }}
|
||||
/>
|
||||
|
||||
@@ -89,8 +89,8 @@ const Header = ({ navItems }: HeaderProps) => {
|
||||
<Image
|
||||
src="/tipilan-icon-white.svg"
|
||||
alt="TipiLAN"
|
||||
width={40}
|
||||
height={40}
|
||||
width={350}
|
||||
height={300}
|
||||
className="h-auto w-10"
|
||||
/>
|
||||
</Link>
|
||||
|
||||
@@ -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() {
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center md:justify-end gap-3 sm:gap-0 mt-2">
|
||||
<Image
|
||||
src="/images/landing/student_award.webp"
|
||||
width={180}
|
||||
height={180}
|
||||
width={512}
|
||||
height={439}
|
||||
alt="TalTech student award"
|
||||
className="object-contain"
|
||||
className="h-auto w-45 shrink-0 object-contain"
|
||||
/>
|
||||
<p
|
||||
className={`text-[clamp(1.25rem,1rem+1.5vw,2rem)] leading-none tracking-normal uppercase text-center md:text-right align-middle text-[#EEE5E5] ${vipnagorgialla.className} font-bold italic px-2 sm:px-0`}
|
||||
|
||||
90
src/components/Livestreams.tsx
Normal file
90
src/components/Livestreams.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { vipnagorgialla } from "@/components/Vipnagorgialla";
|
||||
|
||||
const livestreamsStartAt = new Date("2026-09-11T00:00:00+03:00").getTime();
|
||||
const maximumTimeout = 2_147_483_647;
|
||||
|
||||
const streams = [
|
||||
{ channel: "tipilan_ee", translationKey: "cs2" },
|
||||
{ channel: "tipilan_ee2", translationKey: "lol" },
|
||||
] as const;
|
||||
|
||||
export default function Livestreams() {
|
||||
const t = useTranslations("home.livestreams");
|
||||
const [twitchParent, setTwitchParent] = useState("");
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
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 (
|
||||
<section
|
||||
className={`mx-auto flex w-full flex-col items-center gap-10 border-b-4 border-[#00A3E0]/50 px-4 py-11 sm:gap-12 sm:px-8 sm:py-14 lg:gap-16 lg:px-12 lg:py-[68px] ${vipnagorgialla.className} font-bold italic`}
|
||||
>
|
||||
<h2 className="text-center text-2xl leading-none text-white uppercase sm:text-3xl lg:text-[40px]">
|
||||
{t("title")}
|
||||
</h2>
|
||||
|
||||
<div className="grid w-full max-w-[1216px] grid-cols-1 gap-8 lg:grid-cols-2 lg:gap-12">
|
||||
{streams.map((stream) => {
|
||||
const query = new URLSearchParams({
|
||||
channel: stream.channel,
|
||||
parent: twitchParent,
|
||||
muted: "true",
|
||||
});
|
||||
|
||||
return (
|
||||
<article
|
||||
key={stream.channel}
|
||||
className="overflow-hidden border-3 border-[#1F5673] bg-[#0E0F19]"
|
||||
>
|
||||
<h3 className="px-5 py-4 text-xl leading-none text-[#00A3E0] uppercase sm:px-6 sm:text-2xl">
|
||||
{t(stream.translationKey)}
|
||||
</h3>
|
||||
<div className="relative aspect-video w-full">
|
||||
<iframe
|
||||
src={`https://player.twitch.tv/?${query.toString()}`}
|
||||
title={`${t(stream.translationKey)} — Twitch`}
|
||||
className="absolute inset-0 size-full"
|
||||
allow="autoplay; fullscreen"
|
||||
allowFullScreen
|
||||
/>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
17
src/components/SoldOutOverlay.tsx
Normal file
17
src/components/SoldOutOverlay.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { vipnagorgialla } from "@/components/Vipnagorgialla";
|
||||
|
||||
interface SoldOutOverlayProps {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export default function SoldOutOverlay({ text }: SoldOutOverlayProps) {
|
||||
return (
|
||||
<div className="absolute inset-0 z-20 flex cursor-not-allowed select-none items-center justify-center overflow-hidden bg-[#0E0F19]/40">
|
||||
<p
|
||||
className={`${vipnagorgialla.className} -rotate-[15deg] whitespace-nowrap text-[clamp(2.5rem,5vw,5.5rem)] leading-none font-bold italic text-[#00A3E0] uppercase`}
|
||||
>
|
||||
{text}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -310,7 +310,7 @@ export default function Sponsors({
|
||||
|
||||
return (
|
||||
<section
|
||||
className={`mx-auto flex w-full flex-col items-center gap-10 border-t-4 border-[#00A3E0]/50 px-4 pb-10 pt-11 sm:gap-12 sm:px-8 sm:pb-12 sm:pt-14 lg:gap-16 lg:px-12 lg:pb-16 lg:pt-[68px] ${vipnagorgialla.className} font-bold italic ${className}`}
|
||||
className={`mx-auto flex w-full flex-col items-center gap-10 px-4 pb-10 pt-11 sm:gap-12 sm:px-8 sm:pb-12 sm:pt-14 lg:gap-16 lg:px-12 lg:pb-16 lg:pt-[68px] ${vipnagorgialla.className} font-bold italic ${className}`}
|
||||
>
|
||||
{showTitle && (
|
||||
<h2 className="text-center text-2xl leading-none text-white uppercase sm:text-3xl lg:text-[40px]">
|
||||
|
||||
@@ -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({
|
||||
</div>
|
||||
</div>
|
||||
<div className="pb-16 flex-shrink-0">
|
||||
<Link href={buttonHref} target="_blank" className="w-fit">
|
||||
<button
|
||||
className={`px-4 py-2 border-4 border-transparent bg-[#00A3E0] hover:bg-[#E5E5EE] text-[#0A121F] cursor-pointer ${vipnagorgialla.className} font-bold italic leading-none uppercase transition`}
|
||||
{!soldOut && (
|
||||
<Link
|
||||
href={buttonHref}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={`w-fit px-4 py-2 border-4 border-transparent bg-[#00A3E0] hover:bg-[#E5E5EE] text-[#0A121F] cursor-pointer ${vipnagorgialla.className} font-bold italic leading-none uppercase transition`}
|
||||
>
|
||||
{buttonText}
|
||||
</button>
|
||||
</Link>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -275,33 +288,38 @@ export default function SwitchableTicketCard({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Link href={buttonHref} target="_blank" className="w-fit">
|
||||
<button
|
||||
className={`px-4 py-2 border-4 border-transparent bg-[#00A3E0] hover:bg-[#E5E5EE] text-[#0A121F] cursor-pointer ${vipnagorgialla.className} font-bold italic leading-none uppercase transition`}
|
||||
{!soldOut && (
|
||||
<Link
|
||||
href={buttonHref}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={`w-fit px-4 py-2 border-4 border-transparent bg-[#00A3E0] hover:bg-[#E5E5EE] text-[#0A121F] cursor-pointer ${vipnagorgialla.className} font-bold italic leading-none uppercase transition`}
|
||||
>
|
||||
{buttonText}
|
||||
</button>
|
||||
</Link>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{soldOut && <SoldOutOverlay text={soldOutText} />}
|
||||
|
||||
{/* Chevron buttons on left/right sides */}
|
||||
<button
|
||||
onClick={() => setIsA(false)}
|
||||
className={`${isA ? "flex" : "hidden"} absolute right-0 top-1/2 -translate-y-1/2 w-10 h-10 items-center justify-center cursor-pointer bg-[#0E0F19]/50 hover:bg-[#007CAB] text-[#EEE5E5] transition-all duration-300 z-20`}
|
||||
className={`${isA ? "flex" : "hidden"} absolute right-0 top-1/2 -translate-y-1/2 w-10 h-10 items-center justify-center cursor-pointer bg-[#0E0F19]/50 hover:bg-[#007CAB] text-[#EEE5E5] transition-all duration-300 z-30`}
|
||||
aria-label={optionB.title}
|
||||
>
|
||||
<span className="material-symbols-outlined">chevron_right</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsA(true)}
|
||||
className={`${!isA ? "flex" : "hidden"} absolute left-0 top-1/2 -translate-y-1/2 w-10 h-10 items-center justify-center cursor-pointer bg-[#0E0F19]/50 hover:bg-[#007CAB] text-[#EEE5E5] transition-all duration-300 z-20`}
|
||||
className={`${!isA ? "flex" : "hidden"} absolute left-0 top-1/2 -translate-y-1/2 w-10 h-10 items-center justify-center cursor-pointer bg-[#0E0F19]/50 hover:bg-[#007CAB] text-[#EEE5E5] transition-all duration-300 z-30`}
|
||||
aria-label={optionA.title}
|
||||
>
|
||||
<span className="material-symbols-outlined">chevron_left</span>
|
||||
</button>
|
||||
|
||||
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 flex justify-center gap-2 pb-2 z-20">
|
||||
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 flex justify-center gap-2 pb-2 z-30">
|
||||
<button
|
||||
onClick={() => setIsA(true)}
|
||||
className={`w-3 h-3 rounded-full transition ${
|
||||
|
||||
@@ -112,6 +112,7 @@ export default function TeaserCarousel() {
|
||||
src={slide.image}
|
||||
alt={slide.imageAlt}
|
||||
fill
|
||||
sizes="100vw"
|
||||
className="object-cover object-center"
|
||||
/>
|
||||
|
||||
@@ -162,6 +163,7 @@ export default function TeaserCarousel() {
|
||||
src={slide.hero}
|
||||
alt={slide.imageAlt}
|
||||
fill
|
||||
sizes="50vw"
|
||||
className="object-contain object-bottom"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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<string> 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<string> {
|
||||
// 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
|
||||
|
||||
@@ -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€",
|
||||
|
||||
@@ -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€",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
],
|
||||
"types": [
|
||||
"node",
|
||||
"bun-types"
|
||||
"bun"
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
|
||||
Reference in New Issue
Block a user