mirror of
https://github.com/Lapikud/tipilan.git
synced 2026-03-23 21:34:21 +00:00
Initial commit
This commit is contained in:
@@ -1,77 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { vipnagorgialla } from "@/components/Vipnagorgialla";
|
||||
import { scheduleData } from "@/data/timetable";
|
||||
import SectionDivider from "@/components/SectionDivider";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const tabs = Object.keys(scheduleData);
|
||||
|
||||
export default function Timetable() {
|
||||
const [activeTab, setActiveTab] = useState(tabs[0]);
|
||||
const schedule = scheduleData[activeTab];
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col min-h-[90vh] m-6 mt-16 md:m-16">
|
||||
<h1
|
||||
className={`text-4xl md:text-5xl lg:text-6xl ${vipnagorgialla.className} font-bold italic uppercase text-[#2A2C3F] dark:text-[#EEE5E5] mt-8 md:mt-16 mb-8`}
|
||||
>
|
||||
{t("schedule.title")}
|
||||
</h1>
|
||||
|
||||
{/* Tab menu */}
|
||||
<div className="flex gap-4 mb-8 flex-wrap">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
className={`${vipnagorgialla.className} cursor-pointer uppercase italic px-4 py-2 text-lg font-semibold ${
|
||||
activeTab === tab
|
||||
? "bg-[#00A3E0] text-white"
|
||||
: "bg-[#007CAB] dark:bg-[#007CAB] text-[#EEE5E5] hover:bg-[#00A3E0] dark:hover:bg-[#007CAB]"
|
||||
} transition-colors`}
|
||||
>
|
||||
{t(`schedule.${tab}`)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Schedule entries */}
|
||||
<div className="space-y-6">
|
||||
{schedule.map((item, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="border-l-3 border-[#007CAB] pl-4 flex flex-col sm:flex-row flex-wrap gap-2 sm:gap-5 items-stretch"
|
||||
>
|
||||
<div
|
||||
className={`${vipnagorgialla.className} md:w-[180px] w-30 text-[#00A3E0] text-3xl md:text-4xl font-bold italic flex-shrink-0 flex items-center sm:justify-center`}
|
||||
>
|
||||
{item.time}
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col justify-center min-w-0 sm:min-h-[120px]">
|
||||
<div
|
||||
className={`${vipnagorgialla.className} text-2xl md:text-3xl italic font-bold text-[#2A2C3F] dark:text-[#EEE5E5] text-balance`}
|
||||
>
|
||||
{t(item.titleKey)}
|
||||
</div>
|
||||
{item.description && (
|
||||
<div className="text-xl md:text-2xl text-[#938BA1] dark:text-[#938BA1] text-balance">
|
||||
{item.description}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-xl md:text-2xl text-[#938BA1] dark:text-[#938BA1] text-balance">
|
||||
{t(item.locationKey)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SectionDivider />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
// Fonts
|
||||
import { vipnagorgialla } from "@/components/Vipnagorgialla";
|
||||
|
||||
// Database
|
||||
import { db } from "@/db/drizzle";
|
||||
|
||||
// Types
|
||||
import type { TeamWithMembers, MemberWithUser } from "@/types/database";
|
||||
|
||||
import { Link } from "@/i18n/routing";
|
||||
import { getTranslations, setRequestLocale } from "next-intl/server";
|
||||
|
||||
// User interface
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
|
||||
// Function to translate roles using i18n
|
||||
function translateRole(role: string, t: (key: string) => string): string {
|
||||
switch (role) {
|
||||
case "CAPTAIN":
|
||||
return t("admin.roles.captain");
|
||||
case "TEAMMATE":
|
||||
return t("admin.roles.teammate");
|
||||
default:
|
||||
return role;
|
||||
}
|
||||
}
|
||||
|
||||
export default async function AdminTeams({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>;
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
setRequestLocale(locale);
|
||||
const t = await getTranslations({ locale });
|
||||
// Fetch teams with their members and member users
|
||||
const teams = await db.query.teams.findMany({
|
||||
with: {
|
||||
members: {
|
||||
with: {
|
||||
user: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-h-[90vh] m-6 mt-16 md:m-16">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href={"/haldus"}>
|
||||
<span className="material-symbols-outlined !text-[clamp(2rem,1.5rem+1.5vw,3.5rem)] !font-bold text-[#007CAB] dark:text-[#00A3E0] translate-y-2.5 hover:-translate-x-2 dark:hover:text-[#EEE5E5] hover:text-[#2A2C3F] transition">
|
||||
arrow_left_alt
|
||||
</span>
|
||||
</Link>
|
||||
<h1
|
||||
className={`text-5xl sm:text-6xl ${vipnagorgialla.className} font-bold italic uppercase text-[#2A2C3F] dark:text-[#EEE5E5] mt-8 mb-4`}
|
||||
>
|
||||
{t("admin.title")} - {t("admin.teams")}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="text-2xl text-[#2A2C3F] dark:text-[#EEE5E5]">
|
||||
<div>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[100px]">ID</TableHead>
|
||||
<TableHead>{t("admin.table.name")}</TableHead>
|
||||
<TableHead>{t("admin.table.members")}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{teams.map((team: TeamWithMembers) => (
|
||||
<TableRow key={team.id}>
|
||||
<TableCell className="font-medium">{team.id}</TableCell>
|
||||
<TableCell>{team.name}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-col gap-1">
|
||||
{team.members && team.members.length > 0 ? (
|
||||
team.members.map((member: MemberWithUser) => (
|
||||
<div
|
||||
key={member.id}
|
||||
className="flex items-center gap-2 text-sm"
|
||||
>
|
||||
<span className="font-semibold">
|
||||
{member.user.firstName} {member.user.lastName}
|
||||
</span>
|
||||
<span className="text-gray-500">
|
||||
({translateRole(member.role, t)})
|
||||
</span>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<span className="text-gray-500">
|
||||
{t("admin.table.noMembers")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
// Fonts
|
||||
import { vipnagorgialla } from "@/components/Vipnagorgialla";
|
||||
|
||||
// Database
|
||||
import { db } from "@/db/drizzle";
|
||||
import { syncFientaEvent } from "@/lib/fienta";
|
||||
|
||||
// Enviornment variables
|
||||
import("dotenv");
|
||||
|
||||
// User interface
|
||||
import {
|
||||
Users,
|
||||
IdCardLanyard,
|
||||
DatabaseBackup,
|
||||
CheckCircle2Icon,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
|
||||
import { getTranslations, setRequestLocale } from "next-intl/server";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { redirect, RedirectType } from "next/navigation";
|
||||
import NextLink from "next/link";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
|
||||
import { DataTable } from "@/components/haldus/data-table";
|
||||
import { columns } from "@/components/haldus/columns";
|
||||
|
||||
async function syncAction() {
|
||||
"use server";
|
||||
await syncFientaEvent(process.env.EVENT_ID!, process.env.FIENTA_API_KEY!);
|
||||
|
||||
// Revalidate due to data change
|
||||
revalidatePath("/haldus");
|
||||
redirect("/haldus?success=true");
|
||||
}
|
||||
|
||||
async function dismissAlert() {
|
||||
"use server";
|
||||
redirect("/haldus", RedirectType.replace);
|
||||
}
|
||||
|
||||
const SuccessAlertDB = ({ t }: { t: (key: string) => string }) => {
|
||||
return (
|
||||
<Alert className="flex items-start mt-8">
|
||||
<CheckCircle2Icon className="mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<AlertTitle>{t("admin.success.title")}</AlertTitle>
|
||||
<AlertDescription>{t("admin.success.description")}</AlertDescription>
|
||||
</div>
|
||||
<form action={dismissAlert} className="ml-2">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<X className="" />
|
||||
</Button>
|
||||
</form>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
export default async function Admin({
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>;
|
||||
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
setRequestLocale(locale);
|
||||
const t = await getTranslations({ locale });
|
||||
const alarmStatus = await searchParams;
|
||||
const showSuccess = alarmStatus.success === "true";
|
||||
|
||||
// Fetch users
|
||||
const usersData = await db.query.users.findMany({
|
||||
with: {
|
||||
members: {
|
||||
with: {
|
||||
team: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const teamsData = await db.query.teams.findMany();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-h-[90vh] m-6 mt-16 md:m-16">
|
||||
{showSuccess && <SuccessAlertDB t={t} />}
|
||||
<div className="flex items-center gap-4">
|
||||
<NextLink href={"/"}>
|
||||
<span className="material-symbols-outlined !text-[clamp(2rem,1.5rem+1.5vw,3.5rem)] !font-bold text-[#007CAB] dark:text-[#00A3E0] translate-y-2.5 hover:-translate-x-2 dark:hover:text-[#EEE5E5] hover:text-[#2A2C3F] transition">
|
||||
arrow_left_alt
|
||||
</span>
|
||||
</NextLink>
|
||||
<h1
|
||||
className={`text-5xl sm:text-6xl ${vipnagorgialla.className} font-bold italic uppercase text-[#2A2C3F] dark:text-[#EEE5E5] mt-8 mb-4`}
|
||||
>
|
||||
{t("admin.title")}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="text-2xl text-[#2A2C3F] dark:text-[#EEE5E5]">
|
||||
<div className="pl-2 flex gap-8 pb-4">
|
||||
<div className="flex text-lg md:text-2xl flex-row items-center">
|
||||
<Users className="mr-2" />
|
||||
{t("admin.users")}: {usersData.length}
|
||||
</div>
|
||||
<NextLink href="/haldus/meeskonnad" className="flex items-center">
|
||||
<div className="flex text-lg md:text-2xl flex-row items-center">
|
||||
<IdCardLanyard className="mr-2" />
|
||||
{t("admin.teams")}: {teamsData.length}
|
||||
</div>
|
||||
</NextLink>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<div className="ml-auto">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-12 cursor-pointer"
|
||||
>
|
||||
<DatabaseBackup className="scale-150" />
|
||||
</Button>
|
||||
</div>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogTitle>{t("admin.sync.title")}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t("admin.sync.description1")}{" "}
|
||||
<span className="text-red-600 font-semibold">
|
||||
{t("admin.sync.all")}
|
||||
</span>{" "}
|
||||
{t("admin.sync.description2")}
|
||||
<br />
|
||||
<br />
|
||||
{t("admin.sync.warning")}
|
||||
</AlertDialogDescription>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel className="cursor-pointer">
|
||||
{t("common.cancel")}
|
||||
</AlertDialogCancel>
|
||||
<form action={syncAction}>
|
||||
<AlertDialogAction type="submit" className="cursor-pointer">
|
||||
{t("admin.sync.update")}
|
||||
</AlertDialogAction>
|
||||
</form>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
<div>
|
||||
<DataTable columns={columns} data={usersData} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
// app/kodukord/page.tsx (App Router)
|
||||
import ReactMarkdown, { Components } from "react-markdown";
|
||||
import { vipnagorgialla } from "@/components/Vipnagorgialla";
|
||||
import SectionDivider from "@/components/SectionDivider";
|
||||
import { getTranslations, setRequestLocale } from "next-intl/server";
|
||||
import { loadRulesBun } from "@/lib/loadRules";
|
||||
|
||||
export default async function Page({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>;
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
setRequestLocale(locale);
|
||||
const t = await getTranslations({ locale });
|
||||
const content = await loadRulesBun("kodukord", locale as "et" | "en");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col min-h-[90vh] m-6 mt-16 md:m-16">
|
||||
{/* Page title (separate from markdown headings) */}
|
||||
<h1
|
||||
className={`text-4xl md:text-5xl lg:text-6xl ${vipnagorgialla.className} font-bold italic text-[#2A2C3F] dark:text-[#EEE5E5] mt-8 md:mt-16 mb-4 uppercase`}
|
||||
>
|
||||
{t("rules.houseRules")}
|
||||
</h1>
|
||||
|
||||
<div className="prose prose-lg dark:prose-invert max-w-none">
|
||||
<ReactMarkdown
|
||||
components={
|
||||
{
|
||||
h1: (props) => (
|
||||
<h1 className="text-3xl md:text-4xl font-bold my-4">
|
||||
{props.children}
|
||||
</h1>
|
||||
),
|
||||
h2: (props) => (
|
||||
<h2 className="text-2xl md:text-3xl font-semibold my-3">
|
||||
{props.children}
|
||||
</h2>
|
||||
),
|
||||
ol: (props) => (
|
||||
<ol className="list-decimal ml-6 md:text-xl">
|
||||
{props.children}
|
||||
</ol>
|
||||
),
|
||||
ul: (props) => (
|
||||
<ul className="list-disc ml-6 md:text-xl">
|
||||
{props.children}
|
||||
</ul>
|
||||
),
|
||||
p: (props) => <p className="md:text-xl">{props.children}</p>,
|
||||
} as Components
|
||||
}
|
||||
>
|
||||
{content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SectionDivider />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
import { setRequestLocale, getMessages } from "next-intl/server";
|
||||
import { ThemeProvider } from "@/components/Theme-provider";
|
||||
import SidebarParent from "@/components/SidebarParent";
|
||||
import Footer from "@/components/Footer";
|
||||
|
||||
export default async function LocaleLayout({
|
||||
children,
|
||||
@@ -22,16 +19,7 @@ export default async function LocaleLayout({
|
||||
return (
|
||||
<div lang={locale}>
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<SidebarParent />
|
||||
{children}
|
||||
<Footer />
|
||||
</ThemeProvider>
|
||||
{children}
|
||||
</NextIntlClientProvider>
|
||||
</div>
|
||||
);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,17 +1,14 @@
|
||||
import { vipnagorgialla } from "@/components/Vipnagorgialla";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
export default async function NotFound() {
|
||||
const t = await getTranslations("notFound");
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-h-[90vh] p-12 justify-center items-center">
|
||||
<h1
|
||||
className={`text-7xl ${vipnagorgialla.className} font-bold italic uppercase text-[#2A2C3F] dark:text-[#EEE5E5] mt-8 mb-4`}
|
||||
>
|
||||
<div className="flex flex-col min-h-dvh p-12 justify-center items-center bg-bg-dark text-text-light">
|
||||
<h1 className="text-title">
|
||||
{t("title")}
|
||||
</h1>
|
||||
<p className="text-2xl text-[#2A2C3F] dark:text-[#EEE5E5] mb-8">
|
||||
<p className="text-p-lg mt-4">
|
||||
{t("message")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { vipnagorgialla } from "@/components/Vipnagorgialla";
|
||||
import Sponsors from "@/components/Sponsors";
|
||||
import { Link } from "@/i18n/routing";
|
||||
import { getTranslations, setRequestLocale } from "next-intl/server";
|
||||
import Image from "next/image";
|
||||
import { setRequestLocale } from "next-intl/server";
|
||||
import TeaserPage from "@/components/TeaserPage";
|
||||
|
||||
export default async function Home({
|
||||
params,
|
||||
@@ -11,150 +8,6 @@ export default async function Home({
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
setRequestLocale(locale);
|
||||
const t = await getTranslations({ locale });
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Title */}
|
||||
<div className="border-b-3 border-[#1F5673] grid grid-cols-1 md:grid-cols-[2fr_1fr] items-center justify-between mt-18 gap-12 py-8">
|
||||
<Image
|
||||
src="/tipilan-white.svg"
|
||||
width={850}
|
||||
height={120}
|
||||
alt="TipiLAN Logo"
|
||||
className="px-8 py-8 md:px-12 md:py-14 dark:hidden w-[max(300px,min(100%,850px))] h-auto"
|
||||
/>
|
||||
<Image
|
||||
src="/tipilan-dark.svg"
|
||||
width={850}
|
||||
height={120}
|
||||
alt="TipiLAN Logo"
|
||||
className="px-8 py-8 md:px-12 md:py-14 not-dark:hidden w-[max(300px,min(100%,850px))] h-auto2"
|
||||
/>
|
||||
<div className="pr-12 hidden md:block text-right">
|
||||
<h3
|
||||
className={`text-[clamp(1.25rem,0.75rem+2.5vw,3.75rem)] ${vipnagorgialla.className} leading-[90%] font-bold italic uppercase dark:text-[#EEE5E5] text-[#2A2C3F]`}
|
||||
>
|
||||
{t("tournaments.prizePool")}
|
||||
</h3>
|
||||
<h2
|
||||
className={`text-[clamp(2rem,1.2rem+4vw,6rem)] ${vipnagorgialla.className} leading-[90%] font-bold italic text-[#007CAB] dark:text-[#00A3E0]`}
|
||||
>
|
||||
10 000€
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Farewell message */}
|
||||
<div>
|
||||
<section
|
||||
className={`p-8 md:p-12 flex flex-col ${vipnagorgialla.className} font-bold italic border-b-3 border-[#1F5673] hover:bg-[#007CAB] dark:hover:bg-[#00A3E0] group transition`}
|
||||
>
|
||||
<h2 className="text-[clamp(2rem,1.5rem+0.5vw,3rem)] text-[#007CAB] dark:text-[#00A3E0] dark:group-hover:text-[#EEE5E5] group-hover:text-[#EEE5E5]">
|
||||
{t("home.sections.farewellMessage")} <span className="not-italic">🩵</span>
|
||||
</h2>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* Grid of buttons */}
|
||||
<div className="grid grid-cols-1 xl:grid-cols-3 border-[#1F5673]">
|
||||
<Link
|
||||
href="/ajakava"
|
||||
className="px-8 md:px-12 py-8 flex flex-col gap-4 border-b-3 lg:border-r-3 group border-[#1F5673] hover:bg-[#007CAB] dark:hover:bg-[#00A3E0] transition"
|
||||
>
|
||||
<div className="cursor-pointer 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>
|
||||
<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]">
|
||||
event_note
|
||||
</span>
|
||||
<p className="text-[clamp(0.875rem,0.75rem+0.5vw,1.25rem)] tracking-[-0.045rem] dark:group-hover:text-[#2A2C3F] group-hover:text-black">
|
||||
{t("home.sections.schedule.description")}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
<Link
|
||||
href="/turniirid"
|
||||
className="px-8 md:px-12 py-8 flex flex-col gap-4 border-b-3 lg:border-r-3 group border-[#1F5673] hover:bg-[#007CAB] dark:hover:bg-[#00A3E0] transition"
|
||||
>
|
||||
<div className="cursor-pointer flex flex-row justify-between gap-4 items-center">
|
||||
<h2
|
||||
className={`text-[clamp(2rem,1.8rem+1vw,3rem)] ${vipnagorgialla.className} font-bold italic break-normal uppercase dark:text-[#EEE5E5] text-[#2A2C3F] dark:group-hover:text-[#2A2C3F] group-hover:text-black`}
|
||||
>
|
||||
{t("navigation.tournaments")}
|
||||
</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>
|
||||
|
||||
<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]">
|
||||
trophy
|
||||
</span>
|
||||
<p className="text-[clamp(0.875rem,0.75rem+0.5vw,1.25rem)] tracking-[-0.045rem] dark:group-hover:text-[#2A2C3F] group-hover:text-black">
|
||||
{t("home.sections.tournaments.description")}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
<Link
|
||||
href="/messiala"
|
||||
className="px-8 md:px-12 py-8 flex flex-col gap-4 border-b-3 border-[#1F5673] group hover:bg-[#007CAB] dark:hover:bg-[#00A3E0] transition-all"
|
||||
>
|
||||
<div className="cursor-pointer 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] dark:group-hover:text-[#2A2C3F] group-hover:text-black`}
|
||||
>
|
||||
{t("navigation.expo")}
|
||||
</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>
|
||||
<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]">
|
||||
weekend
|
||||
</span>
|
||||
<p className="text-[clamp(0.875rem,0.75rem+0.5vw,1.25rem)] tracking-[-0.045rem] dark:group-hover:text-[#2A2C3F] group-hover:text-black">
|
||||
{t("home.sections.expo.description")}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Section preserved for next year development */}
|
||||
{/* Date */}
|
||||
{/* <div>*/}
|
||||
{/* <Link*/}
|
||||
{/* href="/piletid"*/}
|
||||
{/* className={`p-8 md:p-12 flex flex-col ${vipnagorgialla.className} font-bold italic border-b-3 border-[#1F5673] hover:bg-[#007CAB] dark:hover:bg-[#00A3E0] group transition`}*/}
|
||||
{/* >*/}
|
||||
{/* <div className="cursor-pointer text-left flex flex-row justify-between xl:justify-start gap-8">*/}
|
||||
{/* <h3 className="text-4xl md:text-5xl dark:text-[#EEE5E5] dark:group-hover:text-[#2A2C3F] text-[#2A2C3F] group-hover:text-black">*/}
|
||||
{/* {t("home.sections.reserveSpot")}*/}
|
||||
{/* </h3>*/}
|
||||
{/* <span*/}
|
||||
{/* className="material-symbols-outlined !text-[clamp(2rem,1.5rem+1.5vw,3.5rem)] !font-bold text-[#007CAB] dark:text-[#00A3E0] hidden md:block group-hover:translate-x-2 group-hover:text-[#EEE5E5] dark:group-hover:text-[#EEE5E5] transition">*/}
|
||||
{/* arrow_right_alt*/}
|
||||
{/* </span>*/}
|
||||
{/* </div>*/}
|
||||
{/* <h2 className="text-[clamp(2.5rem,2.25rem+1.25vw,3.75rem)] text-[#007CAB] dark:text-[#00A3E0] dark:group-hover:text-[#EEE5E5] group-hover:text-[#EEE5E5]">*/}
|
||||
{/* {t("home.sections.dateAndLocation")}*/}
|
||||
{/* </h2>*/}
|
||||
{/* </Link>*/}
|
||||
{/* </div>*/}
|
||||
|
||||
{/* Sponsors */}
|
||||
<Sponsors />
|
||||
|
||||
</div>
|
||||
);
|
||||
return <TeaserPage />;
|
||||
}
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
import { vipnagorgialla } from "@/components/Vipnagorgialla";
|
||||
import Link from "next/link";
|
||||
import SectionDivider from "@/components/SectionDivider";
|
||||
import { getTranslations, setRequestLocale } from "next-intl/server";
|
||||
|
||||
export default async function Tickets({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>;
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
setRequestLocale(locale);
|
||||
const t = await getTranslations({ locale });
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col min-h-[90vh] m-6 mt-16 md:m-16">
|
||||
<h1
|
||||
className={`text-4xl wrap-break-word md:text-5xl lg:text-6xl ${vipnagorgialla.className} font-bold italic text-[#2A2C3F] dark:text-[#EEE5E5] mt-8 md:mt-16 mb-4`}
|
||||
>
|
||||
{t("tickets.title")}
|
||||
</h1>
|
||||
|
||||
<div className="flex justify-center lg:items-center flex-col lg:flex-row gap-8 md:gap-12 flex-grow mb-16 md:mt-8 lg:mt-0">
|
||||
<div className="bg-[#007CAB] -skew-x-2 md:-skew-x-5 text-white italic px-8 md:px-12 py-16 hover:scale-103 transition-all duration-150 w-full md:w-xl lg:w-[400px]">
|
||||
<h2
|
||||
className={`text-6xl ${vipnagorgialla.className} font-bold text-[#EEE5E5] pb-2`}
|
||||
>
|
||||
{t("tickets.visitor.latePrice")}
|
||||
</h2>
|
||||
<h3
|
||||
className={`text-3xl ${vipnagorgialla.className} font-bold text-[#EEE5E5] pb-4`}
|
||||
>
|
||||
{t("tickets.visitor.title")}
|
||||
</h3>
|
||||
<ul className="pl-4 mb-8 list-[square] marker:text-[#1F5673]">
|
||||
{t
|
||||
.raw("tickets.visitor.features")
|
||||
.map((feature: string, index: number) => (
|
||||
<li key={index} className="text-xl">
|
||||
{feature}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Link href="https://fienta.com/et/tipilan" target="_blank">
|
||||
<button
|
||||
className={`px-4 py-2 bg-[#1F5673] cursor-pointer ${vipnagorgialla.className} font-bold`}
|
||||
>
|
||||
{t("tickets.buyTicket")}
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#007CAB] -skew-x-2 md:-skew-x-5 text-white italic px-8 md:px-12 py-16 hover:scale-103 transition-all duration-150 w-full md:w-xl lg:w-[400px]">
|
||||
<h2
|
||||
className={`text-6xl ${vipnagorgialla.className} font-bold text-[#EEE5E5] pb-2`}
|
||||
>
|
||||
{t("tickets.computerParticipant.latePrice")}
|
||||
</h2>
|
||||
<h3
|
||||
className={`text-3xl ${vipnagorgialla.className} font-bold text-[#EEE5E5] pb-4`}
|
||||
>
|
||||
{t("tickets.computerParticipant.title")}
|
||||
</h3>
|
||||
<ul className="pl-4 mb-8 list-[square] marker:text-[#1F5673]">
|
||||
{t
|
||||
.raw("tickets.computerParticipant.features")
|
||||
.map((feature: string, index: number) => (
|
||||
<li key={index} className="text-xl">
|
||||
{feature}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Link href="https://fienta.com/et/tipilan" target="_blank">
|
||||
<button
|
||||
className={`px-4 py-2 bg-[#1F5673] cursor-pointer ${vipnagorgialla.className} font-bold`}
|
||||
>
|
||||
{t("tickets.buyTicket")}
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div className="bg-[#1F5673] -skew-x-2 md:-skew-x-5 text-gray-400 italic px-8 md:px-12 py-16 w-full md:w-xl lg:w-[400px]">
|
||||
<h2
|
||||
className={`text-4xl ${vipnagorgialla.className} font-bold pb-2`}
|
||||
>
|
||||
<s>{t("tickets.competitor.price")}</s>
|
||||
</h2>
|
||||
<h3
|
||||
className={`text-2xl ${vipnagorgialla.className} font-bold pb-4`}
|
||||
>
|
||||
<s>{t("tickets.competitor.title")}</s>
|
||||
</h3>
|
||||
<ul className="pl-4 mb-8 list-[square] marker:text-[#007CAB]">
|
||||
{t
|
||||
.raw("tickets.competitor.features")
|
||||
.map((feature: string, index: number) => (
|
||||
<li key={index} className="text-sm">
|
||||
{feature}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{/*<Link href="https://fienta.com/et/tipilan" target="_blank">*/}
|
||||
<button
|
||||
className={`px-4 py-2 bg-[#007CAB] text-white ${vipnagorgialla.className} font-bold text-xl uppercase opacity-55`}
|
||||
>
|
||||
{t("tickets.soldOut")}!
|
||||
</button>
|
||||
{/*</Link>*/}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SectionDivider />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import ReactMarkdown, { Components } from "react-markdown";
|
||||
import { vipnagorgialla } from "@/components/Vipnagorgialla";
|
||||
import SectionDivider from "@/components/SectionDivider";
|
||||
import { getTranslations, setRequestLocale } from "next-intl/server";
|
||||
import { loadRulesBun } from "@/lib/loadRules";
|
||||
|
||||
// Map of valid slugs to their translation keys
|
||||
const rulesMap = {
|
||||
lol: {
|
||||
titleKey: "rules.lolRules",
|
||||
},
|
||||
cs2: {
|
||||
titleKey: "rules.cs2Rules",
|
||||
},
|
||||
} as const;
|
||||
|
||||
type RuleSlug = keyof typeof rulesMap;
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ slug: string; locale: string }>;
|
||||
}
|
||||
|
||||
async function getRuleContent(slug: string, locale: string) {
|
||||
if (!Object.keys(rulesMap).includes(slug)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ruleConfig = rulesMap[slug as RuleSlug];
|
||||
|
||||
try {
|
||||
const content = await loadRulesBun(
|
||||
slug as "cs2" | "lol",
|
||||
locale as "et" | "en",
|
||||
);
|
||||
return {
|
||||
content,
|
||||
titleKey: ruleConfig.titleKey,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error reading rule file for slug ${slug} in locale ${locale}:`,
|
||||
error,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default async function RulePage({ params }: PageProps) {
|
||||
const { slug, locale } = await params;
|
||||
setRequestLocale(locale);
|
||||
const t = await getTranslations({ locale });
|
||||
const ruleData = await getRuleContent(slug, locale);
|
||||
|
||||
if (!ruleData) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const headingStyle = `text-5xl sm:text-6xl ${vipnagorgialla.className} font-bold uppercase italic text-[#2A2C3F] dark:text-[#EEE5E5]`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col min-h-[90vh] m-6 mt-16 md:m-16">
|
||||
<h1 className={`${headingStyle} mt-8 md:mt-16 mb-4`}>
|
||||
{t(ruleData.titleKey)}
|
||||
</h1>
|
||||
|
||||
<div className="prose prose-lg dark:prose-invert max-w-none">
|
||||
<ReactMarkdown
|
||||
components={
|
||||
{
|
||||
h1: (props) => (
|
||||
<h1 className="text-3xl md:text-4xl font-bold my-4">
|
||||
{props.children}
|
||||
</h1>
|
||||
),
|
||||
h2: (props) => (
|
||||
<h2 className="text-2xl md:text-3xl font-semibold my-3">
|
||||
{props.children}
|
||||
</h2>
|
||||
),
|
||||
ol: (props) => (
|
||||
<ol className="list-none ml-6 md:text-xl">
|
||||
{props.children}
|
||||
</ol>
|
||||
),
|
||||
ul: (props) => (
|
||||
<ul className="list-disc ml-6 md:text-xl">
|
||||
{props.children}
|
||||
</ul>
|
||||
),
|
||||
p: (props) => <p className="md:text-xl">{props.children}</p>,
|
||||
} as Components
|
||||
}
|
||||
>
|
||||
{ruleData.content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SectionDivider />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return Object.keys(rulesMap).map((slug) => ({
|
||||
slug,
|
||||
}));
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
import { vipnagorgialla } from "@/components/Vipnagorgialla";
|
||||
import SectionDivider from "@/components/SectionDivider";
|
||||
import { getTranslations, setRequestLocale } from "next-intl/server";
|
||||
import NextLink from "next/link";
|
||||
|
||||
export default async function RulesMenu({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>;
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
|
||||
setRequestLocale(locale);
|
||||
|
||||
const t = await getTranslations({ locale });
|
||||
|
||||
const headingStyle = `text-4xl md:text-5xl lg:text-6xl ${vipnagorgialla.className} font-bold italic text-[#2A2C3F] dark:text-[#EEE5E5] uppercase`;
|
||||
|
||||
const boxStyle = `-skew-x-2 md:-skew-x-5 text-white md:px-12 hover:scale-103 transition-all duration-150 w-full md:w-xl lg:w-[400px]`;
|
||||
|
||||
const boxTextStyle = `text-2xl md:text-3xl ${vipnagorgialla.className} font-bold uppercase text-[#EEE5E5] pb-2 break-normal whitespace-pre-line`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col md:m-16">
|
||||
<h1 className={`${headingStyle} ml-3 mt-24 md:ml-0 md:mt-16 mb-4 px-4`}>
|
||||
{t("rules.title")}
|
||||
</h1>
|
||||
|
||||
<div className="flex flex-wrap flex-row lg:mt-16 justify-center lg:items-start gap-12 flex-grow mb-8">
|
||||
<NextLink href="/kodukord">
|
||||
<div className={`${boxStyle} bg-[#007CAB] py-20 px-8`}>
|
||||
<h2 className={`${boxTextStyle}`}>{t("rules.houseRules")}</h2>
|
||||
</div>
|
||||
</NextLink>
|
||||
|
||||
<NextLink href="/reeglid/cs2">
|
||||
<div className={`${boxStyle} bg-[#1F5673] py-20 px-8`}>
|
||||
<h2 className={`${boxTextStyle}`}>{t("rules.cs2Rules")}</h2>
|
||||
</div>
|
||||
</NextLink>
|
||||
|
||||
<NextLink href="reeglid/lol">
|
||||
<div className={`${boxStyle} bg-[#007CAB] py-20 px-8`}>
|
||||
<h2 className={`${boxTextStyle}`}>{t("rules.lolRules")}</h2>
|
||||
</div>
|
||||
</NextLink>
|
||||
|
||||
{/* Minitourn. link coming soon*/}
|
||||
{/*<Link href="">*/}
|
||||
{/* ajutine div. kui asendate lingiga, siis saab selle ära võtta */}
|
||||
<div className="cursor-not-allowed">
|
||||
<div
|
||||
className={`${boxStyle} bg-[#1F5673] py-16 px-8 opacity-50 pointer-events-none`}
|
||||
>
|
||||
<h2 className={`${boxTextStyle}`}>{t("rules.miniRules")}</h2>
|
||||
</div>
|
||||
</div>
|
||||
{/*</Link>*/}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SectionDivider />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
import { vipnagorgialla } from "@/components/Vipnagorgialla";
|
||||
import Sponsors from "@/components/Sponsors";
|
||||
import { Link } from "@/i18n/routing";
|
||||
import { getTranslations, setRequestLocale } from "next-intl/server";
|
||||
import Image from "next/image";
|
||||
|
||||
export default async function Home({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>;
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
setRequestLocale(locale);
|
||||
const t = await getTranslations({ locale });
|
||||
return (
|
||||
<div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2">
|
||||
{/* Title */}
|
||||
<div className="grid grid-cols-1 items-center justify-between mt-18 gap-12 pt-8">
|
||||
<Image
|
||||
src="/tipilan-white.svg"
|
||||
width={850}
|
||||
height={120}
|
||||
alt="TipiLAN Logo"
|
||||
className="px-8 py-8 md:px-12 md:py-14 dark:hidden w-[max(300px,min(100%,850px))] h-auto"
|
||||
/>
|
||||
<Image
|
||||
src="/tipilan-dark.svg"
|
||||
width={850}
|
||||
height={120}
|
||||
alt="TipiLAN Logo"
|
||||
className="px-8 py-8 md:px-12 md:py-14 not-dark:hidden w-[max(300px,min(100%,850px))] h-auto2"
|
||||
/>
|
||||
<Link
|
||||
href="/ajakava"
|
||||
className="px-8 md:px-12 py-8 flex flex-col gap-4 border-b-3 border-t-3 group border-[#1F5673] hover:bg-[#007CAB] dark:hover:bg-[#00A3E0] transition"
|
||||
>
|
||||
<div className="cursor-pointer 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>
|
||||
<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]">
|
||||
event_note
|
||||
</span>
|
||||
<p className="text-[clamp(0.875rem,0.75rem+0.5vw,1.25rem)] tracking-[-0.045rem] dark:group-hover:text-[#2A2C3F] group-hover:text-black">
|
||||
{t("home.sections.schedule.description")}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
{/* Stream iframe from Twitch */}
|
||||
<div className="border-[#1F5673] -ml-0.75 border-l-0 md:border-l-3 border-b-3 h-full pt-0 md:pt-16">
|
||||
<iframe
|
||||
src="https://player.twitch.tv/?channel=tipilan_ee&parent=localhost&parent=tipilan.ee"
|
||||
height="100%"
|
||||
width="100%"
|
||||
className="w-full h-full min-h-[400px]"
|
||||
allow="autoplay; encrypted-media"
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
{/* Grid of buttons */}
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 border-[#1F5673]">
|
||||
<Link
|
||||
href="/turniirid"
|
||||
className="px-8 md:px-12 py-8 flex flex-col gap-4 border-b-3 lg:border-r-3 group border-[#1F5673] hover:bg-[#007CAB] dark:hover:bg-[#00A3E0] transition"
|
||||
>
|
||||
<div className="cursor-pointer flex flex-row justify-between gap-4 items-center">
|
||||
<h2
|
||||
className={`text-[clamp(2rem,1.8rem+1vw,3rem)] ${vipnagorgialla.className} font-bold italic break-normal uppercase dark:text-[#EEE5E5] text-[#2A2C3F] dark:group-hover:text-[#2A2C3F] group-hover:text-black`}
|
||||
>
|
||||
{t("navigation.tournaments")}
|
||||
</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>
|
||||
|
||||
<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]">
|
||||
trophy
|
||||
</span>
|
||||
<p className="text-[clamp(0.875rem,0.75rem+0.5vw,1.25rem)] tracking-[-0.045rem] dark:group-hover:text-[#2A2C3F] group-hover:text-black">
|
||||
{t("home.sections.tournaments.description")}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
<Link
|
||||
href="/messiala"
|
||||
className="px-8 md:px-12 py-8 flex flex-col gap-4 border-b-3 border-[#1F5673] group hover:bg-[#007CAB] dark:hover:bg-[#00A3E0] transition-all"
|
||||
>
|
||||
<div className="cursor-pointer 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] dark:group-hover:text-[#2A2C3F] group-hover:text-black`}
|
||||
>
|
||||
{t("navigation.expo")}
|
||||
</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>
|
||||
<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]">
|
||||
weekend
|
||||
</span>
|
||||
<p className="text-[clamp(0.875rem,0.75rem+0.5vw,1.25rem)] tracking-[-0.045rem] dark:group-hover:text-[#2A2C3F] group-hover:text-black">
|
||||
{t("home.sections.expo.description")}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
{/* Date */}
|
||||
<Link
|
||||
href="/piletid"
|
||||
className={`p-8 md:p-12 flex flex-col ${vipnagorgialla.className} font-bold italic border-b-3 border-[#1F5673] hover:bg-[#007CAB] dark:hover:bg-[#00A3E0] group transition`}
|
||||
>
|
||||
<div className="cursor-pointer text-left flex flex-row justify-between xl:justify-start gap-8">
|
||||
<h3 className="text-4xl md:text-5xl dark:text-[#EEE5E5] dark:group-hover:text-[#2A2C3F] text-[#2A2C3F] group-hover:text-black">
|
||||
{t("home.sections.reserveSpot")}
|
||||
</h3>
|
||||
<span className="material-symbols-outlined !text-[clamp(2rem,1.5rem+1.5vw,3.5rem)] !font-bold text-[#007CAB] dark:text-[#00A3E0] hidden md:block group-hover:translate-x-2 group-hover:text-[#EEE5E5] dark:group-hover:text-[#EEE5E5] transition">
|
||||
arrow_right_alt
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="text-[clamp(2.5rem,2.25rem+1.25vw,3.75rem)] text-[#007CAB] dark:text-[#00A3E0] dark:group-hover:text-[#EEE5E5] group-hover:text-[#EEE5E5]">
|
||||
{t("home.sections.dateAndLocation")}
|
||||
</h2>
|
||||
</Link>
|
||||
{/* Sponsors */}
|
||||
<Sponsors />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,264 +0,0 @@
|
||||
import { vipnagorgialla } from "@/components/Vipnagorgialla";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { getTranslations, setRequestLocale } from "next-intl/server";
|
||||
|
||||
export default async function Tourney({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>;
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
setRequestLocale(locale);
|
||||
const t = await getTranslations({ locale });
|
||||
const headingStyle = `text-3xl md:text-5xl lg:text-5xl ${vipnagorgialla.className} font-bold uppercase text-[#2A2C3F] dark:text-[#EEE5E5] -skew-x-2 md:-skew-x-5 break-normal`;
|
||||
|
||||
const miniTournaments: {
|
||||
name: string;
|
||||
prize: string;
|
||||
image: string;
|
||||
objectPosition?: string;
|
||||
bgClass?: string;
|
||||
}[] = [
|
||||
{
|
||||
name: "Tekken 8",
|
||||
prize: "200€",
|
||||
image: "/images/miniturniirid/tekken8.jpg",
|
||||
objectPosition: "object-center",
|
||||
},
|
||||
{
|
||||
name: "WRC",
|
||||
prize: "350€",
|
||||
image: "/images/miniturniirid/wrc.jpg",
|
||||
objectPosition: "object-center",
|
||||
},
|
||||
{
|
||||
name: "Street Fighter 6",
|
||||
prize: "150€",
|
||||
image: "/images/miniturniirid/street_fighter.jpg",
|
||||
objectPosition: "object-center",
|
||||
},
|
||||
{
|
||||
name: "Gran Turismo",
|
||||
prize: "200€",
|
||||
image: "/images/miniturniirid/gran_turismo.jpg",
|
||||
objectPosition: "object-center",
|
||||
},
|
||||
{
|
||||
name: "FC 26",
|
||||
prize: "100€",
|
||||
image: "/images/miniturniirid/fc26.jpg",
|
||||
objectPosition: "object-center",
|
||||
},
|
||||
{
|
||||
name: "Dwarf Escape",
|
||||
prize: "50€",
|
||||
image: "/images/miniturniirid/dwarf_escape.png",
|
||||
objectPosition: "object-center",
|
||||
bgClass: "bg-black",
|
||||
},
|
||||
{
|
||||
name: "Buckshot Roulette",
|
||||
prize: "Merch",
|
||||
image: "/images/miniturniirid/buckshot_tournament.png",
|
||||
objectPosition: "object-center",
|
||||
bgClass: "bg-black",
|
||||
},
|
||||
{
|
||||
name: "2XKO",
|
||||
prize: "100€",
|
||||
image: "/images/miniturniirid/2xko.png",
|
||||
objectPosition: "object-top",
|
||||
},
|
||||
{
|
||||
name: "Super Smash Bros. Ultimate",
|
||||
prize: "100€",
|
||||
image: "/images/miniturniirid/super_smash_bros.jpg",
|
||||
objectPosition: "object-top",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-h-[90vh] mt-16">
|
||||
<h1
|
||||
className={`text-4xl md:text-5xl lg:text-6xl ${vipnagorgialla.className} font-bold italic uppercase
|
||||
text-[#2A2C3F] dark:text-[#EEE5E5] mt-8 md:mt-16 mb-8 m-6 md:m-16`}
|
||||
>
|
||||
{t("tournaments.title")}
|
||||
</h1>
|
||||
|
||||
<div className="flex flex-col">
|
||||
|
||||
{/* Mini-turniirid */}
|
||||
<div className="hover:bg-[#007CAB] py-8 md:py-16 border-b-[3px] border-[#1F5673] transition group">
|
||||
<div className="mx-8 md:mx-16 lg:mx-32 xl:mx-48">
|
||||
<div className="-skew-x-2 md:-skew-x-5 mb-8">
|
||||
<h2 className={`${headingStyle}`}>
|
||||
{t("tournaments.mini.title")}
|
||||
</h2>
|
||||
<p
|
||||
className={
|
||||
"text-2xl mb-4 text-neutral-500 group-hover:text-black"
|
||||
}
|
||||
>
|
||||
{t("tournaments.mini.timing")}
|
||||
</p>
|
||||
<p className="text-balance">
|
||||
{t("tournaments.mini.description1")}
|
||||
</p>
|
||||
<p className="text-balance">
|
||||
{t("tournaments.mini.description2")}
|
||||
</p>
|
||||
<br />
|
||||
<div className="flex flex-row flex-wrap gap-4 md:gap-8">
|
||||
<Link href="/kodukord" target="_blank">
|
||||
<button
|
||||
className={`px-4 py-2 bg-[#1F5673] cursor-pointer ${vipnagorgialla.className} font-bold italic text-[#ECE5E5]`}
|
||||
>
|
||||
{t("tournaments.mini.readRules")}
|
||||
</button>
|
||||
</Link>
|
||||
<a href="https://fienta.com/et/tipilan" target="_blank">
|
||||
<button
|
||||
className={`px-4 py-2 bg-[#00A3E0] group-hover:bg-black cursor-pointer ${vipnagorgialla.className} font-bold italic text-[#ECE5E5]`}
|
||||
>
|
||||
{t("tournaments.mini.buyTicket")}
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-8">
|
||||
{miniTournaments.map((tournament) => (
|
||||
<div key={tournament.name} className="text-center">
|
||||
<Image
|
||||
src={tournament.image}
|
||||
alt={tournament.name}
|
||||
width={400}
|
||||
height={300}
|
||||
className={`outline-10 outline-[#007CAB] bg-black object-cover w-full aspect-video -skew-x-2 md:-skew-x-5 ${
|
||||
tournament.objectPosition || "object-center"
|
||||
}`}
|
||||
/>
|
||||
<div className="-skew-x-2 md:-skew-x-5">
|
||||
<p className="mt-2 font-semibold">{tournament.name} - {tournament.prize}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CS2 turniir */}
|
||||
<div className="hover:bg-[#007CAB] py-8 md:py-16 transition group">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-16 items-center mx-8 md:mx-16 lg:mx-32 xl:mx-48">
|
||||
<div className="-skew-x-2 md:-skew-x-5">
|
||||
<h2 className={`${headingStyle}`}>
|
||||
{t("tournaments.cs2.title")}
|
||||
</h2>
|
||||
<p
|
||||
className={
|
||||
"text-2xl mb-4 text-neutral-500 group-hover:text-black"
|
||||
}
|
||||
>
|
||||
{t("tournaments.cs2.timing")}
|
||||
</p>
|
||||
<p className="text-balance">
|
||||
{t("tournaments.cs2.description1")}
|
||||
</p>
|
||||
<br />
|
||||
<p className="text-balance">
|
||||
{t("tournaments.cs2.description2")}
|
||||
</p>
|
||||
<p className="text-balance">
|
||||
{t("tournaments.cs2.description3")}
|
||||
</p>
|
||||
<br />
|
||||
|
||||
<div className={"flex flex-row flex-wrap gap-8"}>
|
||||
<Link href="/reeglid/cs2" target="_blank">
|
||||
<button
|
||||
className={`px-4 py-2 bg-[#1F5673] cursor-pointer ${vipnagorgialla.className} font-bold italic text-[#ECE5E5]`}
|
||||
>
|
||||
{t("tournaments.cs2.readRules")}
|
||||
</button>
|
||||
</Link>
|
||||
<a href="https://fienta.com/et/tipilan" target="_blank">
|
||||
<button
|
||||
className={`px-4 py-2 bg-[#00A3E0] group-hover:bg-black cursor-pointer ${vipnagorgialla.className} font-bold italic text-[#ECE5E5]`}
|
||||
>
|
||||
{t("tournaments.cs2.buyTicket")}
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden md:block">
|
||||
<div>
|
||||
{/* Outside div needs to remain so that overflow won't occur*/}
|
||||
<Image
|
||||
src="/images/cs2_tournament_logo.png"
|
||||
alt="CS2 tournament"
|
||||
width={600}
|
||||
height={400}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* LoL turniir */}
|
||||
<div className="hover:bg-[#007CAB] py-8 md:py-16 border-t-[3px] border-b-[3px] border-[#1F5673] transition group">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-16 items-center mx-8 md:mx-16 lg:mx-32 xl:mx-48">
|
||||
<div className="hidden md:block">
|
||||
<div>
|
||||
{/* Outside div needs to remain so that overflow won't occur*/}
|
||||
<Image
|
||||
src="/images/lol_tournament_logo.png"
|
||||
alt="LoL tournament"
|
||||
width={600}
|
||||
height={400}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-auto text-right -skew-x-2 md:-skew-x-5">
|
||||
<h2 className={`${headingStyle}`}>
|
||||
{t("tournaments.lol.title")}
|
||||
</h2>
|
||||
<p
|
||||
className={
|
||||
"text-2xl mb-4 text-neutral-500 group-hover:text-black"
|
||||
}
|
||||
>
|
||||
{t("tournaments.lol.timing")}
|
||||
</p>
|
||||
<p className="text-balance">
|
||||
{t("tournaments.lol.description1")}
|
||||
</p>
|
||||
<br />
|
||||
<p className="text-balance">
|
||||
{t("tournaments.lol.description2")}
|
||||
</p>
|
||||
<br />
|
||||
<div className="flex flex-row flex-wrap gap-4 md:gap-8 justify-end">
|
||||
<Link href="/reeglid/lol" target="_blank">
|
||||
<button
|
||||
className={`px-4 py-2 bg-[#1F5673] cursor-pointer ${vipnagorgialla.className} font-bold italic text-[#ECE5E5]`}
|
||||
>
|
||||
{t("tournaments.lol.readRules")}
|
||||
</button>
|
||||
</Link>
|
||||
<a href="https://fienta.com/et/tipilan" target="_blank">
|
||||
<button
|
||||
className={`px-4 py-2 bg-[#00A3E0] group-hover:bg-black cursor-pointer ${vipnagorgialla.className} font-bold italic text-[#ECE5E5]`}
|
||||
>
|
||||
{t("tournaments.lol.buyTicket")}
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,137 +1,158 @@
|
||||
@import 'tailwindcss';
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
/* ===== TipiLAN 2026 Design Tokens ===== */
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
@font-face {
|
||||
font-family: 'Vipnagorgialla';
|
||||
src: url('/fonts/vipnagorgialla/Vipnagorgialla-Rg.otf') format('opentype');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@theme {
|
||||
--breakpoint-xs: 30rem;
|
||||
@font-face {
|
||||
font-family: 'Vipnagorgialla';
|
||||
src: url('/fonts/vipnagorgialla/Vipnagorgialla-Rg-It.otf') format('opentype');
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Vipnagorgialla';
|
||||
src: url('/fonts/vipnagorgialla/Vipnagorgialla-Bd.otf') format('opentype');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Vipnagorgialla';
|
||||
src: url('/fonts/vipnagorgialla/Vipnagorgialla-Bd-It.otf') format('opentype');
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* ===== 2026 Color & Font Theme ===== */
|
||||
@theme inline {
|
||||
--color-bg-dark: #0A121F;
|
||||
--color-primary: #00A3E0;
|
||||
--color-primary-50: rgba(0, 163, 224, 0.5);
|
||||
--color-text-light: #FFFFFF;
|
||||
--color-stroke: #00A3E0;
|
||||
|
||||
--font-heading: 'Vipnagorgialla', sans-serif;
|
||||
--font-body: 'Work Sans', Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Work Sans", Arial, Helvetica, sans-serif;
|
||||
font-family: var(--font-body);
|
||||
max-width: 100vw;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: var(--color-bg-dark);
|
||||
color: var(--color-text-light);
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
/* ===== 2026 Typography Utilities ===== */
|
||||
@utility text-title {
|
||||
font-family: var(--font-heading);
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
font-size: clamp(48px, 8vw, 96px);
|
||||
line-height: 100%;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
@utility text-subtitle {
|
||||
font-family: var(--font-heading);
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
font-size: clamp(36px, 5vw, 64px);
|
||||
line-height: 100%;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
@utility text-h1 {
|
||||
font-family: var(--font-heading);
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
font-size: clamp(28px, 4vw, 48px);
|
||||
line-height: 100%;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
@utility text-btn-lg {
|
||||
font-family: var(--font-heading);
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
font-size: 24px;
|
||||
line-height: 100%;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
@utility text-p-lg {
|
||||
font-family: var(--font-body);
|
||||
font-weight: 400;
|
||||
font-size: clamp(18px, 2.5vw, 28px);
|
||||
line-height: 130%;
|
||||
}
|
||||
|
||||
@utility text-p {
|
||||
font-family: var(--font-body);
|
||||
font-weight: 400;
|
||||
font-size: 20px;
|
||||
line-height: 100%;
|
||||
}
|
||||
|
||||
@utility text-countdown {
|
||||
font-family: var(--font-heading);
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
font-size: clamp(32px, 5vw, 48px);
|
||||
line-height: 100%;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
@utility text-countdown-label {
|
||||
font-family: var(--font-heading);
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
font-size: 16px;
|
||||
line-height: 100%;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* ===== 2026 Effect Utilities ===== */
|
||||
@utility shadow-teaser {
|
||||
filter: drop-shadow(0px 0px 20px rgba(0, 0, 0, 0.5));
|
||||
}
|
||||
|
||||
@utility text-shadow-teaser {
|
||||
text-shadow: 0px 0px 20px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* ===== 2026 Button Utility ===== */
|
||||
@utility btn-primary-lg {
|
||||
background-color: var(--color-primary);
|
||||
color: var(--color-bg-dark);
|
||||
padding: 16px;
|
||||
min-width: 56px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
/* ===== Scrollbar hidden utility ===== */
|
||||
@utility scrollbar-hidden {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 1, 'wght' 700, 'GRAD' 0, 'opsz' 24;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,14 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Work_Sans } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import "material-symbols";
|
||||
|
||||
const workSans = Work_Sans({
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "TipiLAN 2025",
|
||||
description: "TipiLAN 2025 – Eesti suurim tudengite korraldatud LAN!",
|
||||
title: "TipiLAN 2026",
|
||||
description: "TipiLAN 2026 – Eesti suurim tudengite korraldatud LAN!",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
@@ -18,9 +17,9 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html suppressHydrationWarning>
|
||||
<html lang="et">
|
||||
<body
|
||||
className={`${workSans.className} antialiased bg-[#EEE5E5] dark:bg-[#0E0F19]`}
|
||||
className={`${workSans.className} antialiased bg-bg-dark text-text-light`}
|
||||
>
|
||||
{children}
|
||||
</body>
|
||||
|
||||
@@ -1,29 +1,11 @@
|
||||
import { vipnagorgialla } from "@/components/Vipnagorgialla";
|
||||
import { ThemeProvider } from "@/components/Theme-provider";
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<div className="flex flex-col min-h-[90vh] p-12 justify-center items-center">
|
||||
<h1
|
||||
className={`text-7xl ${vipnagorgialla.className} font-bold italic uppercase text-[#2A2C3F] dark:text-[#EEE5E5] mt-8 mb-4`}
|
||||
>
|
||||
404
|
||||
</h1>
|
||||
<div className="text-center">
|
||||
<p className="text-2xl text-[#2A2C3F] dark:text-[#EEE5E5] mb-2">
|
||||
Lehte ei leitud!
|
||||
</p>
|
||||
<p className="text-lg text-[#2A2C3F]/80 dark:text-[#EEE5E5]/80">
|
||||
Page not found!
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col min-h-dvh p-12 justify-center items-center bg-bg-dark text-text-light">
|
||||
<h1 className="text-title">404</h1>
|
||||
<div className="text-center mt-4">
|
||||
<p className="text-p-lg">Lehte ei leitud!</p>
|
||||
<p className="text-p mt-2 opacity-80">Page not found!</p>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
import { SiDiscord, SiInstagram, SiFacebook } from "react-icons/si";
|
||||
import Image from "next/image";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
// Fonts
|
||||
import { vipnagorgialla } from "@/components/Vipnagorgialla";
|
||||
|
||||
const Footer = () => {
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-center sm:justify-between px-6 py-8 md:px-12 md:py-16 gap-4 md:gap-8">
|
||||
<div className="flex md:items-center gap-8 md:gap-0 justify-between flex-col md:flex-row">
|
||||
<div className="flex flex-col items-start md:items-center">
|
||||
<Image
|
||||
src="/tipilan-white.svg"
|
||||
width={250}
|
||||
height={36}
|
||||
alt="TipiLAN Logo"
|
||||
className="h-9 dark:hidden"
|
||||
/>
|
||||
<Image
|
||||
src="/tipilan-dark.svg"
|
||||
width={250}
|
||||
height={36}
|
||||
alt="TipiLAN Logo"
|
||||
className="h-9 not-dark:hidden"
|
||||
/>
|
||||
</div>
|
||||
{/* Social media */}
|
||||
<div className="flex flex-row">
|
||||
<a
|
||||
href="https://discord.gg/pPhhatZAfA"
|
||||
target="_blank"
|
||||
className="mx-4 ml-0 md:ml-4"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<SiDiscord
|
||||
title="Discord"
|
||||
size={"2em"}
|
||||
className="text-[#2A2C3F] dark:text-[#EEE5E5] hover:text-[#007CAB] hover:dark:text-[#00A3E0] transition"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href="https://instagram.com/tipilan.ee"
|
||||
target="_blank"
|
||||
className="mx-4"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<SiInstagram
|
||||
title="Instagram"
|
||||
size={"2em"}
|
||||
className="text-[#2A2C3F] dark:text-[#EEE5E5] hover:text-[#007CAB] hover:dark:text-[#00A3E0] transition"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href="https://facebook.com/tipilan.ee"
|
||||
target="_blank"
|
||||
className="mx-4"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<SiFacebook
|
||||
title="Facebook"
|
||||
size={"2em"}
|
||||
className="text-[#2A2C3F] dark:text-[#EEE5E5] hover:text-[#007CAB] hover:dark:text-[#00A3E0] transition"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<h2
|
||||
className={`text-3xl sm:text-4xl ${vipnagorgialla.className} font-bold italic uppercase text-[#2A2C3F] dark:text-[#EEE5E5] mt-8 mb-4`}
|
||||
>
|
||||
{t("footer.contact")}
|
||||
</h2>
|
||||
<div className="flex flex-row justify-between gap-4 items-center">
|
||||
<div>
|
||||
<h3 className="text-xl font-bold">{t("footer.studentUnion")}</h3>
|
||||
<div className="flex flex-col gap-2 mt-2">
|
||||
<div className="flex flex-row gap-2">
|
||||
<span className="material-symbols-outlined !font-bold text-[#007CAB] dark:text-[#00A3E0]">
|
||||
mail
|
||||
</span>
|
||||
<a href="mailto:tipilan@ituk.ee" className="underline">
|
||||
tipilan@ituk.ee
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex flex-row gap-2">
|
||||
<span className="material-symbols-outlined !font-bold text-[#007CAB] dark:text-[#00A3E0]">
|
||||
phone
|
||||
</span>
|
||||
<a href="tel:+37256931193" className="underline">
|
||||
+372 5693 1193
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="text-xl font-bold pt-4">
|
||||
{t("footer.organization")}
|
||||
</h3>
|
||||
<div>
|
||||
<p>
|
||||
{t("footer.registrationCode")}:{" "}
|
||||
<span className="font-semibold text-[#007CAB] dark:text-[#00A3E0]">
|
||||
80391807
|
||||
</span>
|
||||
</p>
|
||||
<p className="">ICO-210, Raja tn 4c, Tallinn, Harjumaa, 12616</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="block align-middle text-center pt-16">
|
||||
{t("footer.madeBy")}{" "}
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://lapikud.ee/"
|
||||
className="text-[#E3983E] font-bold"
|
||||
>
|
||||
MTÜ Lapikud
|
||||
</a>{" "}
|
||||
{t("footer.withHelpFrom")}{" "}
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://ituk.ee/"
|
||||
className="bg-[#7B1642] font-bold not-dark:text-white"
|
||||
>
|
||||
MTÜ For Tsükkel/ITÜK
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
@@ -1,101 +0,0 @@
|
||||
"use client";
|
||||
|
||||
// Icons
|
||||
import {
|
||||
MdClose,
|
||||
MdMenu,
|
||||
MdSunny,
|
||||
MdModeNight,
|
||||
MdComputer,
|
||||
} from "react-icons/md";
|
||||
|
||||
// Theme Provider
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
import LanguageSwitcher from "./LanguageSwitcher";
|
||||
|
||||
// Shadcn UI
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
// Fonts
|
||||
// import { vipnagorgialla } from "@/components/Vipnagorgialla";
|
||||
|
||||
interface HeaderProps {
|
||||
isOpen: boolean;
|
||||
onToggle: () => void;
|
||||
themeLabels: {
|
||||
light: string;
|
||||
dark: string;
|
||||
system: string;
|
||||
};
|
||||
}
|
||||
|
||||
const Header = ({ isOpen, onToggle, themeLabels }: HeaderProps) => {
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<header className="px-8 py-2 md:px-12 flex items-center bg-[#EEE5E5] dark:bg-[#0E0F19] border-b-3 border-[#1F5673] justify-between text-[#2A2C3F] dark:text-[#EEE5E5]">
|
||||
<button onClick={onToggle}>
|
||||
{isOpen ? (
|
||||
<MdClose className="h-12 w-12 text-[#2A2C3F] dark:text-[#EEE5E5] cursor-pointer" />
|
||||
) : (
|
||||
<MdMenu className="h-12 w-12 text-[#2A2C3F] dark:text-[#EEE5E5] cursor-pointer" />
|
||||
)}
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<LanguageSwitcher />
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-10 cursor-pointer"
|
||||
>
|
||||
<MdSunny className="scale-135 text-[#2A2C3F] dark:hidden" />
|
||||
<MdModeNight className="scale-135 dark:text-[#EEE5E5] not-dark:hidden" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48 translate-y-4">
|
||||
<DropdownMenuItem
|
||||
className={`text-lg ${theme === "light" ? "bg-accent/50 font-medium" : ""}`}
|
||||
onClick={() => setTheme("light")}
|
||||
disabled={theme === "light"}
|
||||
>
|
||||
<MdSunny className={theme === "light" ? "text-amber-500" : ""} />
|
||||
<span>{themeLabels.light}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className={`text-lg ${theme === "dark" ? "bg-accent/50 font-medium" : ""}`}
|
||||
onClick={() => setTheme("dark")}
|
||||
disabled={theme === "dark"}
|
||||
>
|
||||
<MdModeNight
|
||||
className={theme === "dark" ? "text-blue-500" : ""}
|
||||
/>
|
||||
<span>{themeLabels.dark}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className={`text-lg ${theme === "system" ? "bg-accent/50 font-medium" : ""}`}
|
||||
onClick={() => setTheme("system")}
|
||||
disabled={theme === "system"}
|
||||
>
|
||||
<MdComputer
|
||||
className={theme === "system" ? "text-green-500" : ""}
|
||||
/>
|
||||
<span>{themeLabels.system}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
@@ -3,8 +3,7 @@
|
||||
import { useLocale } from "next-intl";
|
||||
import { useRouter, usePathname } from "@/i18n/routing";
|
||||
import { routing } from "@/i18n/routing";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { vipnagorgialla } from "@/components/Vipnagorgialla";
|
||||
import Image from "next/image";
|
||||
|
||||
export default function LanguageSwitcher() {
|
||||
const locale = useLocale();
|
||||
@@ -17,31 +16,24 @@ export default function LanguageSwitcher() {
|
||||
return routing.locales[nextIndex] as "et" | "en";
|
||||
};
|
||||
|
||||
const getNextLanguageName = () => {
|
||||
const nextLocale = getNextLocale();
|
||||
switch (nextLocale) {
|
||||
case "et":
|
||||
return "EST";
|
||||
case "en":
|
||||
return "ENG";
|
||||
default:
|
||||
return nextLocale;
|
||||
}
|
||||
};
|
||||
|
||||
const handleLanguageSwitch = () => {
|
||||
const nextLocale = getNextLocale();
|
||||
router.replace(pathname, { locale: nextLocale });
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
<button
|
||||
onClick={handleLanguageSwitch}
|
||||
variant="ghost"
|
||||
size="lg"
|
||||
className={`${vipnagorgialla.className} text-3xl font-bold italic uppercase cursor-pointer hover:bg-[#007CAB]/10 dark:hover:bg-[#00A3E0]/10 text-[#007CAB] dark:text-[#00A3E0] hover:text-[#2A2C3F] dark:hover:text-[#EEE5E5] transition-colors`}
|
||||
className="relative size-[40px] cursor-pointer overflow-hidden"
|
||||
aria-label="Switch language"
|
||||
>
|
||||
{getNextLanguageName()}
|
||||
</Button>
|
||||
<Image
|
||||
src={`/images/flag-${locale === "et" ? "en" : "et"}.svg`}
|
||||
alt={locale === "et" ? "Switch to English" : "Vaheta eesti keelele"}
|
||||
width={40}
|
||||
height={30}
|
||||
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2"
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function SectionDivider() {
|
||||
return (
|
||||
<hr className="border-t-[3px] border-[#1F5673]" />
|
||||
);
|
||||
};
|
||||
@@ -1,84 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { usePathname } from "@/i18n/routing";
|
||||
import { Link } from "@/i18n/routing";
|
||||
import { vipnagorgialla } from "@/components/Vipnagorgialla";
|
||||
import Header from "./Header";
|
||||
|
||||
interface NavItem {
|
||||
href:
|
||||
| "/"
|
||||
| "/ajakava"
|
||||
| "/haldus"
|
||||
| "/kodukord"
|
||||
| "/messiala"
|
||||
| "/piletid"
|
||||
| "/reeglid"
|
||||
| "/striim"
|
||||
| "/turniirid";
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface SidebarLayoutClientProps {
|
||||
themeLabels: {
|
||||
light: string;
|
||||
dark: string;
|
||||
system: string;
|
||||
};
|
||||
navItems: NavItem[];
|
||||
}
|
||||
|
||||
export default function SidebarLayoutClient({
|
||||
themeLabels,
|
||||
navItems,
|
||||
}: SidebarLayoutClientProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const pathname = usePathname();
|
||||
|
||||
const toggleSidebar = () => setIsOpen(!isOpen);
|
||||
|
||||
// Close sidebar when route changes
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header
|
||||
isOpen={isOpen}
|
||||
onToggle={toggleSidebar}
|
||||
themeLabels={themeLabels}
|
||||
/>
|
||||
|
||||
{/* Sidebar */}
|
||||
<>
|
||||
<div
|
||||
className="fixed inset-0 backdrop-blur mt-16 z-20"
|
||||
style={{ display: isOpen ? "block" : "none" }}
|
||||
onClick={() => setIsOpen(false)}
|
||||
></div>
|
||||
<div
|
||||
className={`text-3xl md:text-4xl ${vipnagorgialla.className} font-bold break-all italic uppercase fixed flex items-start xs:pl-25 pl-20 sm:pl-20 md:pl-24 flex-col gap-8 pt-16 top-0 left-0 h-[99vh] mt-16 -skew-x-5 border-r-3 border-[#1F5673] w-screen sm:w-96 md:w-128 bg-[#EEE5E5] dark:bg-[#0E0F19] text-[#2A2C3F] dark:text-[#EEE5E5] transition-transform transform z-20`}
|
||||
style={{
|
||||
transform: isOpen
|
||||
? "translateX(-13%) skewX(calc(5deg * -1)"
|
||||
: "translateX(-150%) skewX(calc(5deg * -1)",
|
||||
}}
|
||||
>
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="hover:text-[#00A3E0] md:hover:translate-x-2 transition duration-150"
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import SidebarLayoutClient from "./SidebarLayoutClient";
|
||||
|
||||
export default async function SidebarLayoutServer() {
|
||||
const t = await getTranslations("common");
|
||||
|
||||
const themeLabels = {
|
||||
light: t("theme.light"),
|
||||
dark: t("theme.dark"),
|
||||
system: t("theme.system"),
|
||||
};
|
||||
|
||||
const navT = await getTranslations("navigation");
|
||||
|
||||
const navItems = [
|
||||
{ href: "/" as const, label: navT("home") },
|
||||
{ href: "/messiala" as const, label: navT("expo") },
|
||||
{ href: "/piletid" as const, label: navT("tickets") },
|
||||
{ href: "/ajakava" as const, label: navT("schedule") },
|
||||
{ href: "/turniirid" as const, label: navT("tournaments") },
|
||||
{ href: "/kodukord" as const, label: navT("houserules") },
|
||||
{ href: "/reeglid" as const, label: navT("rules") },
|
||||
];
|
||||
|
||||
return <SidebarLayoutClient themeLabels={themeLabels} navItems={navItems} />;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import SidebarLayoutServer from "./SidebarLayoutServer";
|
||||
|
||||
const SidebarParent = () => {
|
||||
return (
|
||||
<div className="fixed w-screen top-0 z-9999">
|
||||
<SidebarLayoutServer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// This component is responsible for rendering the sidebar and header together.
|
||||
// Server-side translations are handled by SidebarLayoutServer.
|
||||
|
||||
export default SidebarParent;
|
||||
@@ -1,203 +0,0 @@
|
||||
import { vipnagorgialla } from "@/components/Vipnagorgialla";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Image from "next/image";
|
||||
import NextLink from "next/link";
|
||||
|
||||
interface SponsorsProps {
|
||||
showTitle?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function Sponsors({
|
||||
showTitle = true,
|
||||
className = "",
|
||||
}: SponsorsProps) {
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`p-12 flex flex-col ${vipnagorgialla.className} font-bold italic border-b-3 border-[#1F5673] ${className}`}
|
||||
>
|
||||
<div className="text-left flex flex-col justify-between xl:justify-start">
|
||||
{showTitle && (
|
||||
<h3 className="text-4xl md:text-5xl dark:text-[#EEE5E5] text-[#2A2C3F] group-hover:text-black pb-8">
|
||||
{t("home.sections.poweredBy")}
|
||||
</h3>
|
||||
)}
|
||||
<div className="flex flex-col sm:flex-row flex-wrap gap-8 md:gap-18 items-center justify-center xl:justify-start">
|
||||
<NextLink href="https://taltech.ee" target="_blank">
|
||||
<Image
|
||||
src="/sponsors/taltech-color.png"
|
||||
alt="Taltech (Tallinna Tehnikaülikool)"
|
||||
width={192}
|
||||
height={192}
|
||||
className="object-contain"
|
||||
/>
|
||||
</NextLink>
|
||||
<NextLink href="https://www.redbull.com/ee-et/" target="_blank">
|
||||
<Image
|
||||
src="/sponsors/redbull.png"
|
||||
alt="Redbull"
|
||||
width={80}
|
||||
height={80}
|
||||
className="object-contain"
|
||||
/>
|
||||
</NextLink>
|
||||
<NextLink href="https://www.alecoq.ee" target="_blank">
|
||||
<Image
|
||||
src="/sponsors/alecoq.svg"
|
||||
alt="Alecoq"
|
||||
width={200}
|
||||
height={200}
|
||||
className="object-contain"
|
||||
/>
|
||||
</NextLink>
|
||||
<NextLink href="https://www.simracing.ee/" target="_blank">
|
||||
<Image
|
||||
src="/sponsors/EVAL.png"
|
||||
alt="EVAL"
|
||||
width={200}
|
||||
height={200}
|
||||
className="object-contain"
|
||||
/>
|
||||
</NextLink>
|
||||
<NextLink href="https://balsnack.ee" target="_blank">
|
||||
<Image
|
||||
src="/sponsors/balsnack.svg"
|
||||
alt="Balsnack"
|
||||
width={200}
|
||||
height={200}
|
||||
className="object-contain"
|
||||
/>
|
||||
</NextLink>
|
||||
<NextLink
|
||||
href="https://www.rara.ee/sundmused/interaktiivne-videomangude-muuseum-lvlup/"
|
||||
target="_blank"
|
||||
>
|
||||
<Image
|
||||
src="/sponsors/lvlup_logo_export.svg"
|
||||
alt="LVLup!"
|
||||
width={192}
|
||||
height={192}
|
||||
className="object-contain"
|
||||
/>
|
||||
</NextLink>
|
||||
<NextLink
|
||||
href="https://www.facebook.com/bfglOfficial"
|
||||
target="_blank"
|
||||
>
|
||||
<Image
|
||||
src="/sponsors/BFGL.png"
|
||||
alt="BFGL"
|
||||
width={192}
|
||||
height={192}
|
||||
className="object-contain"
|
||||
/>
|
||||
</NextLink>
|
||||
<NextLink href="https://www.tallinn.ee/et/haridus" target="_blank">
|
||||
<Image
|
||||
src="/sponsors/Tallinna_Haridusamet_logo_RGB.svg"
|
||||
alt="Tallinna Haridusamet"
|
||||
width={292}
|
||||
height={292}
|
||||
className="object-contain"
|
||||
/>
|
||||
</NextLink>
|
||||
<NextLink href="https://www.militaarseiklus.ee/" target="_blank">
|
||||
<Image
|
||||
src="/sponsors/militaarseiklus.png"
|
||||
alt="Militaarseiklus"
|
||||
width={200}
|
||||
height={200}
|
||||
className="object-contain"
|
||||
/>
|
||||
</NextLink>
|
||||
<NextLink
|
||||
href="https://www.linkedin.com/company/gamedev-guild/"
|
||||
target="_blank"
|
||||
>
|
||||
<Image
|
||||
src="/sponsors/estonian_gamedev_guild.png"
|
||||
alt="Estonian Gamedev Guild"
|
||||
width={200}
|
||||
height={200}
|
||||
className="object-contain not-dark:invert"
|
||||
/>
|
||||
</NextLink>
|
||||
<NextLink href="https://thotell.ee/" target="_blank">
|
||||
<Image
|
||||
src="/sponsors/thotell.png"
|
||||
alt="Tahentorni Hotell (Tähentorni Hotel)"
|
||||
width={200}
|
||||
height={200}
|
||||
className="object-contain"
|
||||
/>
|
||||
</NextLink>
|
||||
<NextLink href="https://www.dominos.ee/" target="_blank">
|
||||
<Image
|
||||
src="/sponsors/dominos.png"
|
||||
alt="Domino's Pizza"
|
||||
width={250}
|
||||
height={250}
|
||||
className="object-contain"
|
||||
/>
|
||||
</NextLink>
|
||||
<NextLink href="https://www.tomorrow.ee/" target="_blank">
|
||||
<Image
|
||||
src="/sponsors/nt.png"
|
||||
alt="Network Tomorrow"
|
||||
width={300}
|
||||
height={200}
|
||||
className="object-contain"
|
||||
/>
|
||||
</NextLink>
|
||||
<NextLink href="https://driftikeskus.ee/" target="_blank">
|
||||
<Image
|
||||
src="/sponsors/driftikeskus.png"
|
||||
alt="Driftikeskus"
|
||||
width={300}
|
||||
height={200}
|
||||
className="object-contain"
|
||||
/>
|
||||
</NextLink>
|
||||
<NextLink href="https://ingame.ee/" target="_blank">
|
||||
<Image
|
||||
src="/sponsors/ingame.png"
|
||||
alt="Ingame"
|
||||
width={200}
|
||||
height={200}
|
||||
className="object-contain"
|
||||
/>
|
||||
</NextLink>
|
||||
<NextLink href="https://alzgamer.ee/" target="_blank">
|
||||
<Image
|
||||
src="/sponsors/alzgamer.png"
|
||||
alt="AlzGamer"
|
||||
width={200}
|
||||
height={200}
|
||||
className="object-contain"
|
||||
/>
|
||||
</NextLink>
|
||||
<NextLink href="https://k-space.ee/" target="_blank">
|
||||
<Image
|
||||
src="/sponsors/k-space_ee-white.png"
|
||||
alt="K-Space"
|
||||
width={200}
|
||||
height={200}
|
||||
className="object-contain not-dark:invert"
|
||||
/>
|
||||
</NextLink>
|
||||
<NextLink href="https://globalproductions.ee/" target="_blank">
|
||||
<Image
|
||||
src="/sponsors/Global-productions.png"
|
||||
alt="Global Productions"
|
||||
width={200}
|
||||
height={200}
|
||||
className="object-contain"
|
||||
/>
|
||||
</NextLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
29
src/components/TeaserPage.tsx
Normal file
29
src/components/TeaserPage.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import { useRef } from "react";
|
||||
import HeroSection from "@/components/teaser/HeroSection";
|
||||
import CarouselSection from "@/components/teaser/CarouselSection";
|
||||
import EndSection from "@/components/teaser/EndSection";
|
||||
import Footer from "@/components/teaser/Footer";
|
||||
|
||||
export default function TeaserPage() {
|
||||
const carouselRef = useRef<HTMLElement>(null);
|
||||
|
||||
const handleScrollDown = () => {
|
||||
if (carouselRef.current) {
|
||||
carouselRef.current.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-bg-dark w-full">
|
||||
{/* Main wrapper with bottom border (desktop) */}
|
||||
<div className="lg:border-b-4 lg:border-primary-50">
|
||||
<HeroSection onScrollDown={handleScrollDown} />
|
||||
<CarouselSection sectionRef={carouselRef} />
|
||||
<EndSection />
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes"
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NextThemesProvider>) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import localFont from 'next/font/local';
|
||||
|
||||
// Style 'only' has normal and italic for some reason.
|
||||
// It uses the weight to determine the style used.
|
||||
export const vipnagorgialla = localFont({
|
||||
src: [
|
||||
{
|
||||
path: '../app/fonts/vipnagorgialla/Vipnagorgialla-Rg.otf',
|
||||
weight: '400',
|
||||
style: 'normal',
|
||||
},
|
||||
{
|
||||
path: '../app/fonts/vipnagorgialla/Vipnagorgialla-Rg-It.otf',
|
||||
weight: '400',
|
||||
style: 'italic',
|
||||
},
|
||||
{
|
||||
path: '../app/fonts/vipnagorgialla/Vipnagorgialla-Bd.otf',
|
||||
weight: '700',
|
||||
style: 'normal',
|
||||
},
|
||||
{
|
||||
path: '../app/fonts/vipnagorgialla/Vipnagorgialla-Bd-It.otf',
|
||||
weight: '700',
|
||||
style: 'italic',
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -1,148 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import {
|
||||
KeyIcon,
|
||||
Mails,
|
||||
Ticket,
|
||||
IdCardLanyard,
|
||||
ArrowUpDown,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
// Define the user type based on the database schema
|
||||
export type User = {
|
||||
id: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
steamId: string | null;
|
||||
ticketId: string | null;
|
||||
ticketType: string | null;
|
||||
members: {
|
||||
team: {
|
||||
name: string;
|
||||
} | null;
|
||||
}[];
|
||||
};
|
||||
|
||||
export const columns: ColumnDef<User>[] = [
|
||||
{
|
||||
accessorKey: "id",
|
||||
header: ({ column }) => (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||
className="flex items-center gap-2 h-auto p-0"
|
||||
>
|
||||
<KeyIcon className="h-4 w-4" />
|
||||
ID
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
),
|
||||
cell: ({ row }) => <div className="font-medium">{row.getValue("id")}</div>,
|
||||
},
|
||||
{
|
||||
accessorKey: "firstName",
|
||||
header: ({ column }) => (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||
className="h-auto p-0"
|
||||
>
|
||||
Eesnimi
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
),
|
||||
cell: ({ row }) => <div>{row.getValue("firstName")}</div>,
|
||||
},
|
||||
{
|
||||
accessorKey: "lastName",
|
||||
header: ({ column }) => (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||
className="h-auto p-0"
|
||||
>
|
||||
Perenimi
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
),
|
||||
cell: ({ row }) => <div>{row.getValue("lastName")}</div>,
|
||||
},
|
||||
{
|
||||
accessorKey: "email",
|
||||
header: ({ column }) => (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||
className="flex items-center gap-2 h-auto p-0"
|
||||
>
|
||||
<Mails className="h-4 w-4" />
|
||||
Email
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
),
|
||||
cell: ({ row }) => <div>{row.getValue("email")}</div>,
|
||||
},
|
||||
{
|
||||
accessorKey: "steamId",
|
||||
header: "Steam ID",
|
||||
cell: ({ row }) => {
|
||||
const steamId = row.getValue("steamId") as string | null;
|
||||
return <div>{steamId || "-"}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "ticketId",
|
||||
header: () => (
|
||||
<div className="flex items-center gap-2">
|
||||
<Ticket className="h-4 w-4" />
|
||||
Pileti ID
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const ticketId = row.getValue("ticketId") as string | null;
|
||||
return <div>{ticketId || "-"}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "ticketType",
|
||||
header: ({ column }) => (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||
className="h-auto p-0"
|
||||
>
|
||||
Pileti tüüp
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const ticketType = row.getValue("ticketType") as string | null;
|
||||
return <div>{ticketType || "-"}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "team",
|
||||
accessorFn: (user) =>
|
||||
user.members && user.members.length > 0 && user.members[0].team
|
||||
? user.members[0].team.name
|
||||
: "",
|
||||
header: ({ column }) => (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||
className="flex items-center gap-2 h-auto p-0"
|
||||
>
|
||||
<IdCardLanyard className="h-4 w-4" />
|
||||
Meeskond
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const teamName = row.getValue("team") as string;
|
||||
return <div>{teamName || "-"}</div>;
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,220 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import {
|
||||
ColumnDef,
|
||||
ColumnFiltersState,
|
||||
SortingState,
|
||||
VisibilityState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
import { Eraser, Funnel, Search } from "lucide-react";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
data: TData[];
|
||||
}
|
||||
|
||||
// Search input placeholders
|
||||
function getPlaceholderText(column: string): string {
|
||||
const placeholders = {
|
||||
email: "Filtreeri emaile...",
|
||||
firstName: "Filtreeri eesnimesid...",
|
||||
ticketType: "Filtreeri pileti tüüpi...",
|
||||
team: "Filtreeri tiimi nimesid...",
|
||||
};
|
||||
return placeholders[column as keyof typeof placeholders] || "Otsi...";
|
||||
}
|
||||
|
||||
export function DataTable<TData, TValue>({
|
||||
columns,
|
||||
data,
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
const [sorting, setSorting] = React.useState<SortingState>([]);
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
||||
[],
|
||||
);
|
||||
const [columnVisibility, setColumnVisibility] =
|
||||
React.useState<VisibilityState>({});
|
||||
const [rowSelection, setRowSelection] = React.useState({});
|
||||
|
||||
const [searchColumn, setSearchColumn] = React.useState<string>("email");
|
||||
const [searchValue, setSearchValue] = React.useState<string>("");
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
onSortingChange: setSorting,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
state: {
|
||||
sorting,
|
||||
columnFilters,
|
||||
columnVisibility,
|
||||
rowSelection,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="flex items-center py-4">
|
||||
<Search className="h-6 w-6 mr-2" />
|
||||
<Input
|
||||
placeholder={getPlaceholderText(searchColumn)}
|
||||
value={searchValue}
|
||||
onChange={(event) => {
|
||||
const value = event.target.value;
|
||||
setSearchValue(value);
|
||||
|
||||
// Clear all column filters first
|
||||
table.getColumn("email")?.setFilterValue("");
|
||||
table.getColumn("firstName")?.setFilterValue("");
|
||||
table.getColumn("ticketType")?.setFilterValue("");
|
||||
table.getColumn("team")?.setFilterValue("");
|
||||
|
||||
// Set filter for selected column
|
||||
table.getColumn(searchColumn)?.setFilterValue(value);
|
||||
}}
|
||||
className="max-w-sm rounded-r-none"
|
||||
/>
|
||||
<Select value={searchColumn} onValueChange={setSearchColumn}>
|
||||
<SelectTrigger className="w-48 border-l-0 rounded-l-none">
|
||||
<Funnel />
|
||||
<SelectValue placeholder="Vali otsingu väli" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="email">Email</SelectItem>
|
||||
<SelectItem value="firstName">Eesnimi</SelectItem>
|
||||
<SelectItem value="ticketType">Pileti tüüp</SelectItem>
|
||||
<SelectItem value="team">Tiim</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{searchValue && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="lg"
|
||||
onClick={() => {
|
||||
setSearchValue("");
|
||||
table.getColumn(searchColumn)?.setFilterValue("");
|
||||
}}
|
||||
className="ml-2"
|
||||
>
|
||||
<Eraser />
|
||||
Tühjenda
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext(),
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
Tulemusi ei leitud.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="flex items-center justify-between space-x-2 py-4">
|
||||
<div className="flex-1 text-sm text-muted-foreground">
|
||||
Näidatakse {table.getFilteredRowModel().rows.length} rida(de) kokku{" "}
|
||||
{data.length} reast.
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 text-sm text-muted-foreground">
|
||||
<span>
|
||||
Lehekülg {table.getState().pagination.pageIndex + 1} /{" "}
|
||||
{table.getPageCount()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
Eelmine
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
Järgmine
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
166
src/components/teaser/CarouselSection.tsx
Normal file
166
src/components/teaser/CarouselSection.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { SLIDES, type CarouselSlide } from "./constants";
|
||||
|
||||
function CarouselSlideComponent({
|
||||
slide,
|
||||
t,
|
||||
isActive,
|
||||
}: {
|
||||
slide: CarouselSlide;
|
||||
t: ReturnType<typeof useTranslations>;
|
||||
isActive: boolean;
|
||||
}) {
|
||||
const isLeft = slide.layout === "left";
|
||||
|
||||
return (
|
||||
<div className="relative h-dvh w-full overflow-hidden">
|
||||
{/* Background image */}
|
||||
<Image
|
||||
src={slide.bgImage}
|
||||
alt=""
|
||||
fill
|
||||
sizes="100vw"
|
||||
className="object-cover pointer-events-none"
|
||||
/>
|
||||
|
||||
{/* Content — top padding accounts for the floating heading */}
|
||||
<div
|
||||
className={`relative z-[1] flex h-full gap-8 xl:gap-16 pt-[120px] px-8 xl:px-16 ${isLeft ? "items-end" : "items-end flex-row-reverse"
|
||||
}`}
|
||||
>
|
||||
{/* Text content */}
|
||||
<div
|
||||
className={`flex-1 flex h-full flex-col gap-6 xl:gap-12 justify-center pb-16 ${isLeft ? "items-start" : "items-end"
|
||||
}`}
|
||||
>
|
||||
<h2 className="text-subtitle text-text-light shadow-teaser">{t(slide.titleKey)}</h2>
|
||||
<p className={`text-p-lg text-text-light ${isLeft ? "text-left" : "text-right"}`}>
|
||||
{t(slide.descKey)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Hero image — locked aspect ratio, enters from bottom, pinned to bottom */}
|
||||
<div
|
||||
className="relative shrink-0 w-[35vw] xl:w-[40vw] overflow-hidden shadow-teaser transition-transform duration-700 ease-out"
|
||||
style={{
|
||||
aspectRatio: "850 / 900",
|
||||
transform: isActive ? "translateY(0)" : "translateY(110%)",
|
||||
}}
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={slide.heroImage}
|
||||
alt=""
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface CarouselSectionProps {
|
||||
sectionRef: React.RefObject<HTMLElement | null>;
|
||||
}
|
||||
|
||||
export default function CarouselSection({ sectionRef }: CarouselSectionProps) {
|
||||
const t = useTranslations();
|
||||
const [currentSlide, setCurrentSlide] = useState(0);
|
||||
const [scrollProgress, setScrollProgress] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
if (!sectionRef.current) return;
|
||||
const rect = sectionRef.current.getBoundingClientRect();
|
||||
const sectionHeight = sectionRef.current.offsetHeight;
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
const scrolledInto = -rect.top;
|
||||
const totalScrollable = sectionHeight - viewportHeight;
|
||||
|
||||
if (totalScrollable <= 0) return;
|
||||
|
||||
const progress = Math.max(0, Math.min(1, scrolledInto / totalScrollable));
|
||||
setScrollProgress(progress);
|
||||
const slideIndex = Math.min(SLIDES.length - 1, Math.floor(progress * SLIDES.length));
|
||||
setCurrentSlide(slideIndex);
|
||||
};
|
||||
|
||||
window.addEventListener("scroll", handleScroll, { passive: true });
|
||||
return () => window.removeEventListener("scroll", handleScroll);
|
||||
}, [sectionRef]);
|
||||
|
||||
// Heading follows slide layout: left on "left" slides, right on "right" slides
|
||||
const headingOnRight = SLIDES[currentSlide]?.layout === "right";
|
||||
|
||||
return (
|
||||
<section
|
||||
ref={sectionRef}
|
||||
className="relative hidden lg:block"
|
||||
style={{ height: `${SLIDES.length * 100}dvh` }}
|
||||
>
|
||||
<div className="sticky top-0 h-dvh w-full overflow-hidden border-t-4 border-primary-50">
|
||||
{/* Slides */}
|
||||
{SLIDES.map((slide, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="absolute inset-0 transition-opacity duration-700"
|
||||
style={{ opacity: currentSlide === i ? 1 : 0 }}
|
||||
>
|
||||
<CarouselSlideComponent
|
||||
slide={slide}
|
||||
t={t}
|
||||
isActive={currentSlide === i}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Floating heading — slides between left/right via flex-grow spacers (ease-out) */}
|
||||
<div className="absolute top-[72px] inset-x-0 px-8 xl:px-16 z-10 flex">
|
||||
<div
|
||||
className="transition-[flex-grow] duration-700 ease-out"
|
||||
style={{ flexGrow: headingOnRight ? 1 : 0 }}
|
||||
/>
|
||||
<div className="text-subtitle text-text-light text-shadow-teaser whitespace-nowrap shrink-0">
|
||||
<span>{t("teaser.carousel.heading")}</span>
|
||||
<span className="text-primary">LAN</span>
|
||||
<span>{t("teaser.carousel.headingSuffix")}</span>
|
||||
</div>
|
||||
<div
|
||||
className="transition-[flex-grow] duration-700 ease-out"
|
||||
style={{ flexGrow: headingOnRight ? 0 : 1 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Progress bar — 3 segments */}
|
||||
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 z-10 flex gap-2 w-[240px]">
|
||||
{SLIDES.map((_, i) => {
|
||||
// Each segment: 0 = empty, 1 = full
|
||||
const segmentStart = i / SLIDES.length;
|
||||
const segmentEnd = (i + 1) / SLIDES.length;
|
||||
const segmentProgress = Math.max(
|
||||
0,
|
||||
Math.min(1, (scrollProgress - segmentStart) / (segmentEnd - segmentStart))
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="flex-1 h-2 rounded-full bg-white/30 overflow-hidden"
|
||||
>
|
||||
<div
|
||||
className="h-full bg-primary rounded-full transition-[width] duration-300 ease-out"
|
||||
style={{ width: `${segmentProgress * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
110
src/components/teaser/EndSection.tsx
Normal file
110
src/components/teaser/EndSection.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useCountUp } from "@/hooks/useCountUp";
|
||||
|
||||
function formatK(n: number): string {
|
||||
if (n < 1000) return String(n);
|
||||
const k = n / 1000;
|
||||
if (k % 1 === 0) return `${k}k`;
|
||||
return `${k.toFixed(1)}k`;
|
||||
}
|
||||
|
||||
function CountUpStat({
|
||||
end,
|
||||
suffix,
|
||||
label,
|
||||
format,
|
||||
minWidth,
|
||||
}: {
|
||||
end: number;
|
||||
suffix: string;
|
||||
label: string;
|
||||
format?: (n: number) => string;
|
||||
minWidth?: string;
|
||||
}) {
|
||||
const { ref, count } = useCountUp({ end, duration: 2000 });
|
||||
const display = format ? format(count) : String(count);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-4 lg:gap-8">
|
||||
<p
|
||||
className="text-title tabular-nums"
|
||||
ref={ref as React.RefObject<HTMLParagraphElement>}
|
||||
style={{ minWidth: minWidth ?? "auto" }}
|
||||
>
|
||||
{display}<span className="text-primary">{suffix}</span>
|
||||
</p>
|
||||
<p className="text-p-lg whitespace-pre-line">{label}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function EndSection() {
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
<section className="relative w-full border-t-4 border-primary-50 hidden lg:block">
|
||||
<div className="flex flex-col md:flex-row min-h-dvh">
|
||||
{/* Tickets side */}
|
||||
<div className="relative flex-1 overflow-hidden min-h-[50dvh] md:min-h-0">
|
||||
<Image
|
||||
src="/images/tickets_teaser.png"
|
||||
alt=""
|
||||
fill
|
||||
sizes="(min-width: 768px) 50vw, 100vw"
|
||||
className="object-cover pointer-events-none"
|
||||
/>
|
||||
<div className="relative z-[1] flex flex-col items-center justify-center h-full gap-12 xl:gap-32 p-8 lg:p-16">
|
||||
{/* Ticket stats */}
|
||||
<div className="flex gap-8 xl:gap-16 text-center text-text-light">
|
||||
<CountUpStat end={0} suffix="€" label={t("teaser.tickets.earlyVisitor")} />
|
||||
<CountUpStat end={0} suffix="€" label={t("teaser.tickets.supporter")} />
|
||||
</div>
|
||||
{/* CTA */}
|
||||
<div className="flex flex-col items-center gap-4 xl:gap-8 w-full">
|
||||
<h2 className="text-h1 text-text-light text-center text-shadow-teaser">
|
||||
{t("teaser.tickets.cta")}
|
||||
</h2>
|
||||
<a href="https://fienta.com/tipilan-2026" target="_blank" rel="noopener noreferrer" className="btn-primary-lg text-btn-lg hover:opacity-80">
|
||||
{t("teaser.tickets.buyButton")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Separator — vertical on desktop, horizontal on stacked */}
|
||||
<div className="hidden md:block relative w-[4px] bg-primary-50 shrink-0" />
|
||||
<div className="block md:hidden relative h-[4px] bg-primary-50 shrink-0" />
|
||||
|
||||
{/* Sponsors side */}
|
||||
<div className="relative flex-1 overflow-hidden min-h-[50dvh] md:min-h-0">
|
||||
<Image
|
||||
src="/images/sponsors_teaser.png"
|
||||
alt=""
|
||||
fill
|
||||
sizes="(min-width: 768px) 50vw, 100vw"
|
||||
className="object-cover pointer-events-none"
|
||||
/>
|
||||
<div className="relative z-[1] flex flex-col items-center justify-center h-full gap-12 xl:gap-32 p-8 lg:p-16">
|
||||
{/* Sponsor stats */}
|
||||
<div className="flex gap-8 xl:gap-16 text-center text-text-light">
|
||||
<CountUpStat end={900} suffix="+" label={t("teaser.sponsors.visitors")} minWidth="5ch" />
|
||||
<CountUpStat end={10000} suffix="+" label={t("teaser.sponsors.streamViewers")} format={formatK} minWidth="5ch" />
|
||||
</div>
|
||||
{/* CTA */}
|
||||
<div className="flex flex-col items-center gap-4 xl:gap-8 w-full">
|
||||
<h2 className="text-h1 text-text-light text-center text-shadow-teaser">
|
||||
{t("teaser.sponsors.cta")}
|
||||
</h2>
|
||||
<a href="mailto:tipilan@ituk.ee" className="btn-primary-lg text-btn-lg hover:opacity-80">
|
||||
{t("teaser.sponsors.contactButton")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
77
src/components/teaser/Footer.tsx
Normal file
77
src/components/teaser/Footer.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
import { SiFacebook, SiInstagram, SiTiktok, SiDiscord, SiTwitch, SiYoutube } from "react-icons/si";
|
||||
|
||||
const SOCIAL_LINKS = [
|
||||
{ icon: SiFacebook, href: "https://facebook.com/tipilan.ee", label: "Facebook" },
|
||||
{ icon: SiInstagram, href: "https://instagram.com/tipilan.ee", label: "Instagram" },
|
||||
{ icon: SiTiktok, href: "https://tiktok.com/@tipilan.ee", label: "TikTok" },
|
||||
{ icon: SiDiscord, href: "https://discord.gg/pPhhatZAfA", label: "Discord" },
|
||||
{ icon: SiTwitch, href: "https://twitch.tv/tipilan", label: "Twitch" },
|
||||
{ icon: SiYoutube, href: "https://youtube.com/@tipilan", label: "YouTube" },
|
||||
];
|
||||
|
||||
export default function Footer() {
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
<footer className="bg-bg-dark w-full">
|
||||
{/* Mobile: only social icons */}
|
||||
<div className="flex lg:hidden items-center justify-center gap-6 py-8 px-6 flex-wrap">
|
||||
{SOCIAL_LINKS.map(({ icon: Icon, href, label }) => (
|
||||
<a
|
||||
key={label}
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-text-light hover:text-primary transition-colors"
|
||||
aria-label={label}
|
||||
>
|
||||
<Icon size={32} />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Desktop: full footer */}
|
||||
<div className="hidden lg:flex flex-wrap gap-y-12 gap-x-8 xl:gap-x-0 items-start justify-between p-8 xl:p-16">
|
||||
{/* Left: Organization info */}
|
||||
<div className="flex flex-col gap-4 text-p font-bold text-text-light">
|
||||
<p>{t("teaser.footer.organization")}</p>
|
||||
<p>{t("teaser.footer.regCode")}</p>
|
||||
<p>{t("teaser.footer.bankAccount")}</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[16px]">©</span>
|
||||
<p>{t("teaser.footer.copyright")}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Center: Contact info */}
|
||||
<div className="flex flex-col gap-4 text-p font-bold text-text-light">
|
||||
<p>{t("teaser.footer.studentUnion")}</p>
|
||||
<a href="mailto:tipilan@ituk.ee" className="underline">{t("teaser.footer.email")}</a>
|
||||
<p>{t("teaser.footer.phone")}</p>
|
||||
<p>{t("teaser.footer.address")}</p>
|
||||
</div>
|
||||
|
||||
{/* Right: Social links */}
|
||||
<div className="flex flex-col items-end">
|
||||
<div className="flex flex-wrap gap-6 items-center justify-center w-[169px]">
|
||||
{SOCIAL_LINKS.map(({ icon: Icon, href, label }) => (
|
||||
<a
|
||||
key={label}
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-text-light hover:text-primary transition-colors"
|
||||
aria-label={label}
|
||||
>
|
||||
<Icon size={40} />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
111
src/components/teaser/HeroSection.tsx
Normal file
111
src/components/teaser/HeroSection.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { useTranslations } from "next-intl";
|
||||
import LanguageSwitcher from "@/components/LanguageSwitcher";
|
||||
import { useCountdown } from "@/hooks/useCountdown";
|
||||
import { EVENT_DATE } from "./constants";
|
||||
|
||||
interface HeroSectionProps {
|
||||
onScrollDown: () => void;
|
||||
}
|
||||
|
||||
export default function HeroSection({ onScrollDown }: HeroSectionProps) {
|
||||
const t = useTranslations();
|
||||
const countdown = useCountdown(EVENT_DATE);
|
||||
const pad = (n: number) => String(n).padStart(2, "0");
|
||||
|
||||
return (
|
||||
<section className="relative h-dvh w-full overflow-hidden">
|
||||
{/* Background */}
|
||||
<Image
|
||||
src="/images/hero_teaser.png"
|
||||
alt=""
|
||||
fill
|
||||
className="object-cover pointer-events-none"
|
||||
sizes="100vw"
|
||||
priority
|
||||
/>
|
||||
|
||||
{/* Language switcher — top right */}
|
||||
<div className="absolute top-6 right-6 lg:top-[40px] lg:right-[40px] z-10">
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
|
||||
{/* Center content */}
|
||||
<div className="relative z-[1] flex flex-col items-center justify-center h-full gap-8 lg:gap-16 px-6">
|
||||
{/* Logo */}
|
||||
<Image
|
||||
src="/tipilan-dark.svg"
|
||||
alt="TipiLAN"
|
||||
width={524}
|
||||
height={129}
|
||||
className="w-[min(524px,80vw)] h-auto"
|
||||
priority
|
||||
/>
|
||||
|
||||
{/* Countdown */}
|
||||
<div className="flex gap-6 sm:gap-8 text-center">
|
||||
<div className="flex flex-col items-center gap-2 lg:gap-4 min-w-0 lg:w-[104px]">
|
||||
<span className="text-countdown text-text-light">{countdown.days}</span>
|
||||
<span className="text-countdown-label text-primary whitespace-nowrap">{t("teaser.countdown.days")}</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2 lg:gap-4 min-w-0 lg:w-[86px]">
|
||||
<span className="text-countdown text-text-light">{pad(countdown.hours)}</span>
|
||||
<span className="text-countdown-label text-primary whitespace-nowrap">{t("teaser.countdown.hours")}</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2 lg:gap-4 min-w-0 lg:w-[86px]">
|
||||
<span className="text-countdown text-text-light">{pad(countdown.minutes)}</span>
|
||||
<span className="text-countdown-label text-primary whitespace-nowrap">{t("teaser.countdown.minutes")}</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2 lg:gap-4 min-w-0 lg:w-[103px]">
|
||||
<span className="text-countdown text-text-light">{pad(countdown.seconds)}</span>
|
||||
<span className="text-countdown-label text-primary whitespace-nowrap">{t("teaser.countdown.seconds")}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* YouTube embed — 16:9, constrained to logo width */}
|
||||
<div className="w-[min(524px,80vw)] aspect-video">
|
||||
<iframe
|
||||
src="https://www.youtube.com/embed/p_xwlExVtIk"
|
||||
title="TipiLAN 2026"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
className="w-full h-full rounded-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Mobile CTA buttons — only visible on small screens */}
|
||||
<div className="flex flex-col gap-4 w-full max-w-sm lg:hidden mt-4">
|
||||
<a
|
||||
href="https://fienta.com/tipilan-2026"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn-primary-lg text-btn-lg hover:opacity-80 text-center"
|
||||
>
|
||||
{t("teaser.tickets.buyButton")}
|
||||
</a>
|
||||
<a
|
||||
href="mailto:tipilan@ituk.ee"
|
||||
className="btn-primary-lg text-btn-lg hover:opacity-80 text-center"
|
||||
>
|
||||
{t("teaser.sponsors.contactButton")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scroll button — bottom center, hidden on mobile */}
|
||||
<div className="absolute bottom-[68px] left-1/2 -translate-x-1/2 z-10 hidden lg:flex">
|
||||
<button
|
||||
onClick={onScrollDown}
|
||||
className="bg-primary rounded-full size-[48px] flex items-center justify-center cursor-pointer hover:opacity-80 transition-opacity"
|
||||
aria-label="Scroll down"
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 4V20M12 20L6 14M12 20L18 14" stroke="#0A121F" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
35
src/components/teaser/constants.ts
Normal file
35
src/components/teaser/constants.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
// TipiLAN 2026 event date — September 11, 17:00 Estonian time (EEST = UTC+3)
|
||||
export const EVENT_DATE = new Date("2026-09-11T17:00:00+03:00");
|
||||
|
||||
export interface CarouselSlide {
|
||||
titleKey: string;
|
||||
descKey: string;
|
||||
bgImage: string;
|
||||
heroImage: string;
|
||||
layout: "left" | "right";
|
||||
}
|
||||
|
||||
export const SLIDES: CarouselSlide[] = [
|
||||
{
|
||||
titleKey: "teaser.compete.title",
|
||||
descKey: "teaser.compete.description",
|
||||
bgImage: "/images/compete_teaser.png",
|
||||
heroImage: "/images/compete_hero.png",
|
||||
layout: "left",
|
||||
},
|
||||
{
|
||||
titleKey: "teaser.play.title",
|
||||
descKey: "teaser.play.description",
|
||||
bgImage: "/images/play_teaser.png",
|
||||
heroImage: "/images/play_hero.png",
|
||||
layout: "right",
|
||||
},
|
||||
{
|
||||
titleKey: "teaser.explore.title",
|
||||
descKey: "teaser.explore.description",
|
||||
bgImage: "/images/explore_teaser.png",
|
||||
heroImage: "/images/explore_hero.png",
|
||||
layout: "left",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
|
||||
function AlertDialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
||||
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
|
||||
}
|
||||
|
||||
function AlertDialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
data-slot="alert-dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
|
||||
return (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
data-slot="alert-dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogHeader({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogFooter({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Title
|
||||
data-slot="alert-dialog-title"
|
||||
className={cn("text-lg font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Description
|
||||
data-slot="alert-dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogAction({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Action
|
||||
className={cn(buttonVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogCancel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Cancel
|
||||
className={cn(buttonVariants({ variant: "outline" }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
AlertDialog,
|
||||
AlertDialogPortal,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogTrigger,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogFooter,
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-card text-card-foreground",
|
||||
destructive:
|
||||
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Alert({
|
||||
className,
|
||||
variant,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert"
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-title"
|
||||
className={cn(
|
||||
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-description"
|
||||
className={cn(
|
||||
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription }
|
||||
@@ -1,67 +0,0 @@
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
if (asChild) {
|
||||
return (
|
||||
<Slot
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...(props as React.ComponentProps<typeof Slot>)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Button, buttonVariants };
|
||||
@@ -1,92 +0,0 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-transparent text-card-foreground flex flex-col gap-6 rounded-xl border-3 border-[#1F5673] py-6 shadow-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
};
|
||||
@@ -1,257 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function DropdownMenu({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Trigger
|
||||
data-slot="dropdown-menu-trigger"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuContent({
|
||||
className,
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
data-slot="dropdown-menu-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
variant?: "default" | "destructive"
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Item
|
||||
data-slot="dropdown-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuRadioGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioGroup
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuRadioItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Label
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto size-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSubContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Input };
|
||||
@@ -1,185 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Select({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||
return <SelectPrimitive.Root data-slot="select" {...props} />;
|
||||
}
|
||||
|
||||
function SelectGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
|
||||
}
|
||||
|
||||
function SelectValue({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
|
||||
}
|
||||
|
||||
function SelectTrigger({
|
||||
className,
|
||||
size = "default",
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||
size?: "sm" | "default";
|
||||
}) {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className="size-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectContent({
|
||||
className,
|
||||
children,
|
||||
position = "popper",
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className,
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
data-slot="select-label"
|
||||
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
data-slot="select-separator"
|
||||
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectScrollUpButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectScrollDownButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="skeleton"
|
||||
className={cn("bg-accent animate-pulse rounded-md", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Skeleton }
|
||||
@@ -1,116 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="table-container"
|
||||
className="relative w-full overflow-x-auto"
|
||||
>
|
||||
<table
|
||||
data-slot="table"
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||
return (
|
||||
<thead
|
||||
data-slot="table-header"
|
||||
className={cn("[&_tr]:border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
||||
return (
|
||||
<tbody
|
||||
data-slot="table-body"
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||
return (
|
||||
<tfoot
|
||||
data-slot="table-footer"
|
||||
className={cn(
|
||||
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||
return (
|
||||
<tr
|
||||
data-slot="table-row"
|
||||
className={cn(
|
||||
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||
return (
|
||||
<th
|
||||
data-slot="table-head"
|
||||
className={cn(
|
||||
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||
return (
|
||||
<td
|
||||
data-slot="table-cell"
|
||||
className={cn(
|
||||
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableCaption({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"caption">) {
|
||||
return (
|
||||
<caption
|
||||
data-slot="table-caption"
|
||||
className={cn("text-muted-foreground mt-4 text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function TooltipProvider({
|
||||
delayDuration = 0,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||
return (
|
||||
<TooltipPrimitive.Provider
|
||||
data-slot="tooltip-provider"
|
||||
delayDuration={delayDuration}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function Tooltip({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function TooltipTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
||||
}
|
||||
|
||||
function TooltipContent({
|
||||
className,
|
||||
sideOffset = 0,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
data-slot="tooltip-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
@@ -1,231 +0,0 @@
|
||||
{
|
||||
"games": [
|
||||
{
|
||||
"id": "broken-alliance",
|
||||
"name": "Broken Alliance",
|
||||
"logo": "/images/EXPO/GameDev logos/broken_alliance.png",
|
||||
"developer": "Placeholder Gameworks",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "buckshot-tournament",
|
||||
"name": "Buckshot Tournament",
|
||||
"logo": "/images/miniturniirid/buckshot_tournament.png",
|
||||
"developer": "Mike Klubnika",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "eleball",
|
||||
"name": "EleBall",
|
||||
"logo": "/images/EXPO/GameDev logos/Eleball.png",
|
||||
"developer": "Pulsar Twin",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "craftcraft-simulator",
|
||||
"name": "CraftCraft Simulator",
|
||||
"logo": "/images/EXPO/GameDev logos/craftcat_sim.png",
|
||||
"developer": "Placeholder Gameworks",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "cyber-doc-rogue",
|
||||
"name": "CYBER DOC ROGUE",
|
||||
"logo": "/images/EXPO/GameDev logos/Cyber_Doc_Rogue.png",
|
||||
"developer": "HRA Interactive",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "death-and-taxes",
|
||||
"name": "Death and Taxes",
|
||||
"logo": "/images/EXPO/GameDev logos/death_and_taxes.png",
|
||||
"developer": "Placeholder Gameworks",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "deep-pixel-melancholy",
|
||||
"name": "Deep Pixel Melancholy",
|
||||
"logo": "/images/EXPO/GameDev logos/deep_pixel_melancholy.svg",
|
||||
"developer": "ok/no",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "delusional",
|
||||
"name": "Delusional",
|
||||
"logo": "/images/EXPO/GameDev logos/DELUSIONAL_logo.svg",
|
||||
"developer": "AUTOPLAY",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "hardwired",
|
||||
"name": "Hardwired",
|
||||
"logo": "/images/EXPO/GameDev logos/Hardwired.png",
|
||||
"developer": "Lostbyte",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "hexwave",
|
||||
"name": "HexWave",
|
||||
"logo": "/images/EXPO/GameDev logos/Hexwave.png",
|
||||
"developer": "HardBoyled Games",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "immortal",
|
||||
"name": "IMMORTAL: And the death that follows",
|
||||
"logo": "/images/EXPO/GameDev logos/Immortal.png",
|
||||
"developer": "Mishura Games",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "kalawindow",
|
||||
"name": "Kalawindow",
|
||||
"logo": "/images/EXPO/GameDev logos/Kalawindow.png",
|
||||
"developer": "Lost Empire Entertainment",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "kortel-1996",
|
||||
"name": "Kortel 1996",
|
||||
"logo": "/images/EXPO/GameDev logos/Korter1996.png",
|
||||
"developer": "Mari-Anna Lepasson",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "midnight-souveneirs",
|
||||
"name": "Midnight Souveneirs",
|
||||
"logo": "/images/EXPO/GameDev logos/Midnight_Souveneirs.png",
|
||||
"developer": "Path of Pixels",
|
||||
"description": ""
|
||||
},
|
||||
|
||||
{
|
||||
"id": "planet-hoarders",
|
||||
"name": "Planet Hoarders",
|
||||
"logo": "/images/EXPO/GameDev logos/Planet_hoarders.png",
|
||||
"developer": "Imago Games",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "realm-hearts",
|
||||
"name": "Realm Hearts",
|
||||
"logo": "/images/EXPO/GameDev logos/realm_hearts.png",
|
||||
"developer": "Dreamaster",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "season-of-solitude",
|
||||
"name": "Season of Solitude",
|
||||
"logo": "/images/EXPO/GameDev logos/Seasons_of_Solitude.png",
|
||||
"developer": "Ninjarithm Studio",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "war-torn",
|
||||
"name": "War-torn",
|
||||
"logo": "/images/EXPO/GameDev logos/War_torn.png",
|
||||
"developer": "KingOfTheEnd, ForgottenCup",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "type-n-magic",
|
||||
"name": "Type 'n Magic",
|
||||
"logo": "/images/EXPO/ylikoolid/type_n_magic.png",
|
||||
"developer": "Mikhail Fiadotau",
|
||||
"description": ""
|
||||
}
|
||||
],
|
||||
"universities": [
|
||||
{
|
||||
"id": "deltavr",
|
||||
"name": "DeltaVR",
|
||||
"logo": "/images/EXPO/ylikoolid/deltavr.png",
|
||||
"university": "Tartu Ülikool",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "tiksu-toksu",
|
||||
"name": "Tiksu-Toksu",
|
||||
"logo": "/images/EXPO/ylikoolid/tiksu-toksu.jpg",
|
||||
"university": "Tartu Ülikool",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "psyrreal",
|
||||
"name": "Psyrreal",
|
||||
"logo": "/images/EXPO/ylikoolid/psyrreal.png",
|
||||
"university": "Tartu Ülikool",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "blastronaut",
|
||||
"name": "Blastronaut",
|
||||
"logo": "/images/EXPO/ylikoolid/blastronaut.png",
|
||||
"university": "Tartu Ülikool",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "oh-crap",
|
||||
"name": "Oh Crap!",
|
||||
"logo": "/images/EXPO/ylikoolid/oh_crap.png",
|
||||
"university": "Tallinna Tehnikaülikool",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "dwarf-escape",
|
||||
"name": "Dwarf Escape",
|
||||
"logo": "/images/EXPO/ylikoolid/dwarf_escape.png",
|
||||
"university": "Tallinna Tehnikaülikool",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "void-of-hermes",
|
||||
"name": "Void of Hermes",
|
||||
"logo": "/images/EXPO/ylikoolid/void_of_hermes.png",
|
||||
"university": "Tallinna Tehnikaülikool",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "nullis",
|
||||
"name": "Nullis",
|
||||
"logo": "/images/EXPO/GameDev logos/Nullis.png",
|
||||
"university": "Tartu Ülikool",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "packet-tracers",
|
||||
"name": "Packet Tracers",
|
||||
"logo": "/images/EXPO/ylikoolid/packet_tracers.png",
|
||||
"university": "Tallinna Tehnikaülikool",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "a-bottles-journey",
|
||||
"name": "A Bottle's Journey",
|
||||
"logo": "/images/EXPO/ylikoolid/a_bottles_journey.png",
|
||||
"university": "Tallinna Ülikool",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "wildings",
|
||||
"name": "Wildings",
|
||||
"logo": "/images/EXPO/ylikoolid/wildings.png",
|
||||
"university": "Tallinna Ülikool",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "the-return",
|
||||
"name": "The Return",
|
||||
"logo": "/images/EXPO/ylikoolid/the_return.png",
|
||||
"university": "Tallinna Ülikool",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": "magic-mineral",
|
||||
"name": "Magic Mineral",
|
||||
"logo": "/images/EXPO/ylikoolid/magic_mineral.png",
|
||||
"university": "Tallinna Ülikool",
|
||||
"description": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
export const roomNameKeys = [
|
||||
"boardGames",
|
||||
"bar",
|
||||
"eval",
|
||||
"simRacing",
|
||||
"fighting",
|
||||
"lvlup",
|
||||
"redbull",
|
||||
"kspace",
|
||||
"photowall",
|
||||
"buckshotroulette",
|
||||
"chillArea",
|
||||
"estoniagamedev",
|
||||
"info",
|
||||
"tartuyk",
|
||||
"tly",
|
||||
"gameup",
|
||||
"ittk",
|
||||
"wc",
|
||||
"alzgamer",
|
||||
"studentformula",
|
||||
] as const;
|
||||
|
||||
export type RoomNameKey = (typeof roomNameKeys)[number];
|
||||
|
||||
/**
|
||||
* Static room names that do not require translation.
|
||||
* Centralizing here allows easy management and expansion.
|
||||
*/
|
||||
export const staticRoomNames: Partial<Record<RoomNameKey, string>> = {
|
||||
eval: "EVAL",
|
||||
redbull: "Red Bull",
|
||||
kspace: "K-space.ee",
|
||||
buckshotroulette: "Buckshot Roulette",
|
||||
gameup: "GameUP! Academy",
|
||||
wc: "WC",
|
||||
alzgamer: "Alzgamer",
|
||||
};
|
||||
|
||||
/**
|
||||
* Room metadata for tudengimaja and fuajee rooms.
|
||||
* Centralizes size, position, and color for easier management.
|
||||
*/
|
||||
export interface RoomMeta {
|
||||
color: number;
|
||||
size: { width: number; height: number; depth: number };
|
||||
position: { x: number; y: number; z: number };
|
||||
view: "tudengimaja" | "fuajee";
|
||||
}
|
||||
|
||||
export const roomMeta: Partial<Record<RoomNameKey, RoomMeta[]>> = {
|
||||
// tudengimaja rooms
|
||||
lvlup: [
|
||||
{
|
||||
color: 0xd34e35,
|
||||
size: { width: 7, height: 0.7, depth: 2 },
|
||||
position: { x: 2.8, y: 0, z: 4.75 },
|
||||
view: "tudengimaja",
|
||||
},
|
||||
],
|
||||
kspace: [
|
||||
{
|
||||
color: 0x2c5da3,
|
||||
size: { width: 5, height: 0.7, depth: 2 },
|
||||
position: { x: -3.2, y: 0, z: 4.75 },
|
||||
view: "tudengimaja",
|
||||
},
|
||||
],
|
||||
bar: [
|
||||
{
|
||||
color: 0x4ecdc4,
|
||||
size: { width: 2, height: 0.7, depth: 0.7 },
|
||||
position: { x: -0.5, y: 0, z: 1 },
|
||||
view: "tudengimaja",
|
||||
},
|
||||
],
|
||||
eval: [
|
||||
{
|
||||
color: 0x4d86f7,
|
||||
size: { width: 2, height: 0.7, depth: 1.5 },
|
||||
position: { x: 1.7, y: 0, z: -3.8 },
|
||||
view: "tudengimaja",
|
||||
},
|
||||
],
|
||||
simRacing: [
|
||||
{
|
||||
color: 0xd8b43c,
|
||||
size: { width: 1.5, height: 0.7, depth: 5 },
|
||||
position: { x: -6.8, y: 0, z: -2.2 },
|
||||
view: "tudengimaja",
|
||||
},
|
||||
],
|
||||
redbull: [
|
||||
{
|
||||
color: 0xc02841,
|
||||
size: { width: 2, height: 0.7, depth: 1.5 },
|
||||
position: { x: -3.9, y: 0, z: -3.8 },
|
||||
view: "tudengimaja",
|
||||
},
|
||||
],
|
||||
fighting: [
|
||||
{
|
||||
color: 0xa8f494,
|
||||
size: { width: 3.5, height: 0.7, depth: 1.5 },
|
||||
position: { x: -1.1, y: 0, z: -3.8 },
|
||||
view: "tudengimaja",
|
||||
},
|
||||
],
|
||||
photowall: [
|
||||
{
|
||||
color: 0xd12e7d,
|
||||
size: { width: 2, height: 0.7, depth: 1 },
|
||||
position: { x: -6.6, y: 0, z: 1.9 },
|
||||
view: "tudengimaja",
|
||||
},
|
||||
],
|
||||
buckshotroulette: [
|
||||
{
|
||||
color: 0xedb4b1,
|
||||
size: { width: 2, height: 0.7, depth: 1.5 },
|
||||
position: { x: 3.7, y: 0, z: -3.8 },
|
||||
view: "tudengimaja",
|
||||
},
|
||||
],
|
||||
chillArea: [
|
||||
{
|
||||
color: 0x05512e,
|
||||
size: { width: 1.5, height: 0.7, depth: 5 },
|
||||
position: { x: 5.5, y: 0, z: -2.1 },
|
||||
view: "tudengimaja",
|
||||
},
|
||||
{
|
||||
color: 0x05512e,
|
||||
size: { width: 3.8, height: 0.7, depth: 1.5 },
|
||||
position: { x: 0.4, y: 0, z: -0.2 },
|
||||
view: "tudengimaja",
|
||||
},
|
||||
],
|
||||
alzgamer: [
|
||||
{
|
||||
color: 0xd08331,
|
||||
size: { width: 3.5, height: 0.7, depth: 1.5 },
|
||||
position: { x: -3.3, y: 0, z: -0.2 },
|
||||
view: "tudengimaja",
|
||||
},
|
||||
],
|
||||
wc: [
|
||||
{
|
||||
color: 0x332b5d,
|
||||
size: { width: 2, height: 0.7, depth: 2 },
|
||||
position: { x: 5.3, y: 0, z: 1.5 },
|
||||
view: "tudengimaja",
|
||||
},
|
||||
],
|
||||
// fuajee rooms
|
||||
ittk: [
|
||||
{
|
||||
color: 0xd12e7d,
|
||||
size: { width: 4.5, height: 0.5, depth: 3 },
|
||||
position: { x: -3.8, y: 0, z: 3.3 },
|
||||
view: "fuajee",
|
||||
},
|
||||
],
|
||||
tartuyk: [
|
||||
{
|
||||
color: 0x365591,
|
||||
size: { width: 5, height: 0.5, depth: 2.5 },
|
||||
position: { x: 2.7, y: 0, z: -1.7 },
|
||||
view: "fuajee",
|
||||
},
|
||||
],
|
||||
estoniagamedev: [
|
||||
{
|
||||
color: 0x183bbf,
|
||||
size: { width: 6, height: 0.5, depth: 2.5 },
|
||||
position: { x: -5.8, y: 0, z: -1.7 },
|
||||
view: "fuajee",
|
||||
},
|
||||
{
|
||||
color: 0x183bbf,
|
||||
size: { width: 2, height: 0.5, depth: 5.5 },
|
||||
position: { x: -7.7, y: 0, z: 2.1 },
|
||||
view: "fuajee",
|
||||
},
|
||||
],
|
||||
info: [
|
||||
{
|
||||
color: 0xff6347,
|
||||
size: { width: 2, height: 0.5, depth: 2 },
|
||||
position: { x: -1, y: 0, z: -2 },
|
||||
view: "fuajee",
|
||||
},
|
||||
],
|
||||
tly: [
|
||||
{
|
||||
color: 0xa82838,
|
||||
size: { width: 4, height: 0.5, depth: 2 },
|
||||
position: { x: 7.5, y: 0, z: -1.8 },
|
||||
view: "fuajee",
|
||||
},
|
||||
],
|
||||
gameup: [
|
||||
{
|
||||
color: 0x228b22,
|
||||
size: { width: 2, height: 0.5, depth: 1.5 },
|
||||
position: { x: 10.7, y: 0, z: -2 },
|
||||
view: "fuajee",
|
||||
},
|
||||
],
|
||||
studentformula: [
|
||||
{
|
||||
color: 0x20b2aa,
|
||||
size: { width: 2.5, height: 0.5, depth: 1.5 },
|
||||
position: { x: 13, y: 0, z: -2 },
|
||||
view: "fuajee",
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,153 +0,0 @@
|
||||
## 1. General Information
|
||||
1. **1.1** The Counter-Strike 2 (hereinafter CS2) tournament takes place October 24-26, 2025 at Tallinn University of Technology (TalTech) premises, Ehitajate tee 5, Tallinn.
|
||||
2. **1.2** The tournament prize pool is 5,750€, distributed as follows:
|
||||
1. **1.2.1** TOP 3 prize pool is 5,000€:
|
||||
- **1.2.1.1** First place team - 500€ per player
|
||||
- **1.2.1.2** Second place team - 300€ per player
|
||||
- **1.2.1.3** Third place team - 200€ per player
|
||||
2. **1.2.2** LCQ prize pool is 750€:
|
||||
- **1.2.2.1** First place team - 100€ per player
|
||||
- **1.2.2.2** Second place team - 50€ per player
|
||||
3. **1.3** Prize money will be paid to the player's bank account.
|
||||
1. **1.3.1** For underage players, the prize will be paid to the parent/guardian's bank account.
|
||||
4. **1.4** Throughout the tournament, all participants must comply with the laws of the Republic of Estonia, TipiLAN house rules, and event regulations.
|
||||
5. **1.5** By purchasing a ticket, each participant gives permission to be photographed, filmed, and for all photographic, audio, and video material to be used for event documentation and marketing.
|
||||
6. **1.6** The CS2 main tournament will be recorded and streamed on Twitch and YouTube platforms.
|
||||
7. **1.7** All tournament-related communications between team members (e.g., in-game chat, voice communications, Discord conversations, etc.) will be recorded.
|
||||
8. **1.8** When self-streaming the game, the stream delay must be at least 5 minutes.
|
||||
9. **1.9** Organizers have the right to use participants' personal information solely within the framework of conducting the event.
|
||||
10. **1.10** The organizing team has the right to modify and edit rules as needed without prior notice.
|
||||
11. **1.11** The organizing team is impartial towards all participants.
|
||||
|
||||
## 2. CS2 Main Tournament Team and Team Composition
|
||||
1. **2.1** A CS2 main tournament team (hereinafter team) core roster consists of five main members, one of whom is the team captain;
|
||||
2. **2.2** The team captain is the team's representative who:
|
||||
1. **2.2.1** Serves as the contact person for the organizing team;
|
||||
2. **2.2.2** Registers the team for the tournament;
|
||||
3. **2.2.3** Is responsible for the team's behavior and actions;
|
||||
4. **2.2.4** Represents the team in cases of warnings, disqualifications, disputes, and timeouts.
|
||||
|
||||
3. **2.3** Each team may have one substitute player who is not part of the team's core roster:
|
||||
1. **2.3.1** The substitute player must purchase a separate substitute player ticket;
|
||||
2. **2.3.2** The substitute player can replace any member of the team's core roster during the tournament;
|
||||
3. **2.3.3** The same rights and requirements apply to the substitute player as to the team's core roster.
|
||||
|
||||
4. **2.4** Team core roster members may be replaced before the team registration deadline:
|
||||
**2.4.1** Player replacement is done through Fienta;
|
||||
**2.4.2** When replacing a member, the team retains the right to a substitute player;
|
||||
**2.4.3** The same rights and requirements apply to replacement players as to the team's core roster.
|
||||
|
||||
5. **2.5** When replacing the team captain (e.g., with a substitute or replacement player), the team decides internally who receives the team captain's rights and responsibilities;
|
||||
|
||||
6. **2.6** Teams must confirm their participation, final team core roster, and team name 2 weeks before the tournament in an email sent to the team captain. If a core member drops out after the final roster has been confirmed, the team must use their substitute player.
|
||||
|
||||
7. **2.7** If a team withdraws from tournament participation before the registration deadline, the participation fee will be refunded to the team.
|
||||
|
||||
8. **2.8** All team members (including core roster, substitute player, replacement player(s)) may only belong to one team at a time and represent only themselves (i.e., it is forbidden to have someone else play on their behalf);
|
||||
|
||||
9. **2.9** All team members (including core roster, substitute player, replacement player(s)) must be at least 16 years old by the tournament registration date;
|
||||
|
||||
10. **2.10** No team member may be a citizen of the Russian Federation or the Republic of Belarus.
|
||||
|
||||
11. **2.11** Teams (including core roster, substitute player, replacement player(s)) are not allowed to:
|
||||
1. **2.11.1** Use coaches;
|
||||
2. **2.11.2** Display team sponsors during the tournament;
|
||||
3. **2.11.3** Play in the interests of another team or team member;
|
||||
|
||||
12. **2.12** Team name:
|
||||
1. **2.12.1** Must not be offensive, vulgar, political, or otherwise inappropriate;
|
||||
2. **2.12.2** Must not contain emoticons or other symbols that are not letters;
|
||||
3. **2.12.3** Must be changed upon request from the organizing team.
|
||||
|
||||
## 3. Equipment
|
||||
1. **3.1** The organizing team provides participants with internet, ethernet cable, extension cords, and a seat with a table.
|
||||
2. **3.2** Tournament participants are responsible for bringing and ensuring the functionality of all other necessary equipment for participation.
|
||||
|
||||
## 4. Schedule
|
||||
1. **4.1** All team members must be present one hour before the scheduled tournament start.
|
||||
2. **4.2** Teams competing in a match round must be ready at their designated locations 10 minutes before the round begins. It is the team captain's responsibility to ensure their team is in the right place at the right time and ready to start.
|
||||
3. **4.3** Match round start times are announced by the organizing team at the tournament start or upon completion of the previous match round.
|
||||
4. **4.4** If a player experiences technical issues with equipment or the game, they must immediately notify the match referee or organizing team.
|
||||
5. **4.5** The organizing team has the right to make changes to the schedule.
|
||||
6. **4.6** The organizing team is obligated to keep all participants informed of any delays and changes.
|
||||
|
||||
## 5. Game Version and Settings
|
||||
1. **5.1** The latest version of CS2 will be used throughout the tournament. If the organizing team deems the latest available version unplayable due to bugs or other changes, an older version may be used (if possible).
|
||||
2. **5.2** The following settings will be used in the CS2 tournament:
|
||||
1. **5.2.1** Best of 24 (mp_maxrounds 24)
|
||||
2. **5.2.2** Round time: 1 minute 55 seconds (mp_roundtime 1.92)
|
||||
3. **5.2.3** Starting money: $800 (mp_startmoney 800)
|
||||
4. **5.2.4** Freeze time at round start: 20 seconds (mp_freezetime 20)
|
||||
5. **5.2.5** Buy time: 20 seconds (mp_buytime 20)
|
||||
6. **5.2.6** Bomb timer: 40 seconds (mp_c4timer 40)
|
||||
7. **5.2.7** Overtime rounds: best of six (6) (mp_overtime_maxrounds 6)
|
||||
8. **5.2.8** Overtime starting money: $12,500 (mp_overtime_startmoney 12500)
|
||||
9. **5.2.9** Round restart delay: 5 seconds (mp_round_restart_delay 5)
|
||||
10. **5.2.10** Prohibited items: none (mp_items_prohibited "")
|
||||
|
||||
3. **5.3** Overtime: if there is a tie after all 24 rounds, overtime will be played as best of six. At the start of each overtime, teams remain on the side they played in the previous half - sides are switched at halftime. Teams continue overtime until a winner is found.
|
||||
|
||||
4. **5.4** Pause: each team is allowed to call a timeout for thirty (30) seconds up to three (3) times during regulation rounds. Pauses can be called by participants typing "!pause" in the in-game chat. Players are allowed to use all three pauses consecutively. The match referee can call a pause unilaterally if necessary.
|
||||
|
||||
5. **5.5** Technical pause: each team has the right to use a technical pause when necessary. To start a pause, the command ".tech" must be entered in the in-game chat. Technical pauses may only be used for valid reasons, when technical problems occur, and organizers must be immediately notified via Discord after starting the pause.
|
||||
|
||||
## 6. Map Selection
|
||||
1. **6.1** The tournament takes place in two stages:
|
||||
1. **6.1.1** Swiss system: initial phase played in Bo1 format. The top 16 teams advance to the playoffs.
|
||||
2. **6.1.2** Playoffs: played in double elimination format, where winners' bracket matches are Bo3 and losers' bracket matches are Bo1.
|
||||
2. **6.2** Maps are selected from the currently active Valve Active Duty Map Group.
|
||||
3. **6.3** Best of 1 (Bo1): coin flip winner decides whether they are Team A or Team B. Team A starts and the process is as follows:
|
||||
1. **6.3.1** Team A removes two maps.
|
||||
2. **6.3.2** Team B removes three maps.
|
||||
3. **6.3.3** Team A removes one map.
|
||||
4. **6.3.4** The remaining map is played.
|
||||
|
||||
4. **6.4** Best of 3 (Bo3): coin flip winner decides whether they are Team A or Team B. Team A starts and the process is as follows:
|
||||
1. **6.4.1** Team A removes one map.
|
||||
2. **6.4.2** Team B removes one map.
|
||||
3. **6.4.3** Team A picks one map.
|
||||
4. **6.4.4** Team B picks one map.
|
||||
5. **6.4.5** Team B removes one map.
|
||||
6. **6.4.6** Team A removes one map.
|
||||
7. **6.4.7** The remaining map is the decider if needed.
|
||||
|
||||
## 7. Prohibited Activities in CS2 Tournament
|
||||
1. **7.1** Any form of cheating, including methods not mentioned here, is prohibited.
|
||||
2. **7.2** The use of scripts is prohibited (except for weapon/grenade buying, jump throwing).
|
||||
3. **7.3** Movement through walls, floors, and ceilings, including sky-walking, is prohibited.
|
||||
4. **7.4** "Pixel walking" - standing, crouching, walking, and other activities on invisible map boundaries is prohibited.
|
||||
5. **7.5** Bombs must be placed so they can be defused. This does not include situations where multiple players are needed to defuse the bomb.
|
||||
6. **7.6** Players are not allowed to place an armed bomb where it cannot be defused, where it doesn't touch a solid object, or where it doesn't make the normal "beeping" sound.
|
||||
7. **7.7** Players are not allowed to give items names (nametags) that violate what is stated in the TipiLAN house rules.
|
||||
8. **7.8** Custom game files/data/drivers are not allowed.
|
||||
9. **7.9** The use of character model skins (agent skins) is not allowed.
|
||||
10. **7.10** Exploiting in-game bugs is prohibited.
|
||||
11. **7.11** Any form of match fixing, influencing, fraud, and manipulation is strictly prohibited and means immediate team disqualification. Organizers have the right to notify Estonian law enforcement agencies in case of suspicion.
|
||||
|
||||
## 8. Penalties
|
||||
1. **8.1** Violation of in-game and out-of-game rules (see section 7) and house rules (see TipiLAN house rules) is punishable.
|
||||
2. **8.2** A team member who violates rules will first receive a first verbal warning. After a second/repeated violation, the team member receives a second verbal warning. On the third occasion, the team member is disqualified from the tournament. The team has the right to use their substitute player.
|
||||
3. **8.3** A team member who fails to appear for the tournament or match round or leaves during the tournament without valid reason receives a tournament disqualification. The team has the right to use their substitute player.
|
||||
4. **8.4** A team member who is not present and ready 10 minutes before their match round begins (no-show situation) receives a tournament disqualification, except if the team member is late for valid reasons and has notified the organizing team. The team has the right to use their substitute player.
|
||||
5. **8.5** If the organizing team determines that a team member has violated section 7, the entire team is immediately disqualified from the tournament. The rule-violating team member receives a permanent ban from TipiLAN tournaments.
|
||||
6. **8.6** If during match review the organizing team has reasonable suspicion that a team member has violated section 7, the entire team is immediately disqualified from the tournament. The rule-violating team member receives a permanent ban from TipiLAN tournaments.
|
||||
7. **8.7** Teams have the right to withdraw from tournament participation, i.e., give themselves a disqualification.
|
||||
8. **8.8** In case of team disqualification, the opposing team automatically wins the current match round.
|
||||
9. **8.9** Only the team captain can dispute a team member/team disqualification. The dispute must be submitted to the organizing team within 15 minutes of the team being notified of the disqualification.
|
||||
1. **8.9.1** The organizing team has up to 25 minutes to make a decision regarding the dispute. During this time, the match round is paused and teams may not leave their positions.
|
||||
10. **8.10** Teams have the right to file a protest in situations where there is a problem that may affect or affects the match round or team:
|
||||
1. **8.10.1** Protests may be filed with the organizing team within 5 minutes of discovering the problem.
|
||||
2. **8.10.2** The organizing team has up to 25 minutes to make a decision regarding the protest. During this time, the match round is paused and teams may not leave their positions.
|
||||
11. **8.11** The match referee notifies the rule-violating team member, their team, and the opposing team of the violation, its content, and consequences.
|
||||
12. **8.12** The organizing team has the right to pause a match round and end the pause at any time as needed.
|
||||
13. **8.13** The organizing team is obligated to publicly announce all eliminations and calculations and further changes.
|
||||
|
||||
## 9. Contact
|
||||
For any event-related questions, problems, concerns, etc., contact the TipiLAN organizing team.
|
||||
|
||||
## 10. References
|
||||
- https://pro.eslgaming.com/csgo/proleague/rules/
|
||||
- https://github.com/ValveSoftware/counter-strike_rules_and_regs/blob/main/tournament-operation-requirements.md
|
||||
- https://www.hltv.org/events/8037/iem-dallas-2025
|
||||
- https://www.hltv.org
|
||||
- https://liquipedia.net/counterstrike/Portal:Maps/CS2
|
||||
@@ -1,29 +0,0 @@
|
||||
Event participation house rules apply to everyone, both visitors and competitors. In case of violation of house rules, TipiLAN reserves the right to remove the participant from the event and, if necessary, notify the police. In the case of an underage participant, we will notify their parents or guardians in case of serious house rule violations.
|
||||
|
||||
# Participant Reminders
|
||||
|
||||
1. Upon arrival, exchange your ticket for a wristband.
|
||||
2. Participants must be at least 16 years old. Ticket controllers may ask you for identification.
|
||||
3. The event will be photographed and filmed, and event content will be covered in various media channels.
|
||||
4. If you have TipiLAN-provided accommodation, notify us when exchanging your ticket for a wristband.
|
||||
5. If you bring your own computer, you will be guided on where to set it up when receiving your wristband.
|
||||
|
||||
# Event House Rules
|
||||
|
||||
1. Participants are obligated to behave politely and with dignity and respect other event participants.
|
||||
2. TipiLAN does not tolerate:
|
||||
2.1. Hate speech based on national, racial, gender, sexual or religious affiliation, disabilities, appearance or age; harassment, threatening, offensive or aggressive behavior, incitement or support thereof
|
||||
2.2. This applies both on the event premises (IRL) and in online environments related to the event.
|
||||
3. Participants are obligated to treat the event building, inventory and furnishings with care. It is forbidden to break, stain or move items that do not belong to the participant.
|
||||
3.1. If a participant has organizer-provided accommodation, then in the accommodation area the participant is obligated to be quiet and allow companions to rest.
|
||||
3.2. Persons who are not provided accommodation there may not be invited to the accommodation area.
|
||||
4. TipiLAN is not responsible for participants' personal property.
|
||||
4.1. The organizer-provided accommodation area is lockable and unauthorized persons are not allowed there, but regardless of this, it is worth keeping an eye on your valuables.
|
||||
4.2. If there is suspicion that theft has occurred, this must be immediately reported to the organizer.
|
||||
4.3. When finding lost items, please give them to the organizer or take them to *lost & found* (Merchandise table).
|
||||
5. Smoking and using vapes is prohibited on the event premises. There are designated smoking areas outside for this purpose.
|
||||
6. Illegal substances or drugs, sharp objects, firearms, explosives or incendiary materials and other items that may harm participants or others may not be brought to the event.
|
||||
7. Underage participants are prohibited from consuming alcohol or using nicotine-containing products.
|
||||
7.1. When purchasing alcohol from the bar, participants are obligated to show identification upon request by bar staff.
|
||||
8. Participants are obligated to behave responsibly regarding alcohol.
|
||||
9. Any form of gambling for money or other benefits is prohibited.
|
||||
@@ -1,131 +0,0 @@
|
||||
## 1. General Information
|
||||
|
||||
1. **1.1** The League of Legends (hereinafter LoL) tournament takes place as a two-day event on **October 24-25, 2025** at Tallinn University of Technology (TalTech) premises, Ehitajate tee 5, Tallinn.
|
||||
2. **1.2** The tournament prize pool is **3500€**, distributed as follows:
|
||||
1. **1.2.1** First place team – **300€** per participant
|
||||
2. **1.2.2** Second place team – **200€** per participant
|
||||
3. **1.2.3** Third place team – **100€** per participant
|
||||
3. **1.3** Prize money will be paid to the participant's bank account
|
||||
1. **1.3.1** For underage participants, the prize will be paid to their parent/guardian's bank account
|
||||
|
||||
## 2. Teams and Players
|
||||
|
||||
1. **2.1** A team must have:
|
||||
1. **2.1.1** Five members (each member hereinafter referred to individually as *Player*)
|
||||
2. **2.1.2** One of the members is the team Captain, who also serves as the spokesperson for the entire team
|
||||
3. **2.1.3** All members must be at least 16 years old at the time of team registration
|
||||
4. **2.1.4** Players cannot be citizens of the Russian Federation or the Republic of Belarus
|
||||
5. **2.1.5** Teams are not allowed to use a coach during the tournament
|
||||
6. **2.1.6** One team member may be substituted, who must also be registered and physically present
|
||||
2. **2.2** Players must provide only truthful information about themselves and be prepared to prove their identity to the Organizer.
|
||||
3. **2.3** Team name and logo as well as player gaming alias and avatar must be appropriate, including not being offensive, containing profanity, vulgarity, political or religious messages or symbols, or references to alcohol, drugs, and other intoxicants.
|
||||
4. **2.4** A player represents only themselves throughout the tournament (i.e., no one else may compete in their place).
|
||||
5. **2.5** All players under 18 years of age are prepared to provide parental/guardian consent for tournament participation to the Organizer.
|
||||
6. **2.6** Players must be polite throughout the tournament and respect fellow competitors, organizers, and visitors. TipiLAN does not tolerate hate speech based on national, racial, gender, sexual, or religious affiliation, disabilities, appearance, or age; harassment, threatening, insulting, or aggressive behavior, incitement or support thereof. It is forbidden to disclose another participant's personal data (doxing) or threaten to do so. It is also forbidden to spam or overwhelm streams (hijacking) and incite such behavior. In such cases, the organizer may remove the player from the tournament.
|
||||
7. **2.7** Teams must be registered on both the TipiLAN website and the **[challengermode](https://www.challengermode.com/s/TipiLAN/tournaments/4b7d832b-7cf3-406a-8425-08ddd311ac8a)** tournament page.
|
||||
|
||||
## 3. Pre-Game
|
||||
|
||||
1. **3.1** Tournament participation, matches, and tournament bracket all operate through the **[challengermode](https://www.challengermode.com/s/TipiLAN/tournaments/4b7d832b-7cf3-406a-8425-08ddd311ac8a)** environment:
|
||||
1. **3.1.1** The entire team, including substitute players, must be registered for the tournament
|
||||
2. **3.1.2** Players must have their highest-ranked account (including from other servers) and the account they will participate with in tournament games linked in challengermode (Challengermode allows linking two regions simultaneously)
|
||||
3. **3.1.3** The tournament takes place on the **EU West** server
|
||||
4. **3.1.4** Players may not use any account other than those linked in challengermode
|
||||
2. **3.2** Matches in the challengermode environment are automatic. For a new match, there are **10 minutes** to be ready. Then there are another **10 minutes** to join the generated lobby.
|
||||
3. **3.3** Draft can begin when both sides have indicated their readiness:
|
||||
1. **3.3.1** Placeholders are not allowed. If a champion is locked in draft, it must be played
|
||||
2. **3.3.2** Before draft, players must be in role-appropriate order: **Top – Jungle – Mid – Bot – Support**
|
||||
3. **3.3.3** Using Drafter.lol for external drafting is allowed, but only with consent from both sides
|
||||
4. **3.3.4** Intentional delays are not allowed
|
||||
4. **3.4** Only players, official streamers, and referees may join the match lobby
|
||||
|
||||
## 4. In-Game Procedures
|
||||
|
||||
1. **4.1** A game is officially started (game of record (hereinafter GOR)) when all 10 players are on the map and the game has reached the first real interaction (see below). Once the game has reached GOR status, it cannot be restarted. Game score will be officially tracked from this moment. After reaching GOR status, the game can only be restarted in cases where completing the current game is not possible for a valid reason. GOR conditions are as follows:
|
||||
1. **4.1.1** Either team successfully attacks or uses an ability against minions, jungle creeps, structures, or enemies
|
||||
2. **4.1.2** Enemies see each other (exception - Clairvoyance does not guarantee GOR)
|
||||
3. **4.1.3** Entering enemy territory
|
||||
4. **4.1.4** The game has lasted 2 minutes
|
||||
2. **4.2** Game pausing:
|
||||
1. **4.2.1** Players are not allowed to leave the match area during game pause, except when officially authorized
|
||||
2. **4.2.2** Organizers may pause the game as needed
|
||||
3. **4.2.3** Either team has the right to take a total of up to **15 minutes** of pause during a match for valid reasons (disconnect, software or hardware problems, something happens to a player)
|
||||
3. **4.3** The game may only be resumed with consent from both sides or with referee permission
|
||||
4. **4.4** If an obstacle arises in fair game conduct (gamebreaking bug, network connection, etc.), the referee will determine new instructions for game conduct
|
||||
|
||||
## 5. Match Completion
|
||||
|
||||
1. **5.1** The match winner is the team that has won the most games (in case of multi-game matches)
|
||||
1. **5.1.1** The organizer presents team win and loss standings to all participants in an accessible manner and informs all players where they can be viewed
|
||||
2. **5.1.2** After each match, the tournament bracket is updated in the challengermode environment and new matches are assigned from there at the first opportunity
|
||||
|
||||
## 6. Tournament Elimination
|
||||
|
||||
1. **6.1** A team may decide at any time to stop participating in the tournament by notifying the referee and/or organizer.
|
||||
2. **6.2** Penalties earned until elimination remain in effect until the end of the tournament.
|
||||
3. **6.3** If a team does not show up or is not ready by the agreed start time, the Organizer may eliminate the team from the tournament to ensure schedule adherence.
|
||||
4. **6.4** The list of registered team members cannot be changed during the tournament, even if a player has left.
|
||||
1. **6.4.1** If a member's departure causes the number of team participants to fall below what is needed to play, the Organizer must eliminate the team from the tournament.
|
||||
5. **6.5** If a team wishes to drop out or is eliminated from the tournament during a match, the team must give a forfeit victory.
|
||||
6. **6.6** All eliminations and removals must be made publicly known immediately.
|
||||
|
||||
## 7. Penalties
|
||||
|
||||
1. **7.1** Referees assign penalties following the guidelines provided in this document.
|
||||
2. **7.2** Only referees may assign penalties.
|
||||
3. **7.3** The referee informs both the content of the violation and the assigned penalty to the rule-violating player, their team, and the opposing team. Additional information may be added as needed.
|
||||
4. **7.4** The referee must first confirm the violation and only then assign the penalty. A penalty cannot have a suitable violation "invented" for it.
|
||||
5. **7.5** The referee must be impartial; team skill level should not be decisive in monitoring violations and penalties.
|
||||
6. **7.6** Penalties may be assigned to both the entire team and individual team members.
|
||||
7. **7.7** The content of a penalty may be valid for either one game or the entire tournament participation period.
|
||||
8. **7.8** A team cannot protest penalties assigned to the opposing team.
|
||||
9. **7.9** Penalties may be as follows:
|
||||
1. **7.9.1 WARNING:** a warning is a recorded notice to a player or team for a minor violation.
|
||||
2. **7.9.2 BAN LOSS:** The team may not ban a certain number of characters in the game following the penalty. In this case, the referee monitors that the team does not select the penalty-specified number of bans and instead lets the timer run to zero.
|
||||
3. **7.9.3 GAME LOSS:** The team receives an automatic loss in one game.
|
||||
4. **7.9.4 MATCH LOSS:** The team receives an automatic match loss.
|
||||
5. **7.9.5 DISQUALIFICATION:** Disqualification applies to the entire team. In this case, the team forfeits all victories. If disqualification results from escalating violations, the team gets the portion of victories they had earned by that point.
|
||||
- In some cases, the referee is allowed to disqualify only one player instead of the team. This is the case when the player's violation does not affect the opposing team in any way and was done without involving anyone from their own team. Generally, this is possible when the player's violation falls under the category of Inappropriate Behavior - Serious Violation. In this case, the remaining team may continue in the tournament if a substitute player is available. Otherwise, the entire team must also be eliminated from the tournament.
|
||||
10. **7.10** Penalty severity levels are as follows:
|
||||
Warning - warning - loss of ban selection right - game loss - match loss - disqualification
|
||||
11. **7.11** Tournament violations are divided as follows:
|
||||
1. **7.11.1 EXTERNAL ASSISTANCE USE:** this violation is recorded when a team communicates during the game with anyone other than their own team and, as a result, in the referee's judgment, gains an advantage in the game. For this violation, it is assumed that it was not an intentional attempt to cheat. Intentionally seeking unfair advantage falls under the Inappropriate Behavior - Cheating category. The penalty is a warning.
|
||||
2. **7.11.2 IGNORING INSTRUCTIONS:** Every player has an obligation to follow Organizer and referee instructions. Ignoring them may result in delays and disputes. The penalty is loss of first selection right.
|
||||
12. **7.12** Inappropriate behavior.
|
||||
Inappropriate behavior disrupts tournament flow and may negatively affect safety, competitive spirit, game enjoyment, or tournament fairness and integrity. Inappropriate behavior is not the same as competitive behavior.
|
||||
Inappropriate behavior violations are divided into:
|
||||
1. **7.12.1 MINOR VIOLATION:** this includes behavior that is unpleasant, unethical, or disruptive, such as excessive profanity; demanding that the opponent receive a penalty after the referee has made their decision; trash talking; littering, etc.
|
||||
The penalty for minor violations is a warning.
|
||||
2. **7.12.2 MODERATE VIOLATION:** this includes three different types of violations
|
||||
- ignores referee or Organizer instructions meant specifically for one team or one player
|
||||
- uses hate speech publicly against someone
|
||||
- is aggressive or violent, but it is not directed at another person
|
||||
The penalty for moderate violations is game loss.
|
||||
3. **7.12.3 SERIOUS VIOLATION:** this includes behavior that is clearly contrary to tournament conduct rules and good practices, such as intentionally breaking tournament equipment or dirtying or damaging the room. The penalty for serious violations is disqualification, removal from tournament premises, or in extreme cases, the Organizer informs the police.
|
||||
4. **7.12.4 MATCH FIXING:** match fixing is considered an agreement between two teams to play unfairly against other teams and attempt to influence tournament results.
|
||||
The penalty for match fixing is disqualification of both teams.
|
||||
5. **7.12.5 BRIBERY AND BETTING:** teams are forbidden from withdrawing from the tournament or attempting to change match results for any favor (not necessarily just monetary). It is also forbidden to offer referees incentives to influence game results. Betting on game results is also forbidden.
|
||||
The penalty for bribery and betting is disqualification.
|
||||
6. **7.12.6 AGGRESSIVE BEHAVIOR:** this includes all manifestations of aggression directed at people, including threats and obviously real violence.
|
||||
The penalty is disqualification and removal from premises, in extreme cases the Organizer informs the police.
|
||||
7. **7.12.7 THEFT:** although every participant has an obligation to keep an eye on their property, there is still an expectation that participant behavior is consistent with good practices.
|
||||
The penalty for theft is disqualification and removal from premises. If the situation cannot be resolved on-site, the Organizer informs the police.
|
||||
8. **7.12.8 ALCOHOL AND INTOXICATION:** Alcohol consumption within the event is forbidden. In case of excessive intoxication, the Organizer has the right to remove the participant from the premises.
|
||||
- if the intoxicated participant is underage, their parents and police will be informed.
|
||||
9. **7.12.9 CHEATING:** this includes intentional player action to gain an advantage in the game. Cheating does not have to be successful to result in penalty.
|
||||
Activities that fall under cheating include, for example:
|
||||
- attempting to view one's own game in spectator mode or receiving information from someone who can watch the game in spectator mode
|
||||
- any attempt to modify the game itself or use additional software not normally included with the game, including changing in-game zoom, using UI modifications that change attack range or make tower firing range visible, using spawn timers, etc. VOIP program use does not fall under this category.
|
||||
- appearing as another player or under a false name, playing using another player's summoner name, or account sharing
|
||||
- intentionally breaking or attempting to modify equipment to cause delays, get pauses, or otherwise influence game flow
|
||||
- intentional exploitation of in-game bugs to gain advantage, i.e., using various glitches to one's benefit.
|
||||
The penalty for cheating is disqualification.
|
||||
|
||||
## 8. Tournament Format
|
||||
|
||||
1. **8.1** The tournament operates on Fearless Draft principles. This means that champions picked during a series cannot be picked in subsequent games until the series ends.
|
||||
2. **8.2** The tournament operates in Round Robin + Single Elimination format
|
||||
This means the first round consists of 2 groups of 6 teams each, where all teams play each other once. This determines the 4 best teams, who advance to the next day's single elimination bracket.
|
||||
1. **8.2.1** In case of a tie in groups, the team that won the match between the tied teams advances.
|
||||
|
||||
**Challengermode link:** https://www.challengermode.com/s/TipiLAN/tournaments/4b7d832b-7cf3-406a-8425-08ddd311ac8a
|
||||
@@ -1,141 +0,0 @@
|
||||
## 1. Üldist
|
||||
1. **1.1** Counter-Strike 2 (edaspidi CS2) turniir toimub 24.-26. Oktoober, 2025 Tallinna Tehnikaülikooli (TalTech) ruumides, Ehitajate tee 5, Tallinn.
|
||||
2. **1.2** Turniiri auhinnafondiks on 5750€, mis jaguneb järgnevalt:
|
||||
1. **1.2.1** TOP 3 auhinna found on 5000€:
|
||||
- **1.2.1.1** Esimese koha saanud võistkond - 500€ võistleja kohta
|
||||
- **1.2.1.2** Teise koha saanud võistkond - 300€ võistleja kohta
|
||||
- **1.2.1.3** Kolmanda koha saanud võistkond - 200€ võistleja kohta
|
||||
2. **1.2.2** LCQ auhinna fond on 750€:
|
||||
- **1.2.2.1** Esimese koha saanud võistkond - 100€ võistleja kohta
|
||||
- **1.2.2.2** Teise koha saanud võistkond - 50€ võistleja kohta
|
||||
3. **1.3** Võidusumma makstakse välja võistleja pangakontole.
|
||||
1. **1.3.1** Alaealise võistleja puhul makstakse võit vanema/eestkostja pangakontole.
|
||||
4. **1.4** Terve turniiri vältel tuleb igal osalejal lähtuda Eesti Vabariigi seadusest, TipiLAN kodukorrast ja ürituse reeglistikust.
|
||||
5. **1.5** Piletiostuga annab iga osaleja loa end pildistada, filmida ja kasutada kogu fotograafilist, audio- ja videomaterjali ürituse jäädvustamiseks ja turundamiseks.
|
||||
6. **1.6** CS2 põhiturniiri salvestatakse ning kantakse üle voogedastusplatvormidel Twitch ja YouTube.
|
||||
7. **1.7** Kõik turniiriga seotud suhtlused tiimiliikmete vahel (nt mängusisene chat, häälvestlused, Discordi vestlused jne) salvestatakse.
|
||||
8. **1.8** Ise mängu üle kandmise puhul peab otseülekande viivis (delay) olema vähemalt 5 minutit.
|
||||
9. **1.9** Korraldajatel on õigus kasutada osalejate isiklikku informatsiooni vaid ürituse läbiviimise raames.
|
||||
10. **1.10** Korraldustiimil on õigus reegleid vajadusel muuta ja redigeerida etteteatamata.
|
||||
11. **1.11** Korraldustiim on kõikide osalejate suhtes erapooletud.
|
||||
|
||||
## 2. CS2 põhiturniiri tiim ja tiimi koosseis
|
||||
1. **2.1** CS2 põhiturniiri tiim (edaspidi tiim) põhikoosseisu kuulub viis põhiliiget, kellest üks on tiimikapten;
|
||||
2. **2.2** Tiimikapten on tiimi esindaja, kes:
|
||||
1. **2.2.1** On kontaktisikuks korraldustiimile;
|
||||
2. **2.2.2** Registreerib tiimi turniirile;
|
||||
3. **2.2.3** Vastutab tiimi käitumise ja tegude eest;
|
||||
4. **2.2.4** Esindab tiimi hoiatuste, diskvalifikatsioonide, vaidlustuste ja timeout-ide korral.
|
||||
|
||||
3. **2.3** Igal tiimil võib olla üks varumängija, kes ei kuulu tiimi põhikoosseisu:
|
||||
1. **2.3.1** Varumängija peab soetama endale eraldi varumängija pileti;
|
||||
2. **2.3.2** Varumängija võib asendada ükskõik millist tiimi põhikoosseisu liiget turniiri toimumisel ajal;
|
||||
3. **2.3.3** Varumängijale kehtivad samad õigused ja nõuded, mis tiimi põhikoosseisule.
|
||||
4. **2.4** Tiimi põhikoosseisus on lubatud välja vahetada mängijaid enne tiimide registreerimiskuupäeva lõppemist:
|
||||
1. **2.4.1** Mängijate väljavahetamine toimub läbi Fienta;
|
||||
2. **2.4.2** Liikme välja vahetamisel jääb tiimil jätkuvalt õigus varumängijale;
|
||||
3. **2.4.3** Asendusmängijale kehtivad samad õigused ja nõuded, mis tiimi põhikoosseisule.
|
||||
5. **2.5** Tiimikapteni väljavahetamisel (nt varumängija või asendusmängija) otsustab tiim ise, kellele tiimikapteni õigused ja kohustused tiimisiseselt üle kanduvad;
|
||||
6. **2.6** Tiim peab 2 nädalat enne turniiri toimumist kinnitama oma osaluse, lõpliku tiimi põhikoosseisu ja tiimi nime tiimikaptenile saadetud emailis. Juhul, kui põhiliige langeb tiimi põhikoosseisut välja pärast seda, kui tiimi lõplik koosseis on kinnitatud, peab tiim rakendama oma varumängijat.
|
||||
7. **2.7** Kui tiim astub turniiril osalemisest tagasi enne registreerimistähtaega, makstakse osalemistasu tiimile tagasi.
|
||||
8. **2.8** Kõik tiimiliikmed (k.a. põhikoosseis, varumängija, asendusmängija(d)) võivad turniiril kuulda vaid ühte tiimi korraga ja esindada vaid iseennast (st keelatud on lasta kellelgi teisel enda eest mängida);
|
||||
9. **2.9** Kõik tiimiliikmed (k.a. põhikoosseis, varumängija, asendusmängija(d)) peavad turniirile registreerimise päevaks olema vähemalt 16-aastased;
|
||||
10. **2.10** Mitte ükski tiimiliige ei tohi olla Venemaa Föderatsiooni ega Valgevene Rahvavabariigi kodanik.
|
||||
11. **2.11** Tiimidel (k.a. põhikoosseis, varumängija, asendusmängija(d)) pole lubatud:
|
||||
1. **2.11.1** Treenerite kasutamine;
|
||||
2. **2.11.2** Tiimisponsorite kajastamine turniiril;
|
||||
3. **2.11.3** Mängida teise tiimi või tiimiliikme huvides;
|
||||
12. **2.12** Tiimi nimi:
|
||||
1. **2.12.1** Ei tohi olla solvav, vulgaarne, poliitiline või muud moodi maitsetu;
|
||||
2. **2.12.2** Ei tohi sisaldada emotikone ega muid sümboleid, mis pole tähemärgid;
|
||||
3. **2.12.3** Tuleb korraldustiimi poolsel nõudel ära muuta.
|
||||
|
||||
## 3. Varustus
|
||||
1. **3.1** Korraldustiimi poolt on turniiril osalejale tagatud internet, internetikaabel, pikendusjuhtmed ja istekoht lauaga.
|
||||
2. **3.2** Turniiril osaleja vastutab selle eest, et temal on osalemiseks muu vajalik varustus kaasas ja töötab.
|
||||
|
||||
## 4. Ajakava
|
||||
1. **4.1** Kõik tiimiliikmed peavad kohal olema tund aega enne ettenähtud turniiri algust.
|
||||
2. **4.2** Mänguvoorus võistlevad tiimid peavad 10 minutit enne vooru algust olema valmis oma ettenähtud kohtadel. Tiimikapteni vastutus on tagada, et tema tiim on õigel ajal õiges kohas ja valmis alustama.
|
||||
3. **4.3** Mänguvoorude algusajad on korraldustiimi välja pandud turniiri alguseks või eelmise mänguvooru lõpuks.
|
||||
4. **4.4** Kui mängijal esinevad tehnika või mänguga seotud tehnilised probleemid, peab ta sellest koheselt teavitama mänguvana või korraldustiimi.
|
||||
5. **4.5** Korraldustiimil on õigus teha ajakavas muudatusi.
|
||||
6. **4.6** Korraldustiimil on kohustus hoida kõiki osalejaid kursis tekkinud viivituste ja muudatusega.
|
||||
|
||||
## 5. Mängu versioon ja seaded
|
||||
1. **5.1** Terve turniiri jooksul kasutatakse CS2 kõige uuemat versiooni. Kui viimast saadaolevat versiooni peab korraldustiim mängitamatuks vigade või muude muutuste tõttu, võidakse kasutada vanemat versiooni (juhul, kui see on võimalik).
|
||||
2. **5.2** CS2 turniiril kasutatakse järgnevaid seadeid:
|
||||
1. **5.2.1** Parim 24st (mp_maxrounds 24)
|
||||
2. **5.2.2** Raundi aeg: 1 minut 55 sekundit (mp_roundtime 1.92)
|
||||
3. **5.2.3** Alustusraha: $ 800 (mp_startmoney 800)
|
||||
4. **5.2.4** Liikumise keelu aeg raundi alguses: 20 sekundit (mp_freezetime 20)
|
||||
5. **5.2.5** Aeg ostmiseks: 20 sekundit (mp_buytime 20)
|
||||
6. **5.2.6** Pommi taimer: 40 sekundit (mp_c4timer 40)
|
||||
7. **5.2.7** Lisaajal raunde: parim kuuest (6) (mp_overtime_maxrounds 6)
|
||||
8. **5.2.8** Lisaaja alustusraha: $12,500 (mp_overtime_startmoney 12500)
|
||||
9. **5.2.9** Raundi alguse viivis: 5 sekundit (mp_round_restart_delay 5)
|
||||
10. **5.2.10** Keelatud esemed: ei ole(mp_items_prohibited "")
|
||||
3. **5.3** Lisaaeg: juhul, kui pärast kõigi 24 raundi mängimist on viik, mängitakse lisaaega parim kuuest. Iga lisaaja alguses jäävad võistkonnad poolele, millelt nad eelmisel poolel mängisid - poolajal pooli vahetatakse. Võistkonnad jätkavad lisaaegu, kuni võitja on leitud.
|
||||
4. **5.4** Paus: iga tiimil on lubatud kutsuda esile paus (timeout) kolmkümmend (30) sekundit kuni kolm (3) korda regulatsiooniraundide ajal. Pausi saavad kutsuda osalejad kirjutades mängusisesesse chatti “!pause”. Võistlejatel on lubatud võtta kõik kolm pausi korraga. Vajadusel saab mänguvana pausi esile kutsuda ühepoolselt.
|
||||
5. **5.5** Tehniline paus: igal tiimil on vajadusel õigus kasutada tehnilist pausi. Pausi alustamiseks tuleb mängusisesesse chatti sisestada käsklus “.tech”. Tehnilist pausi tohib kasutada üksnes mõjuval põhjusel, tehniliste probleemide esinemisel, ning selle põhjusest tuleb korraldajaid viivitamatult teavitada Discordi kaudu pärast pausi alustamist.
|
||||
|
||||
## 6. Kaardivalik
|
||||
1. **6.1** Turniir toimub kahes etapis:
|
||||
1. **6.1.1** Swiss-süsteem: algfaasis mängitakse Bo1 formaadis. 16 parimat meeskonda pääsevad playoffidele.
|
||||
2. **6.1.2** Playoffid: mängitakse double elimination formaadis, kus winners' bracket mängud on Bo3 ja losers' bracket mängud on Bo1.
|
||||
2. **6.2** Mängitav kaart valitakse välja hetkel aktiivsete Valve’i kaardigrupi (Valve Active Duty Map Group) kaartidest.
|
||||
3. **6.3** Parim ühest (Bo1): mündiviske võitja otsustab, kas nad on võistkond A või võistkond B. Võistkond A alustab ning protsess on järgmine:
|
||||
1. **6.3.1** Võistkond A eemaldab kaks kaarti.
|
||||
2. **6.3.2** Võistkond B eemaldab kolm kaarti.
|
||||
3. **6.3.3** Võistkond A eemaldab ühe kaardi.
|
||||
4. **6.3.4** Järelejäänud kaarti mängitakse.
|
||||
4. **6.4** Parim kolmest (Bo3): mündiviske võitja otsustab, kas nad on võistkond A või võistkond B. Võistkond A alustab ning protsess näeb välja järgmine:
|
||||
1. **6.4.1** Võistkond A eemaldab ühe kaardi.
|
||||
2. **6.4.2** Võistkond B eemaldab ühe kaardi.
|
||||
3. **6.4.3** Võistkond A valib ühe kaardi.
|
||||
4. **6.4.4** Võistkond B valib ühe kaardi.
|
||||
5. **6.4.5** Võistkond B eemaldab ühe kaardi.
|
||||
6. **6.4.6** Võistkond A eemaldab ühe kaardi.
|
||||
7. **6.4.7** Järelejäänud kaart on vajadusel otsustav.
|
||||
|
||||
## 7. CS2 turniiril keelatud tegevused
|
||||
1. **7.1** Igasugune sohitegemine, sealhulgas meetodid mis siin ei ole mainitud, on keelatud.
|
||||
2. **7.2** Skriptide kasutamine on keelatud (v.a. relvade/granaatide ostmine, hüppeviskamine).
|
||||
3. **7.3** Liikumine läbi seinte, põrandate ja katuste, k.a. taevas jalutamine (sky-walking) on keelatud.
|
||||
4. **7.4** „Piksel-jalutamine” (Pixel walking) ehk seismine, kükitamine, kõndimine ja muud tegevused nähtamatutel kaardipiiridel on keelatud.
|
||||
5. **7.5** Pomme tuleb asetada nii, et neid saaks desarmeerida. See ei hõlma olukordi, kus pommi desarmeerimiseks on vaja mitut mängijat.
|
||||
6. **7.6** Mängijatel ei ole lubatud armeeritud pommi asetada kohta, kus seda ei saa desarmeerida, kohta, kus see ei puutu kokku tahke objektiga või kus see ei tee tavalist „piiksuvat” heli.
|
||||
7. **7.7** Mängijatel ei ole lubatud panna esemetele nimesid (nametags), mis rikuvad TipiLAN kodukorras väljatoodut.
|
||||
8. **7.8** Kohandatud mängufailid/andmed/draiverid ei ole lubatud.
|
||||
9. **7.9** Mängukarakteri mudelite (agent skins) kasutamine ei ole lubatud.
|
||||
10. **7.10** Mängusiseste vigade ära kasutamine on keelatud.
|
||||
11. **7.11** Igasugune tulemuste kokkuleppimine, mõjutamine, pettus ja manipuleerimine on rangelt keelatud ning tähendab kohest tiimi diskvalifitseerimist. Korraldajatel on õigus kahtluse korral teavitada Eesti Vabariigi korrakaitseorganeid.
|
||||
|
||||
## 8. Karistused
|
||||
1. **8.1** Karistatav on mängusiseste ja mänguväliste reeglite (vt punkti 7) ning kodukorra rikkumine (vaata TipiLAN kodukord).
|
||||
2. **8.2** Reegleid rikkunud tiimiliikmele tehakse esmalt esimene verbaalne hoiatus. Teise/korduva rikkumisel järel tehakse tiimiliikmele teine verbaalne hoiatus. Kolmandal korral saab tiimiliige turniiril diskvalifikatsiooni. Tiimil on õigus rakendada varumängijat.
|
||||
3. **8.3** Tiimiliige, kes ei ilmu turniiriks ega mänguvooruks kohale või lahkub turniiri ajal mõjuva põhjuseta, saab turniirilt diskvalifikatsiooni. Tiimil on õigus rakendada varumängijat.
|
||||
4. **8.4** Tiimiliige, kes pole 10 minutit enne oma mänguvooru algust kohal ja valmis (no-show olukord), saab turniirilt diskvalifikatsiooni, v.a. juhul, kui tiimiliige hilineb mõjuvatel põhjustel ja on sellest teavitanud korraldustiimi. Tiimil on õigus rakendada varumängijat.
|
||||
5. **8.5** Kui korraldustiim tuvastab, et tiimiliige on eksinud punktis 7 väljatoodu osas, siis diskvalifitseeritakse kogu tiimi turniirilt koheselt. Reegleid rikkunud tiimiliikmele antakse TipiLAN turniiridelt igavene mängukeeld.
|
||||
6. **8.6** Kui mängukorduse läbivaatamisel tekib korraldustiimil põhjendatud kahtlus, et tiimiliige on eksinud punktis 7 väljatoodu osas, siis diskvalifitseeritakse kogu tiimi turniirilt koheselt. Reegleid rikkunud tiimiliikmele antakse TipiLAN turniiridelt igavene mängukeeld.
|
||||
7. **8.7** Tiimil endal on õigus astuda turniiril osalemisest tagasi ehk anda iseendale diskvalifikatsioon.
|
||||
8. **8.8** Tiimi diskvalifitseerimise korral võidab vastastiim automaatselt käesoleva mänguvooru.
|
||||
9. **8.9** Ainult tiimikapten saab tiimiliikme/tiimi diskvalifitseerimist vaidlustada. Vaidlustus tuleb korraldustiimile esitada 15 minuti jooksul alates diskvalifikatsiooni teavitamisest tiimile.
|
||||
1. **8.9.1** Korraldustiimil on aega kuni 25 minutit, et teha otsus vaidlustuse osas. Sellel ajal on mänguvoor pausil ning tiimid ei tohi oma kohtadelt lahkuda.
|
||||
10. **8.10** Tiimidel on õigus esitada protest olukorras, kus esineb probleem, mis võib mõjutada või mõjutab mänguvooru või tiimi:
|
||||
1. **8.10.1** Protesti on õigus esitada korraldustiimile 5 minuti jooksul alates probleemi avastamisest.
|
||||
2. **8.10.2** Korraldustiimil on aega kuni 25 minutit, et teha otsus protesti osas. Sellel ajal on mänguvoor pausil ning tiimid ei tohi oma kohtadelt lahkuda.
|
||||
11. **8.11** Mänguvana teavitab eksimusest, selle sisust ja tagajärjest reegleid rikkunud tiimiliiget, tema tiimi ja vastastiimi.
|
||||
12. **8.12** Korraldustiimil on õigus panna mänguvoor pausile ja lõpetada paus ükskõik millise hetkel vastavalt vajadusele.
|
||||
13. **8.13** Korraldustiimil on kohustus kõikidest väljalangemistest ja -arvamistest ning edasistest muudatustest avalikult teada anda.
|
||||
|
||||
## 9. Kontakt
|
||||
Igasuguste üritust puudutavate küsimuste, probleemide, murede jne korral võtta ühendust TipiLAN korraldustiimiga:
|
||||
(kontaktid)
|
||||
|
||||
## 10. Viited
|
||||
- https://pro.eslgaming.com/csgo/proleague/rules/
|
||||
- https://github.com/ValveSoftware/counter-strike_rules_and_regs/blob/main/tournament-operation-requirements.md
|
||||
- https://www.hltv.org/events/8037/iem-dallas-2025
|
||||
- https://www.hltv.org
|
||||
- https://liquipedia.net/counterstrike/Portal:Maps/CS2
|
||||
@@ -1,30 +0,0 @@
|
||||
Üritusel osalemise kodukord kehtib kõigile, nii külastajatele kui võistlejatele. Kodukorra rikkumisel jätab TipiLAN endale õiguse osaleja ürituselt eemaldada ning vajadusel teavitada politseid. Alaealise osaleja puhul teavitame raskema kodukorra eiramise puhul tema vanemaid või eestkostjaid.
|
||||
|
||||
# Osaleja meelespea
|
||||
|
||||
1. Tulles vaheta oma pilet käepaela vastu.
|
||||
2. Osaleja peab olema vähemalt 16-aastane. Pileti kontrollija võib küsida Sinult dokumenti.
|
||||
3. Üritust pildistatakse ning filmitakse ja ürituse sisu kajastatakse erinevates meediakanalites.
|
||||
4. Kui Sul on ette nähtud TipiLANi poolne majutus, anna sellest teada pileti käepaela vastu vahetamisel.
|
||||
5. Kui tuled oma arvutiga, juhendatakse Sind käepaela saades, kuhu saad selle üles panna.
|
||||
|
||||
# Ürituse kodukord
|
||||
|
||||
1. Osaleja kohustub käituma viisakalt ning väärikalt ja austama teisi üritusel osalejaid.
|
||||
2. TipiLAN ei tolereeri:
|
||||
2.1. Vihakõnet rahvusliku, rassilise, soolise, seksuaalse või religioosse kuuluvuse, puude, välimuse või vanuse kohta; ahistamist, ähvardavat, solvavat või agressiivset käitumist, sellele õhutamist või selle pooldamist
|
||||
2.2. See kehtib nii ürituse alal (IRL) kui ka üritusega seotud online-keskkondades.
|
||||
3. Osaleja kohustub käituma ürituse hoone, inventari ja sisustuse suhtes heaperemehelikult. Keelatud on lõhkuda, määrida või viia mujale esemeid, mis ei kuulu osalejale.
|
||||
3.1. Kui osalejal on ette nähtud korraldajapoolne majutus, siis majutusalal on osalejal kohustus olla vaikselt ning lubada kaaslastel puhata.
|
||||
3.2. Majutusalale ei või kaasa kutsuda isikuid, kellel ei ole seal majutust ette nähtud.
|
||||
4. TipiLAN ei vastuta osaleja isikliku vara eest.
|
||||
4.1. Korraldajapoolne majutusala on lukustatav ning kõrvalisi isikuid sinna ei lubata, kuid sellest sõltumata tasub oma väärisesemetel silma peal hoida.
|
||||
4.2. Kui on tekkinud kahtlus, et on toimunud vargus, tuleb sellest koheselt teavitada korraldajat.
|
||||
4.3. Kaotatud asjade leidmisel palume anda need korraldajale või viia need *lost & found’i* (Merchilauda).
|
||||
5. Ürituse alal on keelatud suitsetada ning kasutada vape’i. Selleks on õues ette nähtud suitsetamise kohad.
|
||||
6. Üritusele ei tohi kaasa võtta illegaalseid aineid või ravimeid, terariistu, tulirelvi, lõhke- või süüteained ning muid esemeid, mis võivad osalejatele või teistele viga teha.
|
||||
7. Alaealisel osalejal on keelatud tarbida alkoholi või kasutada nikotiini sisaldavaid tooteid.
|
||||
7.1. Olles baarist alkoholi ostmas, on osalejal baaritöötaja nõudmisel kohustus näidata isikuttõendavat dokumenti.
|
||||
8. Osaleja kohustub käituma alkoholi suhtes vastutustundlikult.
|
||||
9. Keelatud on igasugune hasartmäng nii raha kui muude hüvede peale.
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
## 1. Üldist
|
||||
|
||||
1. **1.1** League of Legends (edaspidi LoL) turniir toimub kahepäevase üritusena **24.–25. oktoober 2025** Tallinna Tehnikaülikooli (TalTech) ruumides, Ehitajate tee 5, Tallinn.
|
||||
2. **1.2** Turniiri auhinnafondiks on **3500€**, mis jaguneb järgnevalt:
|
||||
1. **1.2.1** Esimese koha saanud võistkond – **300€** võistleja kohta
|
||||
2. **1.2.2** Teise koha saanud võistkond – **200€** võistleja kohta
|
||||
3. **1.2.3** Kolmanda koha saanud võistkond – **100€** võistleja kohta
|
||||
3. **1.3** Võidusumma makstakse välja võistleja pangakontole
|
||||
1. **1.3.1** Alaealise võistleja puhul makstakse võit vanema/eestkostja pangakontole
|
||||
|
||||
## 2. Võistkonnad ja võistlejad
|
||||
|
||||
1. **2.1** Võistkonnas peab olema:
|
||||
1. **2.1.1** Viis liiget (iga liige edaspidi eraldi kui *Võistleja*)
|
||||
2. **2.1.2** Liikmetest üks on võistkonna Kapten, kes on ühtlasi kogu meeskonna eestkõnelejaks
|
||||
3. **2.1.3** Kõik liikmed peavad olema võistkonna registreerumise hetkel vähemalt 16 aastat vanad
|
||||
4. **2.1.4** Võistleja ei või olla Venemaa Föderatsiooni ega Valgevene Rahvavabariigi kodanik
|
||||
5. **2.1.5** Võistkonnal pole lubatud kasutada turniiri jooksul treenerit
|
||||
6. **2.1.6** Lubatud välja vahetada üks võistkonna liige, kes peab samuti olema registreeritud ja füüsiliselt kohal
|
||||
2. **2.2** Võistleja peab esitama enda kohta ainult tõest informatsiooni ning valmis Korraldajale tõendama enda isikut.
|
||||
3. **2.3** Võistkonna nimi ja logo ning Võistleja arvutimängu alias ja avatar peab olema sünnis, sh ei tohi olla kohatud, sisaldada roppusi, vulgaarsusi, poliitilisi või religioosseid sõnumeid ega sümboleid või viiteid alkoholile, narkootikumidele ning muudele mõnuainetele.
|
||||
4. **2.4** Võistleja esindab terve Turniiri vältel ainult iseennast (st. enda asemel ei või lasta kellelgi teisel võistelda).
|
||||
5. **2.5** Kõik alla 18-aastased Võistlejad on valmis Korraldajale esitama eestkostja nõusoleku turniiril osalemiseks.
|
||||
6. **2.6** Võistleja peab olema kogu turniiri vältel viisakas ning austama kaasvõistlejaid, korraldajaid ning külastajaid, TipiLAN ei tolereeri vihakõne rahvusliku, rassilise, soolise, seksuaalse või religioosse kuuluvuse, puude, välimuse või vanuse kohta; ahistamist, ähvardavat, solvavat või agressiivset käitumist, sellele õhutamist või selle pooldamist. Keelatud on avalikustada teise osaleja isikuandmeid (doxing) või selle tegemisega ähvardada. Samuti on keelatud striimi spämmimine või ülekoormamine (hijacking) ja sellele õhutamine. Sel juhul võib korraldaja eemaldada võistleja turniirilt.
|
||||
7. **2.7** Võistkond peab olema registreeritud nii TipiLAN lehel kui ka **[challengermode](https://www.challengermode.com/s/TipiLAN/tournaments/4b7d832b-7cf3-406a-8425-08ddd311ac8a)** turniirilehel.
|
||||
|
||||
## 3. Mängule eelnev
|
||||
|
||||
1. **3.1** Turniiril osalemine, matchid ning turniiripuu toimib kõik läbi **[challengermode](https://www.challengermode.com/s/TipiLAN/tournaments/4b7d832b-7cf3-406a-8425-08ddd311ac8a)** keskkonna:
|
||||
1. **3.1.1** Turniirile peab olema registreeritud kogu meeskond, kaasa arvatud varumängija
|
||||
2. **3.1.2** Mängijatel peab challengermodes olema linkitud kõige kõrgema rankiga kasutaja (ka teistest serveritest) ja kasutaja, millega osaletakse turniirimängudes (Challengermode lubab kahte regiooni korraga linkida)
|
||||
3. **3.1.3** Turniir toimub **EU West** serveris
|
||||
4. **3.1.4** Mängijad ei tohi kasutada ühtegi teist kasutajat peale challengermodes linkitud kasutajate
|
||||
2. **3.2** Challengermode keskkonnas on matchid automaatsed. Uue matchi puhul on valmisolekuks aega **10 minutit**. Seejärel genereeritud lobby'sse jõudmiseks on uuesti aega **10 minutit**.
|
||||
3. **3.3** Draft saab alata kui mõlemad pooled on andnud enda valmisolekust märku:
|
||||
1. **3.3.1** Placeholderid ei ole lubatud. Kui champion on draftis lukustatud, peab seda ka mängima
|
||||
2. **3.3.2** Enne drafti peavad mängijad olema rollidele vastavas järjekorras: **Top – Jungle – Mid – Bot – Support**
|
||||
3. **3.3.3** Lubatud on Drafter.lol kasutamine väliseks draftimiseks, kuid seda ainult mõlema poole nõusolekul
|
||||
4. **3.3.4** Sihilikult viivitamine ei ole lubatud
|
||||
4. **3.4** Matchi lobbysse tohivad lisaks mängijatele liituda ainult ametlikuks streaerid ja kohtunikud
|
||||
|
||||
## 4. Mängusisesed protseduurid
|
||||
|
||||
1. **4.1** Mäng on ametlikult alanud (game of record (edaspidi GOR)) kui kõik 10 mängijat on kaardil ning mäng on jõudnud esimese reaalse interaktsioonini (vt. allpool). Hetkel kui mäng on jõudnud GOR staatuseni, ei või seda uuesti alustada. Mängu skoori hakatakse sellest hetkest ametlikult jälgima. Peale GOR staatuseni jõudmist on võimalik mängu restartida vaid juhtudel, kui käesoleva mängu lõpuni viimine ei osutu mõjuval põhjusel võimalikuks. GOR'i tingimused on järgnevad:
|
||||
1. **4.1.1** Kummalgi tiimil õnnestub rünnak või võime (ability) kasutamine käsilaste, jungle creep'ide, ehitiste või vastaste vastu
|
||||
2. **4.1.2** Vastased näevad teineteist (erandina - Clairvoyance ei taga GOR'i)
|
||||
3. **4.1.3** Sisenetakse vastase territooriumile
|
||||
4. **4.1.4** Mäng on kestnud 2 minutit
|
||||
2. **4.2** Mängu seiskamine:
|
||||
1. **4.2.1** Mängu pausile panemise ajal ei ole mängijatel lubatud lahkuda matši alalt, v.a juhul kui see on ametlikult autoriseeritud
|
||||
2. **4.2.2** Korraldajad võivad mängu pausile panna vastavalt vajadusele
|
||||
3. **4.2.3** Kummalgi tiimil on õigus matši jooksul võtta kokku kuni **15 minutit pausi** mõjuval põhjusel (disconnect, tark- või riistvara probleemid, mängija endaga juhtub midagi)
|
||||
3. **4.3** Mängu võib uuesti käima panna ainult mõlema poole nõusolekul või kohtuniku loal
|
||||
4. **4.4** Kui tekib mängu ausal läbiviimisel takistus (gamebreaking bug, netiühendus, etc.), määrab kohtunik uued juhised mängu läbiviimiseks
|
||||
|
||||
## 5. Match'i lõpetamine
|
||||
|
||||
1. **5.1** Match'i võitja on tiim, kellel on kõige rohkem mänge võidetud (mitmemängulise match'i puhul)
|
||||
1. **5.1.1** Korraldaja esitab tiimide võitude ja kaotuste seisud kõigile osalejatele kättesaadaval viisil ning teavitab kõiki mängijaid sellest, kus on neid võimalik näha
|
||||
2. **5.1.2** Peale igat match'i uuendatakse challengermode keskkonnas turniiripuud ning esimesel võimalusel määratakse sealtkaudu uued match'id
|
||||
|
||||
## 6. Turniirilt välja langemine
|
||||
|
||||
1. **6.1** Tiim võib igal ajal otsustada lõpetada turniiril osalemise andes sellest teada kohtunikule ja/või korraldajale.
|
||||
2. **6.2** Kuni välja kukkumiseni teenitud karistused jäävad kehtima turniiri lõpuni.
|
||||
3. **6.3** Kui tiim ei ilmu kohale või pole kokku lepitud algusajaks valmis, võib Korraldaja arvata tiimi turniirilt välja, et tagada graafikust kinni pidamine.
|
||||
4. **6.4** Tiimi registreerunute nimekirja ei saa muuta turniiri vältel, isegi kui mängija on lahkunud.
|
||||
1. **6.4.1** Kui liikme lahkumise tõttu langeb tiimis osalejate arv alla mängimiseks vajaliku, peab Korraldaja tiimi turniirilt välja arvama.
|
||||
5. **6.5** Kui tiim soovib välja langeda või tiim arvatakse turniirilt välja match'i toimumise ajal, peab tiim andma loobumisvõidu.
|
||||
6. **6.6** Kõik välja langemised ja välja arvamised tuleb teha koheselt avalikult teatavaks.
|
||||
|
||||
## 7. Karistused
|
||||
|
||||
1. **7.1** Kohtunikud määravad karistusi järgides selles dokumendis toodud juhiseid.
|
||||
2. **7.2** Karistusi võivad määrata ainult kohtunikud.
|
||||
3. **7.3** Kohtunik teavitab nii eksimuse sisu kui ka määratud karistuse nii reeglite vastu eksinud mängijale, tema tiimile kui ka vastasvõistkonnale. Vajaduse korral võib lisada ka muud infot.
|
||||
4. **7.4** Kohtunik peab enne kinnitama eksimuse ning alles siis määrama karistuse. Karistusele ei tohi "välja mõelda" sobivat eksimust.
|
||||
5. **7.5** Kohtunik peab olema erapooletu, tiimi oskuste tase ei tohi olla määravaks eksimuste ja karistuste jälgimisel.
|
||||
6. **7.6** Karistusi võib määrata nii kogu tiimile kui ka ühele tiimiliikmele.
|
||||
7. **7.7** Karistuse sisu võib olla kehtiv nii ühele mängule kui ka tervele turniiril osalemise ajale.
|
||||
8. **7.8** Tiim ei saa protesteerida vastastiimile määratud karistuse vastu.
|
||||
9. **7.9** Karistused võivad olla järgnevad:
|
||||
1. **7.9.1 HOIATUS:** hoiatus on kirja pandud märguanne mängijale või tiimile väikese eksimuse eest.
|
||||
2. **7.9.2 BAN'i KAOTUS:** Tiim ei või karistusele järgneval mängul ban'ida kindel arv tegelasi. Sel juhul kohtunik jälgib, et tiim ei valiks karistusena määratud arvu ban'e ning laseks selle asemel taimeril nulli joosta.
|
||||
3. **7.9.3 MÄNGU KAOTUS:** Tiim saab automaatse kaotuse ühel mängul.
|
||||
4. **7.9.4 MATCH'I KAOTUS:** Tiim saab automaatse match'i kaotuse.
|
||||
5. **7.9.5 DISKVALIFITSEERIMINE:** Diskvalifikatsioon kehtib tervele tiimile. Sellel juhul loobub tiim kõigist võitudest. Kui disvalifikatsioon on saadud eskaleeruvate eksimuste tulemusel, saab tiim selle osa võitudest, mis neil oli selleks hetkeks välja teenitud.
|
||||
- Mõningatel juhtudel on kohtunikul lubatud diskvalifitseerida ainult üks mängija tiimi asemel. See on sel juhul, kui mängija eksimus ei mõjuta mingil viisil vastasmeeskonda ning on tehtud kaasamata kedagi ka oma tiimist. Üldiselt on see võimalik juhul kui mängija eksimus kuulub kategooriasse Mittesobilik käitumine - Raske eksimus. Sel juhul võib ülejäänud tiim turniiril jätkata varumängija olemasolul. Vastasel korral peab ka kogu tiim turniirilt välja langema.
|
||||
10. **7.10** Karistuse raskusastmed on järgnevad:
|
||||
Hoiatus - hoiatus - ban'i valimise õiguse kaotus - mängu kaotus - match'i kaotus - diskvalifikatsioon
|
||||
11. **7.11** Turniiri eksimused jagunevad järgmiselt:
|
||||
1. **7.11.1 VÄLISE ABI KASUTAMINE:** see eksimus läheb kirja, kui tiim suhtleb mängu ajal ükskõik kelle muuga peale omaenda tiimi ning selle tagajärjel, kohtuniku otsustusel, saab mängus eelise. Eksimuse puhul eeldatakse, et tegu ei olnud tahtliku kavatsusega sohki teha. Tahtlikult ebaõiglase eelise otsimine läheb punkti Mittesobilik käitumine - Sohk alla. Karistuseks on hoiatus.
|
||||
2. **7.11.2 JUHISTE EIRAMINE:** Igal mängijal on kohustus järgida Korraldaja ja kohtunike juhiseid. Nende eiramine võib endaga kaasa tuua viivitusi ning vaidlusi. Karistuseks on esimese valiku tegemise kaotus.
|
||||
12. **7.12** Mittesobilik käitumine.
|
||||
Mittesobilik käitumine on turniiri käiku häiriv ning võib negatiivselt mõjutada turvalisust, võistlushimu, mängurõõmu või turniiri ausameelsust ning terviklikkust. Mittesobilik käitumine ei ole sama, mis konkurentsihimuline käitumine.
|
||||
Mittesobiliku käitumise eksimused jagunevad:
|
||||
1. **7.12.1 KERGE EKSIMUS:** siia kuulub käitumine, mis on ebameeldiv, ebaeetiline või häiriv, näiteks liigne ropendamine; nõudmine, et vastane saaks karistuse peale seda, kui kohtunik on teinud oma otsuse; lõugamine; prügi maha loopimine jne.
|
||||
Kerge eksimuse karistuseks on hoiatus.
|
||||
2. **7.12.2 KESKMINE EKSIMUS:** selle alla kuulub kolme erinevat tüüpi eksimusi
|
||||
- eirab kohtuniku või Korraldaja juhiseid, mis on mõeldud spetsiaalselt ühele tiimile või ühele mängijale
|
||||
- kasutab avalikult vihakõne kellegi suunas
|
||||
- on agressiivne või vägivaldne, kuid see ei ole suunatud teise inimese vastu
|
||||
Keskmise eksimuse karistuseks on mängu kaotus.
|
||||
3. **7.12.3 RASKE EKSIMUS:** siia alla kuulub käitumine, mis on selgelt vastuolus turniiril käitumise reeglite ning heade tavadega, näiteks tahtlikult turniiri vahendite lõhkumine või ruumi määrimine või lõhkumine. Raske eksimuse karistuseks on diskvalifikatsioon, turniiri toimumiskohast eemaldamine või ekstreemsematel juhtudel informeerib Korraldaja politseid.
|
||||
4. **7.12.4 KOKKUMÄNG:** kokkumänguks loetakse kahe tiimi vahelist kokkulepet, et ebaausal teel teiste tiimide vastu mängida ning püüda mõjutada turniiri tulemusi.
|
||||
Kokkumängu karistuseks on mõlema tiimi diskvalifitseerimine.
|
||||
5. **7.12.5 ALTKÄEMAKS JA PANUSTAMINE:** tiimidel on keelatud mingi meelehea (ei pea olema ainult rahaline) nimel loobuda turniirist või püüda muuta match'ide tulemusi. Samuti on keelatud pakkuda kohtunikule stiimulit mängu tulemuse mõjutamiseks. Keelatud on ka mängude tulemuste peale panuseid teha.
|
||||
Altkäemaksu ja panustamise karistuseks on diskvalifitseerimine.
|
||||
6. **7.12.6 AGRESSIIVNE KÄITUMINE:** siia hulka kuuluvad kõik inimeste vastu suunatud agressiooni ilmingud, kaasa arvatud ähvardamine ning ilmselgelt reaalne vägivald.
|
||||
Karistuseks on diskvalifitseerimine ja toimumiskohast eemaldamine, ekstreemsematel juhtudel informeerib Korraldaja politseid.
|
||||
7. **7.12.7 VARGUS:** kuigi igal osalejal on kohustus oma varal silma peal hoida, on siiski eeldus, et osalejate käitumine on kooskõlas heade tavadega.
|
||||
Varguse eest on karistuseks diskvalifitseerimine ja toimumiskohast eemaldamine. Kui situatsioon ei leia lahendust koha peal, informeerib Korraldaja politseid.
|
||||
8. **7.12.8 ALKOHOL JA JOOVE:** Alkoholi tarbimine ürituse raames on keelatud. Liigse joobe korral on Korraldajal õigus osaleja toimumiskohast eemaldada.
|
||||
- kui joobes osaleja on alaealine, informeeritakse sellest tema vanemaid ning politseid.
|
||||
9. **7.12.9 SOHK:** selle alla kuulub mängijapoolne teadlik tegevus, et saavutada mängus eelis. Sohitegemine ei pea olema edukas selleks, et kaasa tuua karistust.
|
||||
Sohi alla kuuluvad tegevused on näiteks:
|
||||
- üritamine ise enda mängu spectator mode's näha või info saamine kelleltki, kes saab vaadata mängu spectator mode's
|
||||
- igasugune mängu enda modifitseerimise üritamine või lisatarkvara kasutamine, mis ei ole tavapäraselt mänguga kaasas, sh. mängusisese zoomi muutmine, UI modifikatsioonide kasutamine, mis muudab löögiraadiust või muudab torni laskeraadiuse nähtavaks, spawn taimerite kasutamine jne. Siia alla ei kuulu VOIP programmide kasutamine.
|
||||
- teise mängijana või vale nime all esinemine, mängimine kasutades teise mängija summoner nime või konto jagamine
|
||||
- tahtlikult varustuse rikkumine või moonutada üritamine, et tekitada viivitusi, saada pause või mõnel muul moel mõjutada mängu käiku
|
||||
- mängusiseste vigade tahtlik ära kasutamine, et saada eelist, st erinevate glitch'ide enda kasuks kasutamine.
|
||||
Sohki tegemise eest on karistuseks diskvalifikatsioon.
|
||||
|
||||
## 8. Turniiri formaat
|
||||
|
||||
1. **8.1** Turniir toimub Fearless drafti põhimõtetel. See tähendab, et seeria jooksul pickitud champione ei saa pickida järgmistes mängudes kuni seeria lõpuni.
|
||||
2. **8.2** Turniir toimub Round Robin + Single Elimination formaadis
|
||||
See tähendab, et esimene round on 2 6-liimelist gruppi, kus kõik tiimid mängivad üksteisega korra läbi. Sellega selgitatakse 4 parimat, kes lähevad edasi järgmise päeva single elimination bracketisse.
|
||||
1. **8.2.1** Gruppides tekkinud viigi korral pääseb edasi võistkond, kes viigistunud tiimide vahelise matši võitis.
|
||||
@@ -1,112 +0,0 @@
|
||||
export type ScheduleItem = {
|
||||
time?: string; // Aeg on ajutine praegu kuna pole 100% kindlalt paigas
|
||||
titleKey: string;
|
||||
locationKey: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export const scheduleData: Record<string, ScheduleItem[]> = {
|
||||
oct24: [
|
||||
{
|
||||
titleKey: "schedule.events.doorsOpen",
|
||||
locationKey: "schedule.locations.registrationSetup",
|
||||
time: "17:00",
|
||||
},
|
||||
|
||||
{
|
||||
titleKey: "schedule.events.miniTournaments",
|
||||
locationKey: "schedule.locations.studentHouse",
|
||||
time: "18:00",
|
||||
},
|
||||
{
|
||||
titleKey: "schedule.events.smashBrosUltimate",
|
||||
locationKey: "schedule.locations.studentHouse",
|
||||
time: "18:30",
|
||||
},
|
||||
{
|
||||
titleKey: "schedule.events.cs2LolTournaments",
|
||||
locationKey: "schedule.locations.auditorium",
|
||||
time: "20:00",
|
||||
},
|
||||
{
|
||||
titleKey: "schedule.events.doorsClose",
|
||||
locationKey: "schedule.locations.auditoriumAndStudentHouse",
|
||||
time: "*01:00",
|
||||
},
|
||||
],
|
||||
oct25: [
|
||||
{
|
||||
titleKey: "schedule.events.doorsOpenSimple",
|
||||
locationKey: "schedule.locations.auditorium",
|
||||
time: "09:30",
|
||||
},
|
||||
{
|
||||
titleKey: "schedule.events.cs2Continue",
|
||||
locationKey: "schedule.locations.auditorium",
|
||||
time: "11:00",
|
||||
},
|
||||
{
|
||||
titleKey: "schedule.events.lolContinue",
|
||||
locationKey: "schedule.locations.auditorium",
|
||||
time: "12:00",
|
||||
},
|
||||
{
|
||||
titleKey: "schedule.events.expoOpens",
|
||||
locationKey: "schedule.locations.entranceHall",
|
||||
time: "12:00",
|
||||
},
|
||||
{
|
||||
titleKey: "schedule.events.2xkoTournament",
|
||||
locationKey: "schedule.locations.studentHouse",
|
||||
time: "13:30",
|
||||
},
|
||||
{
|
||||
titleKey: "schedule.events.lolFinale",
|
||||
locationKey: "schedule.locations.auditorium",
|
||||
time: "19:30",
|
||||
},
|
||||
{
|
||||
titleKey: "schedule.events.cs2lbsemifinals",
|
||||
locationKey: "schedule.locations.auditorium",
|
||||
time: "19:30",
|
||||
},
|
||||
{
|
||||
titleKey: "schedule.events.cs2lbfinals",
|
||||
locationKey: "schedule.locations.auditorium",
|
||||
time: "21:30",
|
||||
},
|
||||
{
|
||||
titleKey: "schedule.events.granTurismoFinale",
|
||||
locationKey: "schedule.locations.studentHouse",
|
||||
time: "20:00",
|
||||
},
|
||||
{
|
||||
titleKey: "schedule.events.doorsClose",
|
||||
locationKey: "schedule.locations.auditoriumAndStudentHouse",
|
||||
time: "*01:00",
|
||||
},
|
||||
],
|
||||
oct26: [
|
||||
{
|
||||
titleKey: "schedule.events.cs2semifinals",
|
||||
locationKey: "schedule.locations.auditorium",
|
||||
time: "11:00",
|
||||
},
|
||||
{
|
||||
titleKey: "schedule.events.Cs2finals",
|
||||
locationKey: "schedule.locations.auditorium",
|
||||
time: "15:30",
|
||||
},
|
||||
{
|
||||
|
||||
titleKey: "schedule.events.AwardsCeremony",
|
||||
locationKey: "schedule.locations.auditorium",
|
||||
time: "18:00",
|
||||
},
|
||||
{
|
||||
titleKey: "schedule.events.doorsClose",
|
||||
locationKey: "schedule.locations.auditorium",
|
||||
time: "19:00",
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
import { drizzle } from "drizzle-orm/bun-sqlite";
|
||||
import { Database } from "bun:sqlite";
|
||||
import * as schema from "./schema/schema";
|
||||
|
||||
const sqlite = new Database("data/tipilan.db");
|
||||
export const db = drizzle(sqlite, { schema });
|
||||
@@ -1,14 +0,0 @@
|
||||
// Export all types
|
||||
export * from "./types";
|
||||
|
||||
// Export all tables
|
||||
export * from "./users";
|
||||
export * from "./teams";
|
||||
export * from "./members";
|
||||
export * from "./tournaments";
|
||||
|
||||
// Export session table
|
||||
// export * from ./session";
|
||||
|
||||
// Export all relations
|
||||
export * from "./relations";
|
||||
@@ -1,31 +0,0 @@
|
||||
import { sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { users } from "./users";
|
||||
import { teams } from "./teams";
|
||||
import { RoleEnum } from "./types";
|
||||
|
||||
// Member table (join table for User and Team with role)
|
||||
export const members = sqliteTable(
|
||||
"member",
|
||||
{
|
||||
id: text("id")
|
||||
.primaryKey()
|
||||
.notNull()
|
||||
.$defaultFn(() => createId()),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
teamId: text("team_id").references(() => teams.id, { onDelete: "cascade" }),
|
||||
role: text("role", {
|
||||
enum: Object.values(RoleEnum) as [string, ...string[]],
|
||||
}).notNull(),
|
||||
},
|
||||
(table) => {
|
||||
return {
|
||||
userTeamUnique: uniqueIndex("user_team_unique").on(
|
||||
table.userId,
|
||||
table.teamId,
|
||||
),
|
||||
};
|
||||
},
|
||||
);
|
||||
@@ -1,42 +0,0 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { users } from "./users";
|
||||
import { teams } from "./teams";
|
||||
import { members } from "./members";
|
||||
import { tournaments, tournamentTeams } from "./tournaments";
|
||||
|
||||
// User relations
|
||||
export const usersRelations = relations(users, ({ many }) => ({
|
||||
members: many(members),
|
||||
}));
|
||||
|
||||
// Team relations
|
||||
export const teamsRelations = relations(teams, ({ many }) => ({
|
||||
members: many(members),
|
||||
tournamentTeams: many(tournamentTeams),
|
||||
}));
|
||||
|
||||
// Member relations
|
||||
export const membersRelations = relations(members, ({ one }) => ({
|
||||
user: one(users, { fields: [members.userId], references: [users.id] }),
|
||||
team: one(teams, { fields: [members.teamId], references: [teams.id] }),
|
||||
}));
|
||||
|
||||
// Tournament relations
|
||||
export const tournamentsRelations = relations(tournaments, ({ many }) => ({
|
||||
tournamentTeams: many(tournamentTeams),
|
||||
}));
|
||||
|
||||
// Tournament team relations
|
||||
export const tournamentTeamsRelations = relations(
|
||||
tournamentTeams,
|
||||
({ one }) => ({
|
||||
tournament: one(tournaments, {
|
||||
fields: [tournamentTeams.tournamentId],
|
||||
references: [tournaments.id],
|
||||
}),
|
||||
team: one(teams, {
|
||||
fields: [tournamentTeams.teamId],
|
||||
references: [teams.id],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
@@ -1,2 +0,0 @@
|
||||
// Re-export all schema components from modular files
|
||||
export * from "./index";
|
||||
@@ -1,11 +0,0 @@
|
||||
import { sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
|
||||
// Team table
|
||||
export const teams = sqliteTable("team", {
|
||||
id: text("id")
|
||||
.primaryKey()
|
||||
.notNull()
|
||||
.$defaultFn(() => createId()),
|
||||
name: text("name").notNull(),
|
||||
});
|
||||
@@ -1,46 +0,0 @@
|
||||
import {
|
||||
sqliteTable,
|
||||
text,
|
||||
integer,
|
||||
uniqueIndex,
|
||||
} from "drizzle-orm/sqlite-core";
|
||||
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { teams } from "./teams";
|
||||
|
||||
// Tournament table
|
||||
export const tournaments = sqliteTable("tournament", {
|
||||
id: text("id")
|
||||
.primaryKey()
|
||||
.notNull()
|
||||
.$defaultFn(() => createId()),
|
||||
name: text("name").notNull(),
|
||||
});
|
||||
|
||||
// TournamentTeam join table
|
||||
export const tournamentTeams = sqliteTable(
|
||||
"tournament_team",
|
||||
{
|
||||
id: text("id")
|
||||
.primaryKey()
|
||||
.notNull()
|
||||
.$defaultFn(() => createId()),
|
||||
tournamentId: text("tournament_id")
|
||||
.notNull()
|
||||
.references(() => tournaments.id, { onDelete: "cascade" }),
|
||||
teamId: text("team_id")
|
||||
.notNull()
|
||||
.references(() => teams.id, { onDelete: "cascade" }),
|
||||
registrationDate: integer("registration_date", { mode: "timestamp" })
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date()),
|
||||
},
|
||||
(table) => {
|
||||
return {
|
||||
tournamentTeamUnique: uniqueIndex("tournament_team_unique").on(
|
||||
table.tournamentId,
|
||||
table.teamId,
|
||||
),
|
||||
};
|
||||
},
|
||||
);
|
||||
@@ -1,10 +0,0 @@
|
||||
// Roles enum equivalent
|
||||
export const RoleEnum = {
|
||||
VISITOR: "VISITOR",
|
||||
PARTICIPANT: "PARTICIPANT",
|
||||
TEAMMATE: "TEAMMATE",
|
||||
CAPTAIN: "CAPTAIN",
|
||||
ADMIN: "ADMIN",
|
||||
} as const;
|
||||
|
||||
export type Role = (typeof RoleEnum)[keyof typeof RoleEnum];
|
||||
@@ -1,18 +0,0 @@
|
||||
import { sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
|
||||
// User table
|
||||
export const users = sqliteTable("user", {
|
||||
id: text("id")
|
||||
.primaryKey()
|
||||
.notNull()
|
||||
.$defaultFn(() => createId()),
|
||||
email: text("email").notNull(),
|
||||
|
||||
// Other information
|
||||
steamId: text("steam_id"),
|
||||
firstName: text("first_name").notNull(),
|
||||
lastName: text("last_name").notNull(),
|
||||
ticketId: text("ticket_id").unique(),
|
||||
ticketType: text("ticket_type"),
|
||||
});
|
||||
57
src/hooks/useCountUp.ts
Normal file
57
src/hooks/useCountUp.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
interface UseCountUpOptions {
|
||||
end: number;
|
||||
duration?: number;
|
||||
suffix?: string;
|
||||
prefix?: string;
|
||||
}
|
||||
|
||||
export function useCountUp({ end, duration = 2000, suffix = "", prefix = "" }: UseCountUpOptions) {
|
||||
const [count, setCount] = useState(0);
|
||||
const [hasStarted, setHasStarted] = useState(false);
|
||||
const ref = useRef<HTMLElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting && !hasStarted) {
|
||||
setHasStarted(true);
|
||||
}
|
||||
},
|
||||
{ threshold: 0.3 }
|
||||
);
|
||||
|
||||
observer.observe(el);
|
||||
return () => observer.disconnect();
|
||||
}, [hasStarted]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasStarted) return;
|
||||
|
||||
const startTime = performance.now();
|
||||
|
||||
const animate = (currentTime: number) => {
|
||||
const elapsed = currentTime - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
// ease-out cubic
|
||||
const eased = 1 - Math.pow(1 - progress, 3);
|
||||
setCount(Math.round(eased * end));
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
};
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
}, [hasStarted, end, duration]);
|
||||
|
||||
const display = `${prefix}${count}${suffix}`;
|
||||
|
||||
return { ref, display, count };
|
||||
}
|
||||
29
src/hooks/useCountdown.ts
Normal file
29
src/hooks/useCountdown.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function useCountdown(targetDate: Date) {
|
||||
const [timeLeft, setTimeLeft] = useState({ days: 0, hours: 0, minutes: 0, seconds: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
const tick = () => {
|
||||
const now = new Date().getTime();
|
||||
const diff = targetDate.getTime() - now;
|
||||
if (diff <= 0) {
|
||||
setTimeLeft({ days: 0, hours: 0, minutes: 0, seconds: 0 });
|
||||
return;
|
||||
}
|
||||
setTimeLeft({
|
||||
days: Math.floor(diff / (1000 * 60 * 60 * 24)),
|
||||
hours: Math.floor((diff / (1000 * 60 * 60)) % 24),
|
||||
minutes: Math.floor((diff / (1000 * 60)) % 60),
|
||||
seconds: Math.floor((diff / 1000) % 60),
|
||||
});
|
||||
};
|
||||
tick();
|
||||
const interval = setInterval(tick, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [targetDate]);
|
||||
|
||||
return timeLeft;
|
||||
}
|
||||
@@ -2,54 +2,12 @@ import { defineRouting } from "next-intl/routing";
|
||||
import { createNavigation } from "next-intl/navigation";
|
||||
|
||||
export const routing = defineRouting({
|
||||
// A list of all locales that are supported
|
||||
locales: ["et", "en"],
|
||||
|
||||
// Used when no locale matches
|
||||
defaultLocale: "et",
|
||||
|
||||
// The `pathnames` object holds pairs of internal and
|
||||
// external paths. The external paths are shown in the URL.
|
||||
pathnames: {
|
||||
// If all locales use the same pathname, a single
|
||||
// external path can be used for all locales
|
||||
"/": "/",
|
||||
"/ajakava": {
|
||||
et: "/ajakava",
|
||||
en: "/schedule",
|
||||
},
|
||||
"/haldus": {
|
||||
et: "/haldus",
|
||||
en: "/admin",
|
||||
},
|
||||
"/kodukord": {
|
||||
et: "/kodukord",
|
||||
en: "/houserules",
|
||||
},
|
||||
"/messiala": {
|
||||
et: "/messiala",
|
||||
en: "/expo",
|
||||
},
|
||||
"/piletid": {
|
||||
et: "/piletid",
|
||||
en: "/tickets",
|
||||
},
|
||||
"/reeglid": {
|
||||
et: "/reeglid",
|
||||
en: "/gamerules",
|
||||
},
|
||||
"/striim": {
|
||||
et: "/striim",
|
||||
en: "/stream",
|
||||
},
|
||||
"/turniirid": {
|
||||
et: "/turniirid",
|
||||
en: "/tournaments",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Lightweight wrappers around Next.js' navigation APIs
|
||||
// that will consider the routing configuration
|
||||
export const { Link, redirect, usePathname, useRouter } =
|
||||
createNavigation(routing);
|
||||
|
||||
@@ -1,353 +0,0 @@
|
||||
import { db } from "@/db/drizzle";
|
||||
import { users } from "@/db/schema/users";
|
||||
import { teams } from "@/db/schema/teams";
|
||||
import { members } from "@/db/schema/members";
|
||||
import { tournamentTeams } from "@/db/schema/tournaments";
|
||||
import { eq, and, isNull } from "drizzle-orm";
|
||||
import { RoleEnum, type Role } from "@/db/schema/types";
|
||||
|
||||
// Types based on the Fienta API response
|
||||
export interface FientaApiResponse {
|
||||
success: {
|
||||
code: number;
|
||||
user_message: string;
|
||||
internal_message: string;
|
||||
};
|
||||
time: {
|
||||
timestamp: number;
|
||||
date: string;
|
||||
time: string;
|
||||
full_datetime: string;
|
||||
timezone: string;
|
||||
timezone_short: string;
|
||||
gmt: string;
|
||||
};
|
||||
count: number;
|
||||
tickets: FientaTicket[];
|
||||
}
|
||||
|
||||
export interface FientaTicket {
|
||||
id: number;
|
||||
organizer_id: number;
|
||||
event_id: number;
|
||||
order_id: number;
|
||||
code: string;
|
||||
status: string;
|
||||
used_at: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
validated_by_id: number | null;
|
||||
ip: string;
|
||||
is_parent: boolean;
|
||||
parent_id: number | null;
|
||||
order_email: string;
|
||||
order_phone: string;
|
||||
contents_html: string;
|
||||
nametag_html: string;
|
||||
qty: number;
|
||||
permissions: {
|
||||
update: boolean;
|
||||
};
|
||||
rows: FientaTicketRow[];
|
||||
}
|
||||
|
||||
export interface FientaTicketRow {
|
||||
ticket_type: {
|
||||
id: number;
|
||||
title: string;
|
||||
attendance_mode: string;
|
||||
};
|
||||
attendee: {
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
full_name: string;
|
||||
email: string;
|
||||
[key: string]: string; // To handle dynamic field names
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches tickets from the Fienta API for a specific event
|
||||
*/
|
||||
export async function fetchFientaTickets(
|
||||
eventId: string,
|
||||
apiKey: string,
|
||||
): Promise<FientaApiResponse> {
|
||||
const response = await fetch(
|
||||
`https://fienta.com/api/v1/events/${eventId}/tickets/?attendees`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
Accept: "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch tickets: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts Steam ID from a Steam profile URL
|
||||
*/
|
||||
export function extractSteamId(steamUrl: string): string | null {
|
||||
if (!steamUrl) return null;
|
||||
|
||||
// Use regex to handle both escaped and unescaped URLs, with or without trailing slash
|
||||
const regex = /(?:\/|\\\/)(id|profiles)\/([^\/\\]+)(?:\/|\\"\/)?$/;
|
||||
const match = steamUrl.match(regex);
|
||||
|
||||
// Return the matched ID or null if no match
|
||||
return match && match[2] ? match[2] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds an attendee field by prefix in the attendee data
|
||||
* Fienta's API can return custom form fields with dynamic names
|
||||
*/
|
||||
function findAttendeeFieldByPrefix(
|
||||
attendee: { [key: string]: string },
|
||||
prefix: string,
|
||||
): string | null {
|
||||
// Find the first field that starts with the given prefix
|
||||
const fieldKey = Object.keys(attendee).find((key) => key.startsWith(prefix));
|
||||
|
||||
// Return the value if found, or null otherwise
|
||||
return fieldKey ? attendee[fieldKey] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the appropriate user role based on ticket type and captain status
|
||||
*/
|
||||
function determineUserRole(
|
||||
ticketType: string,
|
||||
isCaptain: boolean,
|
||||
isTeamMember: boolean,
|
||||
): Role {
|
||||
// Case-insensitive check for tournament participants
|
||||
const isTournamentParticipant = ticketType
|
||||
.toLowerCase()
|
||||
.includes("põhiturniiri osaleja");
|
||||
|
||||
const isParticipant = ticketType.toLowerCase().includes("arvutiga osaleja");
|
||||
|
||||
if (!isTeamMember && isParticipant) {
|
||||
return RoleEnum.PARTICIPANT;
|
||||
}
|
||||
|
||||
if (isTournamentParticipant) {
|
||||
if (isCaptain) {
|
||||
return RoleEnum.CAPTAIN;
|
||||
} else if (isTeamMember) {
|
||||
return RoleEnum.TEAMMATE;
|
||||
}
|
||||
return RoleEnum.PARTICIPANT;
|
||||
}
|
||||
|
||||
return RoleEnum.VISITOR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upserts a user in the database (create or update)
|
||||
*/
|
||||
async function upsertUser(userData: {
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
steamId: string | null;
|
||||
ticketId: string;
|
||||
ticketType: string;
|
||||
}) {
|
||||
// Try to find existing user by ticket ID (primary unique identifier)
|
||||
const existingUser = await db.query.users.findFirst({
|
||||
where: eq(users.ticketId, userData.ticketId),
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
// Update existing user
|
||||
await db
|
||||
.update(users)
|
||||
.set({
|
||||
email: userData.email,
|
||||
firstName: userData.firstName,
|
||||
lastName: userData.lastName,
|
||||
steamId: userData.steamId,
|
||||
ticketType: userData.ticketType,
|
||||
})
|
||||
.where(eq(users.id, existingUser.id));
|
||||
|
||||
return existingUser;
|
||||
} else {
|
||||
// Create new user
|
||||
const [newUser] = await db.insert(users).values(userData).returning();
|
||||
return newUser;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upserts a team member relationship
|
||||
*/
|
||||
async function upsertMember(userId: string, teamId: string | null, role: Role) {
|
||||
// Try to find existing member
|
||||
const whereCondition = teamId
|
||||
? and(eq(members.userId, userId), eq(members.teamId, teamId))
|
||||
: and(eq(members.userId, userId), isNull(members.teamId));
|
||||
|
||||
const existingMember = await db.query.members.findFirst({
|
||||
where: whereCondition,
|
||||
});
|
||||
|
||||
if (existingMember) {
|
||||
// Update existing member
|
||||
await db
|
||||
.update(members)
|
||||
.set({ role })
|
||||
.where(eq(members.id, existingMember.id));
|
||||
} else {
|
||||
// Create new member
|
||||
await db.insert(members).values({
|
||||
userId,
|
||||
teamId,
|
||||
role,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes tickets from Fienta and updates the database
|
||||
*/
|
||||
export async function syncFientaTickets(
|
||||
tickets: FientaTicket[],
|
||||
): Promise<void> {
|
||||
// Process each ticket to extract user and team information
|
||||
for (const ticket of tickets) {
|
||||
// Skip tickets with CANCELLED status or empty rows
|
||||
if (ticket.rows.length === 0 || ticket.status === "CANCELLED") continue;
|
||||
|
||||
const ticketRow = ticket.rows[0];
|
||||
const attendee = ticketRow.attendee;
|
||||
const ticketType = ticketRow.ticket_type.title;
|
||||
|
||||
// Extract data
|
||||
const email = attendee.email || ticket.order_email;
|
||||
const firstName = attendee.first_name;
|
||||
const lastName = attendee.last_name;
|
||||
const steamUrl = findAttendeeFieldByPrefix(attendee, "steam_konto_link");
|
||||
const teamName = findAttendeeFieldByPrefix(attendee, "tiimi_nimi");
|
||||
const captainName = findAttendeeFieldByPrefix(
|
||||
attendee,
|
||||
"tiimi_kapteni_nimi",
|
||||
);
|
||||
|
||||
const steamId = steamUrl ? extractSteamId(steamUrl) : null;
|
||||
// Check if user is captain - captain name must exist and match user's name
|
||||
const isCaptain = captainName !== null && steamId === captainName;
|
||||
|
||||
// Create or update user
|
||||
const user = await upsertUser({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
steamId,
|
||||
ticketId: ticket.code,
|
||||
ticketType,
|
||||
});
|
||||
|
||||
// Handle team association if there is a team name
|
||||
if (teamName) {
|
||||
// Find the team by name first
|
||||
let team = await db.query.teams.findFirst({
|
||||
where: eq(teams.name, teamName),
|
||||
});
|
||||
|
||||
// Create the team if it doesn't exist
|
||||
if (!team) {
|
||||
const [newTeam] = await db
|
||||
.insert(teams)
|
||||
.values({ name: teamName })
|
||||
.returning();
|
||||
team = newTeam;
|
||||
}
|
||||
|
||||
// Determine appropriate role
|
||||
const role = determineUserRole(ticketType, isCaptain, true);
|
||||
|
||||
// Create or update membership with appropriate role
|
||||
await upsertMember(user.id, team.id, role);
|
||||
} else {
|
||||
// For users without a team, handle membership without team association
|
||||
const role = determineUserRole(ticketType, false, false);
|
||||
await upsertMember(user.id, null, role);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function to fetch and sync tickets from Fienta to the database
|
||||
*/
|
||||
export async function syncFientaEvent(
|
||||
eventId: string,
|
||||
apiKey: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const response = await fetchFientaTickets(eventId, apiKey);
|
||||
await syncFientaTickets(response.tickets);
|
||||
} catch (error) {
|
||||
console.error("Error syncing Fienta tickets:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets teams with their members from the database
|
||||
*/
|
||||
export async function getTeamsWithMembers() {
|
||||
return await db.query.teams.findMany({
|
||||
with: {
|
||||
members: {
|
||||
with: {
|
||||
user: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a specific team with its members
|
||||
*/
|
||||
export async function getTeamWithMembers(teamId: string) {
|
||||
return await db.query.teams.findFirst({
|
||||
where: eq(teams.id, teamId),
|
||||
with: {
|
||||
members: {
|
||||
with: {
|
||||
user: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets tournament teams with related data
|
||||
*/
|
||||
export async function getTournamentTeams(tournamentId: string) {
|
||||
return await db.query.tournamentTeams.findMany({
|
||||
where: eq(tournamentTeams.tournamentId, tournamentId),
|
||||
with: {
|
||||
team: {
|
||||
with: {
|
||||
members: {
|
||||
with: {
|
||||
user: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
export type Locale = "et" | "en";
|
||||
export type RuleType = "cs2" | "lol" | "kodukord";
|
||||
|
||||
/**
|
||||
* Loads any rule content for a specific type and locale
|
||||
* @param ruleType - The type of rules to load (cs2, lol, kodukord)
|
||||
* @param locale - The locale to load rules for (et, en)
|
||||
* @returns Promise<string> The markdown content of the rules file
|
||||
*/
|
||||
export async function loadRules(
|
||||
ruleType: RuleType,
|
||||
locale: Locale,
|
||||
): Promise<string> {
|
||||
// Try to load the file for the current locale first
|
||||
const filePath = path.join(
|
||||
process.cwd(),
|
||||
"src",
|
||||
"data",
|
||||
"rules",
|
||||
locale,
|
||||
`${ruleType}.md`,
|
||||
);
|
||||
|
||||
try {
|
||||
// Check if file exists for current locale
|
||||
if (fs.existsSync(filePath)) {
|
||||
const file = await import("fs").then(() =>
|
||||
fs.readFileSync(filePath, "utf8"),
|
||||
);
|
||||
return file;
|
||||
}
|
||||
|
||||
// Fallback to Estonian if English version doesn't exist
|
||||
if (locale !== "et") {
|
||||
console.warn(
|
||||
`Rules file not found for ${ruleType} in ${locale}, falling back to Estonian`,
|
||||
);
|
||||
const fallbackPath = path.join(
|
||||
process.cwd(),
|
||||
"src",
|
||||
"data",
|
||||
"rules",
|
||||
"et",
|
||||
`${ruleType}.md`,
|
||||
);
|
||||
|
||||
if (fs.existsSync(fallbackPath)) {
|
||||
const fallbackFile = fs.readFileSync(fallbackPath, "utf8");
|
||||
return fallbackFile;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Rules file not found for ${ruleType}`);
|
||||
} 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"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all available rule types for a given locale
|
||||
* @param locale - The locale to check for available rules
|
||||
* @returns Promise<RuleType[]> Array of available rule types
|
||||
*/
|
||||
export async function getAvailableRules(locale: Locale): Promise<RuleType[]> {
|
||||
const rulesDir = path.join(process.cwd(), "src", "data", "rules", locale);
|
||||
|
||||
try {
|
||||
const files = fs.readdirSync(rulesDir);
|
||||
const ruleTypes = files
|
||||
.filter((file) => file.endsWith(".md"))
|
||||
.map((file) => file.replace(".md", "") as RuleType);
|
||||
|
||||
return ruleTypes;
|
||||
} catch (error) {
|
||||
console.warn(`Could not read rules directory for locale ${locale}:`, error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a specific rule file exists for a given locale
|
||||
* @param ruleType - The type of rules to check
|
||||
* @param locale - The locale to check
|
||||
* @returns boolean indicating if the file exists
|
||||
*/
|
||||
export function ruleExists(ruleType: RuleType, locale: Locale): boolean {
|
||||
const filePath = path.join(
|
||||
process.cwd(),
|
||||
"src",
|
||||
"data",
|
||||
"rules",
|
||||
locale,
|
||||
`${ruleType}.md`,
|
||||
);
|
||||
return fs.existsSync(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the best available locale for a rule type
|
||||
* Prefers the requested locale, falls back to Estonian
|
||||
* @param ruleType - The type of rules to check
|
||||
* @param preferredLocale - The preferred locale
|
||||
* @returns Locale The best available locale for the rule type
|
||||
*/
|
||||
export function getBestAvailableLocale(
|
||||
ruleType: RuleType,
|
||||
preferredLocale: Locale,
|
||||
): Locale {
|
||||
if (ruleExists(ruleType, preferredLocale)) {
|
||||
return preferredLocale;
|
||||
}
|
||||
|
||||
if (ruleExists(ruleType, "et")) {
|
||||
return "et";
|
||||
}
|
||||
|
||||
// If neither exists, return preferred (will throw error when trying to load)
|
||||
return preferredLocale;
|
||||
}
|
||||
119
src/lib/rules.ts
119
src/lib/rules.ts
@@ -1,119 +0,0 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
export type Locale = "et" | "en";
|
||||
export type RuleType = "cs2" | "lol" | "kodukord";
|
||||
|
||||
/**
|
||||
* Loads rules content for a specific game and locale
|
||||
* @param ruleType - The type of rules to load (cs2, lol, kodukord)
|
||||
* @param locale - The locale to load rules for (et, en)
|
||||
* @returns The markdown content of the rules file
|
||||
*/
|
||||
export async function getRules(
|
||||
ruleType: RuleType,
|
||||
locale: Locale,
|
||||
): Promise<string> {
|
||||
const filePath = path.join(
|
||||
process.cwd(),
|
||||
"src",
|
||||
"data",
|
||||
"rules",
|
||||
locale,
|
||||
`${ruleType}.md`,
|
||||
);
|
||||
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, "utf8");
|
||||
return content;
|
||||
} catch (error) {
|
||||
// Fallback to Estonian if English version doesn't exist
|
||||
if (locale === "en") {
|
||||
console.warn(
|
||||
`Rules file not found for ${ruleType} in ${locale}, falling back to Estonian`,
|
||||
);
|
||||
const fallbackPath = path.join(
|
||||
process.cwd(),
|
||||
"src",
|
||||
"data",
|
||||
"rules",
|
||||
"et",
|
||||
`${ruleType}.md`,
|
||||
);
|
||||
try {
|
||||
const fallbackContent = fs.readFileSync(fallbackPath, "utf8");
|
||||
return fallbackContent;
|
||||
} catch (fallbackError) {
|
||||
throw new Error(
|
||||
`Rules file not found for ${ruleType} in either ${locale} or et locale: ${fallbackError}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Rules file not found for ${ruleType} in ${locale} locale: ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all available rule types
|
||||
* @param locale - The locale to check for available rules
|
||||
* @returns Array of available rule types
|
||||
*/
|
||||
export async function getAvailableRules(locale: Locale): Promise<RuleType[]> {
|
||||
const rulesDir = path.join(process.cwd(), "src", "data", "rules", locale);
|
||||
|
||||
try {
|
||||
const files = fs.readdirSync(rulesDir);
|
||||
const ruleTypes = files
|
||||
.filter((file) => file.endsWith(".md"))
|
||||
.map((file) => file.replace(".md", "") as RuleType);
|
||||
|
||||
return ruleTypes;
|
||||
} catch (error) {
|
||||
console.warn(`Could not read rules directory for locale ${locale}:`, error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a specific rule file exists for a given locale
|
||||
* @param ruleType - The type of rules to check
|
||||
* @param locale - The locale to check
|
||||
* @returns Boolean indicating if the file exists
|
||||
*/
|
||||
export function ruleExists(ruleType: RuleType, locale: Locale): boolean {
|
||||
const filePath = path.join(
|
||||
process.cwd(),
|
||||
"src",
|
||||
"data",
|
||||
"rules",
|
||||
locale,
|
||||
`${ruleType}.md`,
|
||||
);
|
||||
return fs.existsSync(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the best available locale for a rule type
|
||||
* Prefers the requested locale, falls back to Estonian
|
||||
* @param ruleType - The type of rules to check
|
||||
* @param preferredLocale - The preferred locale
|
||||
* @returns The best available locale for the rule type
|
||||
*/
|
||||
export function getBestAvailableLocale(
|
||||
ruleType: RuleType,
|
||||
preferredLocale: Locale,
|
||||
): Locale {
|
||||
if (ruleExists(ruleType, preferredLocale)) {
|
||||
return preferredLocale;
|
||||
}
|
||||
|
||||
if (ruleExists(ruleType, "et")) {
|
||||
return "et";
|
||||
}
|
||||
|
||||
// If neither exists, return preferred (will throw error when trying to load)
|
||||
return preferredLocale;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import createMiddleware from 'next-intl/middleware';
|
||||
import {routing} from './i18n/routing';
|
||||
|
||||
export default createMiddleware(routing);
|
||||
|
||||
export const config = {
|
||||
// Match only internationalized pathnames
|
||||
matcher: [
|
||||
// Enable a redirect to a matching locale at the root
|
||||
'/',
|
||||
|
||||
// Set a cookie to remember the previous locale for
|
||||
// all requests that have a locale prefix
|
||||
'/(et|en)/:path*',
|
||||
|
||||
// Enable redirects that add missing locales
|
||||
// (e.g. `/pathnames` -> `/en/pathnames`)
|
||||
'/((?!_next|_vercel|.*\\..*).*)'
|
||||
]
|
||||
};
|
||||
17
src/proxy.ts
Normal file
17
src/proxy.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import createMiddleware from 'next-intl/middleware';
|
||||
import { NextRequest } from 'next/server';
|
||||
import { routing } from './i18n/routing';
|
||||
|
||||
const intlMiddleware = createMiddleware(routing);
|
||||
|
||||
export function proxy(request: NextRequest) {
|
||||
return intlMiddleware(request);
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
'/',
|
||||
'/(et|en)/:path*',
|
||||
'/((?!_next|_vercel|.*\\..*).*)'
|
||||
]
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { InferSelectModel } from "drizzle-orm";
|
||||
import { users } from "@/db/schema/users";
|
||||
import { teams } from "@/db/schema/teams";
|
||||
import { members } from "@/db/schema/members";
|
||||
|
||||
// Base types from schema
|
||||
export type User = InferSelectModel<typeof users>;
|
||||
export type Team = InferSelectModel<typeof teams>;
|
||||
export type Member = InferSelectModel<typeof members>;
|
||||
|
||||
// Extended types for queries with relations
|
||||
export type TeamWithMembers = Team & {
|
||||
members: (Member & {
|
||||
user: User;
|
||||
})[];
|
||||
};
|
||||
|
||||
export type MemberWithUser = Member & {
|
||||
user: User;
|
||||
};
|
||||
|
||||
export type UserWithMembers = User & {
|
||||
members: Member[];
|
||||
};
|
||||
Reference in New Issue
Block a user