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 RulesLastChanged from "@/components/RulesLastChanged";
|
||||||
import RuleNav from "@/components/RuleNav";
|
import RuleNav from "@/components/RuleNav";
|
||||||
import { getTranslations, setRequestLocale } from "next-intl/server";
|
import { getTranslations, setRequestLocale } from "next-intl/server";
|
||||||
import { loadRulesBun } from "@/lib/loadRules";
|
import { loadRules } from "@/lib/loadRules";
|
||||||
|
|
||||||
const sectionKeys = [
|
const sectionKeys = [
|
||||||
{ id: "reminder", labelKey: "rules.reminder" },
|
{ id: "reminder", labelKey: "rules.reminder" },
|
||||||
@@ -19,7 +19,7 @@ export default async function Page({
|
|||||||
const { locale } = await params;
|
const { locale } = await params;
|
||||||
setRequestLocale(locale);
|
setRequestLocale(locale);
|
||||||
const t = await getTranslations({ 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) => ({
|
const sections = sectionKeys.map((section) => ({
|
||||||
id: section.id,
|
id: section.id,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { vipnagorgialla } from "@/components/Vipnagorgialla";
|
|||||||
import Sponsors from "@/components/Sponsors";
|
import Sponsors from "@/components/Sponsors";
|
||||||
import HeroSection from "@/components/HeroSection";
|
import HeroSection from "@/components/HeroSection";
|
||||||
import TeaserCarousel from "@/components/TeaserCarousel";
|
import TeaserCarousel from "@/components/TeaserCarousel";
|
||||||
|
import Livestreams from "@/components/Livestreams";
|
||||||
import { Link } from "@/i18n/routing";
|
import { Link } from "@/i18n/routing";
|
||||||
import { getTranslations, setRequestLocale } from "next-intl/server";
|
import { getTranslations, setRequestLocale } from "next-intl/server";
|
||||||
|
|
||||||
@@ -21,8 +22,11 @@ export default async function Home({
|
|||||||
<HeroSection />
|
<HeroSection />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Nav cards: Piletid + Turniirid */}
|
{/* Livestreams — shown automatically when the event begins */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 md:h-65 border-b-3 border-[#1F5673]">
|
<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
|
<Link
|
||||||
href="/piletid"
|
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"
|
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>
|
</div>
|
||||||
</Link>
|
</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
|
<Link
|
||||||
href="/turniirid"
|
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"
|
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 Link from "next/link";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { getTranslations, setRequestLocale } from "next-intl/server";
|
import { getTranslations, setRequestLocale } from "next-intl/server";
|
||||||
|
import SoldOutOverlay from "@/components/SoldOutOverlay";
|
||||||
import SwitchableTicketCard from "@/components/SwitchableTicketCard";
|
import SwitchableTicketCard from "@/components/SwitchableTicketCard";
|
||||||
|
|
||||||
|
const ticketSoldOut = {
|
||||||
|
visitor: false,
|
||||||
|
supporter: false,
|
||||||
|
lan: false,
|
||||||
|
lol: true,
|
||||||
|
cs2: true,
|
||||||
|
} as const;
|
||||||
|
|
||||||
interface TicketCardProps {
|
interface TicketCardProps {
|
||||||
title: string;
|
title: string;
|
||||||
subtitle: string;
|
subtitle: string;
|
||||||
@@ -14,6 +23,8 @@ interface TicketCardProps {
|
|||||||
backgroundImage?: string;
|
backgroundImage?: string;
|
||||||
backgroundOpacity?: number;
|
backgroundOpacity?: number;
|
||||||
blueTinge?: boolean;
|
blueTinge?: boolean;
|
||||||
|
soldOut?: boolean;
|
||||||
|
soldOutText?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,17 +38,21 @@ function TicketCard({
|
|||||||
backgroundImage,
|
backgroundImage,
|
||||||
backgroundOpacity = 40,
|
backgroundOpacity = 40,
|
||||||
blueTinge = false,
|
blueTinge = false,
|
||||||
|
soldOut = false,
|
||||||
|
soldOutText = "",
|
||||||
className = "",
|
className = "",
|
||||||
}: TicketCardProps) {
|
}: TicketCardProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<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 && (
|
{backgroundImage && (
|
||||||
<Image
|
<Image
|
||||||
src={backgroundImage}
|
src={backgroundImage}
|
||||||
alt=""
|
alt=""
|
||||||
fill
|
fill
|
||||||
|
sizes="(min-width: 1024px) 50vw, 100vw"
|
||||||
className="object-cover object-center"
|
className="object-cover object-center"
|
||||||
style={{ opacity: backgroundOpacity / 100 }}
|
style={{ opacity: backgroundOpacity / 100 }}
|
||||||
/>
|
/>
|
||||||
@@ -83,14 +98,19 @@ function TicketCard({
|
|||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
<Link href={buttonHref} target="_blank" className="w-fit">
|
{!soldOut && (
|
||||||
<button
|
<Link
|
||||||
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`}
|
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}
|
{buttonText}
|
||||||
</button>
|
</Link>
|
||||||
</Link>
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{soldOut && <SoldOutOverlay text={soldOutText} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -129,6 +149,9 @@ export default async function Tickets({
|
|||||||
backgroundImageB="/images/landing/main_supporter.jpg"
|
backgroundImageB="/images/landing/main_supporter.jpg"
|
||||||
backgroundOpacityA={30}
|
backgroundOpacityA={30}
|
||||||
backgroundOpacityB={40}
|
backgroundOpacityB={40}
|
||||||
|
soldOutA={ticketSoldOut.visitor}
|
||||||
|
soldOutB={ticketSoldOut.supporter}
|
||||||
|
soldOutText={t("tickets.soldOut")}
|
||||||
className="border-b-[3px] lg:border-r-[3px]"
|
className="border-b-[3px] lg:border-r-[3px]"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -142,6 +165,8 @@ export default async function Tickets({
|
|||||||
buttonHref="https://fienta.com/et/tipilan"
|
buttonHref="https://fienta.com/et/tipilan"
|
||||||
backgroundImage="/images/landing/explore_teaser.png"
|
backgroundImage="/images/landing/explore_teaser.png"
|
||||||
backgroundOpacity={100}
|
backgroundOpacity={100}
|
||||||
|
soldOut={ticketSoldOut.lan}
|
||||||
|
soldOutText={t("tickets.soldOut")}
|
||||||
className="border-b-[3px]"
|
className="border-b-[3px]"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -154,6 +179,8 @@ export default async function Tickets({
|
|||||||
buttonText={t("tickets.buyButton")}
|
buttonText={t("tickets.buyButton")}
|
||||||
buttonHref="https://fienta.com/et/tipilan"
|
buttonHref="https://fienta.com/et/tipilan"
|
||||||
backgroundImage="/images/landing/league_ticket.jpg"
|
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]"
|
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"
|
buttonHref="https://fienta.com/et/tipilan"
|
||||||
backgroundImage="/images/landing/compete_teaser.jpg"
|
backgroundImage="/images/landing/compete_teaser.jpg"
|
||||||
backgroundOpacity={30}
|
backgroundOpacity={30}
|
||||||
|
soldOut={ticketSoldOut.cs2}
|
||||||
|
soldOutText={t("tickets.soldOut")}
|
||||||
className="border-b-0"
|
className="border-b-0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -45,13 +45,6 @@ export default async function CS2Tournament({
|
|||||||
|
|
||||||
{/* Buttons */}
|
{/* Buttons */}
|
||||||
<div className="flex flex-wrap gap-4 mb-12">
|
<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
|
<Link
|
||||||
href="https://git.edunaut.ee/slunk/TipiLAN_reeglistik_ruleset/src/branch/main/CS2%20tournament"
|
href="https://git.edunaut.ee/slunk/TipiLAN_reeglistik_ruleset/src/branch/main/CS2%20tournament"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { vipnagorgialla } from "@/components/Vipnagorgialla";
|
|||||||
import RuleNav from "@/components/RuleNav";
|
import RuleNav from "@/components/RuleNav";
|
||||||
import RulesContent from "@/components/RulesContent";
|
import RulesContent from "@/components/RulesContent";
|
||||||
import RulesLastChanged from "@/components/RulesLastChanged";
|
import RulesLastChanged from "@/components/RulesLastChanged";
|
||||||
import Link from "next/link";
|
|
||||||
import { getTranslations, setRequestLocale } from "next-intl/server";
|
import { getTranslations, setRequestLocale } from "next-intl/server";
|
||||||
|
|
||||||
const sectionKeys = [
|
const sectionKeys = [
|
||||||
@@ -42,17 +41,6 @@ export default async function LoLTournament({
|
|||||||
</h1>
|
</h1>
|
||||||
<RulesLastChanged locale={locale as "et" | "en"} ruleType="lol" />
|
<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 */}
|
{/* SISSEJUHATUS */}
|
||||||
<section id="intro" className="mb-12 scroll-mt-24 md:scroll-mt-28 text-justify">
|
<section id="intro" className="mb-12 scroll-mt-24 md:scroll-mt-28 text-justify">
|
||||||
<h2
|
<h2
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ function TournamentCard({
|
|||||||
src={backgroundImage}
|
src={backgroundImage}
|
||||||
alt=""
|
alt=""
|
||||||
fill
|
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"
|
className="object-cover opacity-50 transition-opacity duration-200 group-hover:opacity-40 group-focus-visible:opacity-40"
|
||||||
style={{ objectPosition }}
|
style={{ objectPosition }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -89,8 +89,8 @@ const Header = ({ navItems }: HeaderProps) => {
|
|||||||
<Image
|
<Image
|
||||||
src="/tipilan-icon-white.svg"
|
src="/tipilan-icon-white.svg"
|
||||||
alt="TipiLAN"
|
alt="TipiLAN"
|
||||||
width={40}
|
width={350}
|
||||||
height={40}
|
height={300}
|
||||||
className="h-auto w-10"
|
className="h-auto w-10"
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export default function HeroSection() {
|
|||||||
src="/images/landing/main_teaser.jpg"
|
src="/images/landing/main_teaser.jpg"
|
||||||
alt=""
|
alt=""
|
||||||
fill
|
fill
|
||||||
|
sizes="100vw"
|
||||||
className="object-cover object-center"
|
className="object-cover object-center"
|
||||||
priority
|
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">
|
<div className="flex flex-col sm:flex-row items-center justify-center md:justify-end gap-3 sm:gap-0 mt-2">
|
||||||
<Image
|
<Image
|
||||||
src="/images/landing/student_award.webp"
|
src="/images/landing/student_award.webp"
|
||||||
width={180}
|
width={512}
|
||||||
height={180}
|
height={439}
|
||||||
alt="TalTech student award"
|
alt="TalTech student award"
|
||||||
className="object-contain"
|
className="h-auto w-45 shrink-0 object-contain"
|
||||||
/>
|
/>
|
||||||
<p
|
<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`}
|
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 (
|
return (
|
||||||
<section
|
<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 && (
|
{showTitle && (
|
||||||
<h2 className="text-center text-2xl leading-none text-white uppercase sm:text-3xl lg:text-[40px]">
|
<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 { useState } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import SoldOutOverlay from "@/components/SoldOutOverlay";
|
||||||
import { vipnagorgialla } from "@/components/Vipnagorgialla";
|
import { vipnagorgialla } from "@/components/Vipnagorgialla";
|
||||||
|
|
||||||
interface TicketData {
|
interface TicketData {
|
||||||
@@ -22,6 +23,9 @@ interface SwitchableTicketCardProps {
|
|||||||
backgroundOpacity?: number;
|
backgroundOpacity?: number;
|
||||||
backgroundOpacityA?: number;
|
backgroundOpacityA?: number;
|
||||||
backgroundOpacityB?: number;
|
backgroundOpacityB?: number;
|
||||||
|
soldOutA?: boolean;
|
||||||
|
soldOutB?: boolean;
|
||||||
|
soldOutText: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,11 +39,15 @@ export default function SwitchableTicketCard({
|
|||||||
backgroundOpacity = 40,
|
backgroundOpacity = 40,
|
||||||
backgroundOpacityA,
|
backgroundOpacityA,
|
||||||
backgroundOpacityB,
|
backgroundOpacityB,
|
||||||
|
soldOutA = false,
|
||||||
|
soldOutB = false,
|
||||||
|
soldOutText,
|
||||||
className = "",
|
className = "",
|
||||||
}: SwitchableTicketCardProps) {
|
}: SwitchableTicketCardProps) {
|
||||||
const [isA, setIsA] = useState(true);
|
const [isA, setIsA] = useState(true);
|
||||||
|
|
||||||
const titleOnRight = !isA;
|
const titleOnRight = !isA;
|
||||||
|
const soldOut = isA ? soldOutA : soldOutB;
|
||||||
|
|
||||||
function renderPrice(price: string) {
|
function renderPrice(price: string) {
|
||||||
return Array.from(price).map((char, index) => (
|
return Array.from(price).map((char, index) => (
|
||||||
@@ -97,6 +105,7 @@ export default function SwitchableTicketCard({
|
|||||||
src={backgroundImage}
|
src={backgroundImage}
|
||||||
alt=""
|
alt=""
|
||||||
fill
|
fill
|
||||||
|
sizes="(min-width: 1024px) 50vw, 100vw"
|
||||||
className="object-cover object-center transition-opacity duration-700 ease-out"
|
className="object-cover object-center transition-opacity duration-700 ease-out"
|
||||||
style={{ opacity: isA ? (backgroundOpacityA ?? backgroundOpacity) / 100 : 0 }}
|
style={{ opacity: isA ? (backgroundOpacityA ?? backgroundOpacity) / 100 : 0 }}
|
||||||
/>
|
/>
|
||||||
@@ -106,6 +115,7 @@ export default function SwitchableTicketCard({
|
|||||||
src={backgroundImageB}
|
src={backgroundImageB}
|
||||||
alt=""
|
alt=""
|
||||||
fill
|
fill
|
||||||
|
sizes="(min-width: 1024px) 50vw, 100vw"
|
||||||
className="object-cover object-center transition-opacity duration-700 ease-out"
|
className="object-cover object-center transition-opacity duration-700 ease-out"
|
||||||
style={{ opacity: isA ? 0 : (backgroundOpacityB ?? backgroundOpacity) / 100 }}
|
style={{ opacity: isA ? 0 : (backgroundOpacityB ?? backgroundOpacity) / 100 }}
|
||||||
/>
|
/>
|
||||||
@@ -163,13 +173,16 @@ export default function SwitchableTicketCard({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="pb-16 flex-shrink-0">
|
<div className="pb-16 flex-shrink-0">
|
||||||
<Link href={buttonHref} target="_blank" className="w-fit">
|
{!soldOut && (
|
||||||
<button
|
<Link
|
||||||
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`}
|
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}
|
{buttonText}
|
||||||
</button>
|
</Link>
|
||||||
</Link>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -275,33 +288,38 @@ export default function SwitchableTicketCard({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Link href={buttonHref} target="_blank" className="w-fit">
|
{!soldOut && (
|
||||||
<button
|
<Link
|
||||||
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`}
|
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}
|
{buttonText}
|
||||||
</button>
|
</Link>
|
||||||
</Link>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{soldOut && <SoldOutOverlay text={soldOutText} />}
|
||||||
|
|
||||||
{/* Chevron buttons on left/right sides */}
|
{/* Chevron buttons on left/right sides */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsA(false)}
|
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}
|
aria-label={optionB.title}
|
||||||
>
|
>
|
||||||
<span className="material-symbols-outlined">chevron_right</span>
|
<span className="material-symbols-outlined">chevron_right</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsA(true)}
|
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}
|
aria-label={optionA.title}
|
||||||
>
|
>
|
||||||
<span className="material-symbols-outlined">chevron_left</span>
|
<span className="material-symbols-outlined">chevron_left</span>
|
||||||
</button>
|
</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
|
<button
|
||||||
onClick={() => setIsA(true)}
|
onClick={() => setIsA(true)}
|
||||||
className={`w-3 h-3 rounded-full transition ${
|
className={`w-3 h-3 rounded-full transition ${
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ export default function TeaserCarousel() {
|
|||||||
src={slide.image}
|
src={slide.image}
|
||||||
alt={slide.imageAlt}
|
alt={slide.imageAlt}
|
||||||
fill
|
fill
|
||||||
|
sizes="100vw"
|
||||||
className="object-cover object-center"
|
className="object-cover object-center"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -162,6 +163,7 @@ export default function TeaserCarousel() {
|
|||||||
src={slide.hero}
|
src={slide.hero}
|
||||||
alt={slide.imageAlt}
|
alt={slide.imageAlt}
|
||||||
fill
|
fill
|
||||||
|
sizes="50vw"
|
||||||
className="object-contain object-bottom"
|
className="object-contain object-bottom"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -68,40 +68,15 @@ export async function loadRules(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads rules using Bun.file (for Bun runtime environments)
|
* Backwards-compatible alias for callers that still use the old Bun-specific
|
||||||
* @param ruleType - The type of rules to load
|
* function name. Rule files are loaded through Node so this also works in the
|
||||||
* @param locale - The locale to load rules for
|
* Next.js runtime.
|
||||||
* @returns Promise<string> The markdown content
|
|
||||||
*/
|
*/
|
||||||
export async function loadRulesBun(
|
export async function loadRulesBun(
|
||||||
ruleType: RuleType,
|
ruleType: RuleType,
|
||||||
locale: Locale,
|
locale: Locale,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
// Try to load the file for the current locale first
|
return loadRules(ruleType, locale);
|
||||||
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"}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO review unused method
|
// TODO review unused method
|
||||||
|
|||||||
@@ -67,6 +67,11 @@
|
|||||||
"link": "/messiala"
|
"link": "/messiala"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"livestreams": {
|
||||||
|
"title": "TipiLAN livestreams",
|
||||||
|
"cs2": "Counter-Strike 2",
|
||||||
|
"lol": "League of Legends"
|
||||||
|
},
|
||||||
"sections": {
|
"sections": {
|
||||||
"schedule": {
|
"schedule": {
|
||||||
"description": "TipiLAN is packed with exciting tournaments, mini-competitions and much more."
|
"description": "TipiLAN is packed with exciting tournaments, mini-competitions and much more."
|
||||||
@@ -87,6 +92,7 @@
|
|||||||
"title": "TICKETS",
|
"title": "TICKETS",
|
||||||
"subtitle": "TICKET",
|
"subtitle": "TICKET",
|
||||||
"buyButton": "BUY TICKET FROM FIENTA",
|
"buyButton": "BUY TICKET FROM FIENTA",
|
||||||
|
"soldOut": "SOLD OUT",
|
||||||
"visitor": {
|
"visitor": {
|
||||||
"name": "VISITOR",
|
"name": "VISITOR",
|
||||||
"price": "10€",
|
"price": "10€",
|
||||||
|
|||||||
@@ -67,6 +67,11 @@
|
|||||||
"link": "/messiala"
|
"link": "/messiala"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"livestreams": {
|
||||||
|
"title": "TipiLANi otseülekanded",
|
||||||
|
"cs2": "Counter-Strike 2",
|
||||||
|
"lol": "League of Legends"
|
||||||
|
},
|
||||||
"sections": {
|
"sections": {
|
||||||
"schedule": {
|
"schedule": {
|
||||||
"description": "TipiLAN on pungil põnevatest turniiridest, mini-võistlustest ja paljust muust."
|
"description": "TipiLAN on pungil põnevatest turniiridest, mini-võistlustest ja paljust muust."
|
||||||
@@ -87,6 +92,7 @@
|
|||||||
"title": "PILETID",
|
"title": "PILETID",
|
||||||
"subtitle": "PILET",
|
"subtitle": "PILET",
|
||||||
"buyButton": "OSTA PILET FIENTAST",
|
"buyButton": "OSTA PILET FIENTAST",
|
||||||
|
"soldOut": "VÄLJA MÜÜDUD",
|
||||||
"visitor": {
|
"visitor": {
|
||||||
"name": "KÜLASTAJA",
|
"name": "KÜLASTAJA",
|
||||||
"price": "10€",
|
"price": "10€",
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
],
|
],
|
||||||
"types": [
|
"types": [
|
||||||
"node",
|
"node",
|
||||||
"bun-types"
|
"bun"
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": [
|
||||||
|
|||||||
Reference in New Issue
Block a user