Merge branch 'development' into gamerules

pull/53/head
Renkar 4 months ago committed by GitHub
commit e5edbee7ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 24
      next.config.ts
  2. 35
      package.json
  3. BIN
      public/spaces/fuajeeTalTech.glb
  4. BIN
      public/sponsors/tallinnaharidusamet.jpg
  5. 77
      src/app/[locale]/ajakava/page.tsx
  6. 32
      src/app/[locale]/haldus/meeskonnad/page.tsx
  7. 48
      src/app/[locale]/haldus/page.tsx
  8. 64
      src/app/[locale]/kodukord/page.tsx
  9. 38
      src/app/[locale]/layout.tsx
  10. 974
      src/app/[locale]/messiala/page.tsx
  11. 19
      src/app/[locale]/not-found.tsx
  12. 79
      src/app/[locale]/page.tsx
  13. 114
      src/app/[locale]/piletid/page.tsx
  14. 106
      src/app/[locale]/reeglid/[slug]/page.tsx
  15. 63
      src/app/[locale]/reeglid/page.tsx
  16. 224
      src/app/[locale]/striim/page.tsx
  17. 188
      src/app/[locale]/turniirid/page.tsx
  18. 72
      src/app/ajakava/page.tsx
  19. 63
      src/app/kodukord/page.tsx
  20. 37
      src/app/layout.tsx
  21. 480
      src/app/messiala/page.tsx
  22. 29
      src/app/not-found.tsx
  23. 96
      src/app/piletid/page.tsx
  24. 44
      src/app/reeglid/[slug]/page.tsx
  25. 63
      src/app/reeglid/page.tsx
  26. 184
      src/app/turniirid/page.tsx
  27. 211
      src/components/Footer.tsx
  28. 107
      src/components/Header.tsx
  29. 47
      src/components/LanguageSwitcher.tsx
  30. 7
      src/components/SectionDivider.tsx
  31. 88
      src/components/Sidebar.tsx
  32. 84
      src/components/SidebarLayoutClient.tsx
  33. 26
      src/components/SidebarLayoutServer.tsx
  34. 26
      src/components/SidebarParent.tsx
  35. 30
      src/components/ui/button.tsx
  36. 13
      src/components/ui/skeleton.tsx
  37. 30
      src/data/kodukord.md
  38. 62
      src/data/rules/lol.md
  39. 67
      src/data/timetable.ts
  40. 17
      src/i18n/request.ts
  41. 55
      src/i18n/routing.ts
  42. 20
      src/middleware.ts
  43. 227
      translations/en.json
  44. 227
      translations/et.json

@ -1,7 +1,27 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
import createNextIntlPlugin from "next-intl/plugin";
const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts");
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
/* config options here */ async headers() {
return [
{
source: "/(.*)",
headers: [
{
key: "Content-Security-Policy",
value:
"frame-src 'self' https://tipilan.ee https://player.twitch.tv https://embed.twitch.tv; frame-ancestors 'self' https://tipilan.ee;",
},
{
key: "X-Frame-Options",
value: "SAMEORIGIN",
},
],
},
];
},
}; };
export default nextConfig; export default withNextIntl(nextConfig);

@ -15,47 +15,46 @@
"drizzle:studio": "drizzle-kit studio" "drizzle:studio": "drizzle-kit studio"
}, },
"dependencies": { "dependencies": {
"@libsql/client": "^0.15.9", "@libsql/client": "^0.15.12",
"@paralleldrive/cuid2": "^2.2.2", "@paralleldrive/cuid2": "^2.2.2",
"@radix-ui/react-alert-dialog": "^1.1.14", "@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-select": "^2.2.5", "@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tooltip": "^1.2.7", "@radix-ui/react-tooltip": "^1.2.8",
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
"@types/three": "^0.178.1", "@types/three": "^0.178.1",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"drizzle-orm": "^0.44.2", "drizzle-orm": "^0.44.4",
"gray-matter": "^4.0.3",
"lucide-react": "^0.522.0", "lucide-react": "^0.522.0",
"material-symbols": "^0.31.8", "material-symbols": "^0.31.9",
"next": "15.3.0", "next": "15.3.0",
"next-intl": "^4.3.4",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"remark-gfm": "^4.0.1",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"three": "^0.178.0", "three": "^0.178.0",
"tw-animate-css": "^1.3.4" "tw-animate-css": "^1.3.7"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",
"@tailwindcss/postcss": "^4.1.10", "@tailwindcss/postcss": "^4.1.12",
"@types/bun": "^1.2.18", "@types/bun": "^1.2.20",
"@types/node": "^20.19.1", "@types/node": "^20.19.11",
"@types/react": "^19.1.9", "@types/react": "^19.1.10",
"@types/react-dom": "^19.1.7", "@types/react-dom": "^19.1.7",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"dotenv": "^16.3.1", "dotenv": "^16.6.1",
"drizzle-kit": "^0.31.4", "drizzle-kit": "^0.31.4",
"eslint": "^9.29.0", "eslint": "^9.33.0",
"eslint-config-next": "15.3.0", "eslint-config-next": "15.3.0",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"tailwindcss": "^4.1.10", "tailwindcss": "^4.1.12",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.8.3" "typescript": "^5.9.2"
} }
} }

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

@ -0,0 +1,77 @@
"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-5xl sm: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 space-x-4 mb-8">
{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-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 justify-center`}
>
{item.time}
</div>
<div className="flex-1 flex flex-col justify-center min-w-0 min-h-[120px]">
<div
className={`${vipnagorgialla.className} 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>
);
}

@ -7,7 +7,8 @@ import { db } from "@/db/drizzle";
// Types // Types
import type { TeamWithMembers, MemberWithUser } from "@/types/database"; import type { TeamWithMembers, MemberWithUser } from "@/types/database";
import Link from "next/link"; import { Link } from "@/i18n/routing";
import { getTranslations, setRequestLocale } from "next-intl/server";
// User interface // User interface
import { import {
@ -19,19 +20,26 @@ import {
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
// Later on we can use a i8 solution? // Function to translate roles using i18n
function translateRole(role: string): string { function translateRole(role: string, t: (key: string) => string): string {
switch (role) { switch (role) {
case "CAPTAIN": case "CAPTAIN":
return "Kapten"; return t("admin.roles.captain");
case "TEAMMATE": case "TEAMMATE":
return "Meeskonnaliige"; return t("admin.roles.teammate");
default: default:
return role; return role;
} }
} }
export default async function AdminTeams() { 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 // Fetch teams with their members and member users
const teams = await db.query.teams.findMany({ const teams = await db.query.teams.findMany({
with: { with: {
@ -54,7 +62,7 @@ export default async function AdminTeams() {
<h1 <h1
className={`text-5xl sm:text-6xl ${vipnagorgialla.className} font-bold italic uppercase text-[#2A2C3F] dark:text-[#EEE5E5] mt-8 mb-4`} className={`text-5xl sm:text-6xl ${vipnagorgialla.className} font-bold italic uppercase text-[#2A2C3F] dark:text-[#EEE5E5] mt-8 mb-4`}
> >
Haldus - Meeskonnad {t("admin.title")} - {t("admin.teams")}
</h1> </h1>
</div> </div>
<div className="text-2xl text-[#2A2C3F] dark:text-[#EEE5E5]"> <div className="text-2xl text-[#2A2C3F] dark:text-[#EEE5E5]">
@ -63,8 +71,8 @@ export default async function AdminTeams() {
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead className="w-[100px]">ID</TableHead> <TableHead className="w-[100px]">ID</TableHead>
<TableHead>Nimi</TableHead> <TableHead>{t("admin.table.name")}</TableHead>
<TableHead>Liikmed</TableHead> <TableHead>{t("admin.table.members")}</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
@ -84,12 +92,14 @@ export default async function AdminTeams() {
{member.user.firstName} {member.user.lastName} {member.user.firstName} {member.user.lastName}
</span> </span>
<span className="text-gray-500"> <span className="text-gray-500">
({translateRole(member.role)}) ({translateRole(member.role, t)})
</span> </span>
</div> </div>
)) ))
) : ( ) : (
<span className="text-gray-500">Liikmeid puuduvad</span> <span className="text-gray-500">
{t("admin.table.noMembers")}
</span>
)} )}
</div> </div>
</TableCell> </TableCell>

@ -17,9 +17,10 @@ import {
X, X,
} from "lucide-react"; } from "lucide-react";
import Link from "next/link"; import { getTranslations, setRequestLocale } from "next-intl/server";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { redirect, RedirectType } from "next/navigation"; import { redirect, RedirectType } from "next/navigation";
import NextLink from "next/link";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -53,13 +54,13 @@ async function dismissAlert() {
redirect("/haldus", RedirectType.replace); redirect("/haldus", RedirectType.replace);
} }
const SuccessAlertDB = () => { const SuccessAlertDB = ({ t }: { t: (key: string) => string }) => {
return ( return (
<Alert className="flex items-start mt-8"> <Alert className="flex items-start mt-8">
<CheckCircle2Icon className="mt-0.5" /> <CheckCircle2Icon className="mt-0.5" />
<div className="flex-1"> <div className="flex-1">
<AlertTitle>Toiming oli edukas!</AlertTitle> <AlertTitle>{t("admin.success.title")}</AlertTitle>
<AlertDescription>Andmebaasi andmed on uuendatud.</AlertDescription> <AlertDescription>{t("admin.success.description")}</AlertDescription>
</div> </div>
<form action={dismissAlert} className="ml-2"> <form action={dismissAlert} className="ml-2">
<Button <Button
@ -76,10 +77,15 @@ const SuccessAlertDB = () => {
}; };
export default async function Admin({ export default async function Admin({
params,
searchParams, searchParams,
}: { }: {
params: Promise<{ locale: string }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>; searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}) { }) {
const { locale } = await params;
setRequestLocale(locale);
const t = await getTranslations({ locale });
const alarmStatus = await searchParams; const alarmStatus = await searchParams;
const showSuccess = alarmStatus.success === "true"; const showSuccess = alarmStatus.success === "true";
@ -97,31 +103,31 @@ export default async function Admin({
return ( return (
<div className="flex flex-col min-h-[90vh] m-6 mt-16 md:m-16"> <div className="flex flex-col min-h-[90vh] m-6 mt-16 md:m-16">
{showSuccess && <SuccessAlertDB />} {showSuccess && <SuccessAlertDB t={t} />}
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<Link href={"/"}> <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"> <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 arrow_left_alt
</span> </span>
</Link> </NextLink>
<h1 <h1
className={`text-5xl sm:text-6xl ${vipnagorgialla.className} font-bold italic uppercase text-[#2A2C3F] dark:text-[#EEE5E5] mt-8 mb-4`} className={`text-5xl sm:text-6xl ${vipnagorgialla.className} font-bold italic uppercase text-[#2A2C3F] dark:text-[#EEE5E5] mt-8 mb-4`}
> >
Haldus {t("admin.title")}
</h1> </h1>
</div> </div>
<div className="text-2xl text-[#2A2C3F] dark:text-[#EEE5E5]"> <div className="text-2xl text-[#2A2C3F] dark:text-[#EEE5E5]">
<div className="pl-2 flex gap-8 pb-4"> <div className="pl-2 flex gap-8 pb-4">
<div className="flex text-lg md:text-2xl flex-row items-center"> <div className="flex text-lg md:text-2xl flex-row items-center">
<Users className="mr-2" /> <Users className="mr-2" />
Kasutajaid: {usersData.length} {t("admin.users")}: {usersData.length}
</div> </div>
<Link href="/haldus/meeskonnad" className="flex items-center"> <NextLink href="/haldus/meeskonnad" className="flex items-center">
<div className="flex text-lg md:text-2xl flex-row items-center"> <div className="flex text-lg md:text-2xl flex-row items-center">
<IdCardLanyard className="mr-2" /> <IdCardLanyard className="mr-2" />
Meeskondasid: {teamsData.length} {t("admin.teams")}: {teamsData.length}
</div> </div>
</Link> </NextLink>
<AlertDialog> <AlertDialog>
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
<div className="ml-auto"> <div className="ml-auto">
@ -135,24 +141,24 @@ export default async function Admin({
</div> </div>
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogTitle> <AlertDialogTitle>{t("admin.sync.title")}</AlertDialogTitle>
Kas soovite värskendada andmebaasi?
</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
See tõmbab Fientast praegused andmed ning asendab{" "} {t("admin.sync.description1")}{" "}
<span className="text-red-600 font-semibold">KÕIK</span>{" "} <span className="text-red-600 font-semibold">
olemasolevad andmed andmebaasis! {t("admin.sync.all")}
</span>{" "}
{t("admin.sync.description2")}
<br /> <br />
<br /> <br />
Kui sa ei ole kindel, vajuta &quot;Tühista&quot;. {t("admin.sync.warning")}
</AlertDialogDescription> </AlertDialogDescription>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel className="cursor-pointer"> <AlertDialogCancel className="cursor-pointer">
Tühista {t("common.cancel")}
</AlertDialogCancel> </AlertDialogCancel>
<form action={syncAction}> <form action={syncAction}>
<AlertDialogAction type="submit" className="cursor-pointer"> <AlertDialogAction type="submit" className="cursor-pointer">
Värskenda {t("admin.sync.update")}
</AlertDialogAction> </AlertDialogAction>
</form> </form>
</AlertDialogFooter> </AlertDialogFooter>

@ -0,0 +1,64 @@
// 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";
export default async function Page({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
setRequestLocale(locale);
const t = await getTranslations({ locale });
const file = Bun.file("src/data/kodukord.md");
const content = await file.text();
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>
);
}

@ -0,0 +1,38 @@
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,
params,
}: Readonly<{
children: React.ReactNode;
params: Promise<{ locale: string }>;
}>) {
const { locale } = await params;
// Enable static rendering
setRequestLocale(locale);
// Provide messages for client-side components
const messages = await getMessages();
return (
<div lang={locale}>
<NextIntlClientProvider messages={messages}>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<SidebarParent />
{children}
<Footer />
</ThemeProvider>
</NextIntlClientProvider>
</div>
);
}

@ -0,0 +1,974 @@
"use client";
import { vipnagorgialla } from "@/components/Vipnagorgialla";
import * as THREE from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { useEffect, useRef, useState, useMemo } from "react";
import { EyeClosed, Eye } from "lucide-react";
import SectionDivider from "@/components/SectionDivider";
import { useTranslations } from "next-intl";
// Define interface for the ref with toggle function
interface MountRefCurrent extends HTMLDivElement {
toggleDividers?: (show: boolean) => void;
switchView?: (view: "tudengimaja" | "fuajee") => void;
}
export default function Expo() {
const mountRef = useRef<MountRefCurrent | null>(null);
const [hoveredRoom, setHoveredRoom] = useState<string | null>(null);
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
const [showDividers, setShowDividers] = useState<boolean>(true);
const [currentView, setCurrentView] = useState<"tudengimaja" | "fuajee">(
"tudengimaja",
);
const currentViewRef = useRef<"tudengimaja" | "fuajee">("tudengimaja");
const t = useTranslations();
// Define room names with translations
const roomNames = useMemo(
() => ({
boardGames: t("expo.areas.boardGames"),
bar: t("expo.areas.bar"),
eval: "EVAL",
simRacing: t("expo.areas.simRacing"),
fighting: t("expo.areas.fighting"),
lvlup: "LVLup!",
redbull: "Red Bull",
// fuajee rooms
ityk: t("expo.areas.ityk"),
estoniagamedev: t("expo.areas.estoniagamedev"),
info: t("expo.areas.info"),
tartuyk: t("expo.areas.tartuyk"),
tly: t("expo.areas.tly"),
gameup: "GameUP!",
ittk: t("expo.areas.ittk"),
photobooth: t("expo.areas.photobooth"),
}),
[t],
);
useEffect(() => {
if (!mountRef.current) return;
// Copy ref to variable to avoid stale closure in cleanup
const mountElement = mountRef.current;
let dividersRef: THREE.Mesh[] = [];
const fuajeeMeshes: THREE.Mesh[] = [];
let tudengimajaObjects: THREE.Object3D[] = [];
let fuajeeMesh: THREE.Group | null = null;
const fuajeeRooms: THREE.Mesh[] = [];
// Scene setup
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0e0f19);
// Get responsive dimensions
const getResponsiveDimensions = () => {
const container = mountRef.current;
if (!container) return { width: 800, height: 600 };
const containerWidth = container.offsetWidth;
const maxWidth = Math.min(containerWidth, 800);
const width = Math.max(maxWidth, 300); // Minimum width
const height = (width * 600) / 800; // Maintain aspect ratio
return { width, height };
};
const { width, height } = getResponsiveDimensions();
// Isometric camera setup with responsive sizing
const aspect = width / height;
const baseFrustumSize = 14;
const frustumSize = baseFrustumSize; // Keep consistent frustum size
const camera = new THREE.OrthographicCamera(
(frustumSize * aspect) / -2,
(frustumSize * aspect) / 2,
frustumSize / 2,
frustumSize / -2,
1,
1000,
);
// Camera positions for different views
const cameraPositions = {
tudengimaja: {
position: new THREE.Vector3(10, 10, 14),
lookAt: new THREE.Vector3(-1.4, 0, 0),
},
fuajee: {
position: new THREE.Vector3(30, 20, 15),
lookAt: new THREE.Vector3(0, 0, 0),
},
};
// Position camera for isometric view (default to tudengimaja)
camera.position.copy(cameraPositions.tudengimaja.position);
camera.lookAt(cameraPositions.tudengimaja.lookAt);
// Renderer
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(width, height);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
mountElement.appendChild(renderer.domElement);
// Raycaster for mouse interactions
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
// Lighting
const ambientLight = new THREE.AmbientLight(0x404040, 1.2);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5);
directionalLight.position.set(10, 10, 5);
directionalLight.castShadow = false;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
scene.add(directionalLight);
// Room colors and names
const roomColors = [
0x343434, // Gray - Lauamängude ala
0x4ecdc4, // Turquoise - Baariala
0xffe66d, // Yellow - EVAL
0xff6600, // Orange - Redbull Sim Racing
0xff1493, // Deep Pink - Võitlusmängu ala
0x3498db, // Blue - Sony
0x2ecc71, // Green - Lava
0x080682, // Dark Blue - LVLup!
0xc02841, // Red - RedBull
];
// Create individual rooms as rectangles with custom positions
const rooms: THREE.Mesh[] = [];
const roomData: Array<{
mesh: THREE.Mesh;
name: string;
originalColor: number;
originalScale: THREE.Vector3;
view: "tudengimaja" | "fuajee";
}> = [];
const dividers: THREE.Mesh[] = [];
// Define rooms with custom positions, sizes and colors
const roomDefinitions = [
{
width: 7,
height: 0.7,
depth: 3,
x: 2.5,
z: 4,
color: roomColors[0],
name: roomNames.boardGames,
},
{
width: 3.5,
height: 0.7,
depth: 1.2,
x: 0.7,
z: -0.3,
color: roomColors[1],
name: roomNames.bar,
},
{
width: 1.8,
height: 0.7,
depth: 1.5,
x: 1,
z: -3.5,
color: roomColors[2],
name: roomNames.eval,
},
{
width: 2,
height: 0.7,
depth: 4.5,
x: 5.2,
z: -2,
color: roomColors[3],
name: roomNames.simRacing,
},
{
width: 3,
height: 0.7,
depth: 1.5,
x: -1.7,
z: -3.5,
color: roomColors[4],
name: roomNames.fighting,
},
// {
// width: 1.8,
// height: 0.7,
// depth: 1.5,
// x: -4.3,
// z: -3.5,
// color: roomColors[5],
// name: "Sony",
// },
{
width: 3,
height: 0.7,
depth: 1.7,
x: -3.5,
z: -0.5,
color: roomColors[7],
name: roomNames.lvlup,
},
//{
// width: 2,
// height: 0.7,
// depth: 4,
// x: -6.4,
// z: -2.3,
// color: roomColors[6],
// name: "Lava",
//},
{
width: 1.8,
height: 0.7,
depth: 1.5,
x: 3,
z: -3.5,
color: roomColors[8],
name: roomNames.redbull,
},
];
roomDefinitions.forEach((roomDef) => {
const geometry = new THREE.BoxGeometry(
roomDef.width,
roomDef.height,
roomDef.depth,
);
const material = new THREE.MeshLambertMaterial({
color: roomDef.color,
});
const room = new THREE.Mesh(geometry, material);
room.position.set(roomDef.x, roomDef.height / 2, roomDef.z);
room.castShadow = true;
room.receiveShadow = true;
room.userData = { name: roomDef.name, originalColor: roomDef.color };
scene.add(room);
rooms.push(room);
roomData.push({
mesh: room,
name: roomDef.name,
originalColor: roomDef.color,
originalScale: room.scale.clone(),
view: "tudengimaja",
});
});
// Create toggleable room dividers
const createTogglableDivider = (
width: number,
height: number,
depth: number,
x: number,
z: number,
) => {
const wallGeometry = new THREE.BoxGeometry(width, height, depth);
const wallMaterial = new THREE.MeshLambertMaterial({
color: 0x555555,
transparent: true,
opacity: 0,
});
const wall = new THREE.Mesh(wallGeometry, wallMaterial);
wall.position.set(x, height / 2, z);
wall.visible = false;
scene.add(wall);
dividers.push(wall);
};
// Add strategic dividers between major areas
createTogglableDivider(10, 2, 2, -2.5, 1.5); // Wall between main entrance
createTogglableDivider(2, 2, 2, 5.5, 1.5); // Wall right next to Lauamängud & Redbull Sim Racing
// Store dividers reference for later access
dividersRef = [...dividers];
// Ground plane
const groundGeometry = new THREE.PlaneGeometry(14, 10.5);
const groundMaterial = new THREE.MeshLambertMaterial({ color: 0xcccccc });
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
ground.position.x = -1.1;
ground.position.y = -0.5;
ground.receiveShadow = true;
scene.add(ground);
// Second ground plane
const groundGeometry2 = new THREE.PlaneGeometry(2, 7);
const groundMaterial2 = new THREE.MeshLambertMaterial({
color: 0xcccccc,
});
const ground2 = new THREE.Mesh(groundGeometry2, groundMaterial2);
ground2.rotation.x = -Math.PI / 2;
ground2.position.x = -12.2;
ground2.position.y = -5;
ground2.receiveShadow = true;
scene.add(ground2);
// Store tudengimaja objects (rooms, ground, dividers)
tudengimajaObjects = [...rooms, ground, ground2, ...dividers];
// Load fuajee GLTF model
const loader = new GLTFLoader();
loader.load(
"/spaces/fuajeeTalTech.glb",
(gltf) => {
fuajeeMesh = gltf.scene;
fuajeeMesh.position.set(-1.5, 1, 0);
fuajeeMesh.scale.set(0.3, 0.3, 0.3);
fuajeeMesh.visible = false; // Initially hidden
// Traverse the model to collect meshes
fuajeeMesh.traverse((child) => {
if (child instanceof THREE.Mesh) {
child.castShadow = true;
child.receiveShadow = true;
fuajeeMeshes.push(child);
}
});
scene.add(fuajeeMesh);
// Create example rooms for fuajee after the model loads
createfuajeeRooms();
},
(progress) => {
console.log(
"Loading progress:",
(progress.loaded / progress.total) * 100 + "%",
);
},
(error) => {
console.error("Error loading GLTF:", error);
},
);
// Function to create example rooms for fuajee
const createfuajeeRooms = () => {
const fuajeeRoomColors = [
0x7b1642, // ITÜK - Cherry Red
0x365591, // Light Blue - Tartu Ülikool
0xa82838, // Red - Tallinna Ülikool
0x183bbf, // Dark Blue - Eesti Gamedev
0xd12e7d, // Purple - Taltech
0x228b22, // Green - GameUP
0xff6347, // Orange - Info
0x20b2aa, // Light Sea Green - Photobooth
];
const fuajeeRoomDefinitions = [
{
width: 5,
height: 0.5,
depth: 3.5,
x: -6,
z: 2.8,
color: fuajeeRoomColors[0],
name: roomNames.ityk,
},
{
width: 5,
height: 0.5,
depth: 2,
x: 2.2,
z: -1.5,
color: fuajeeRoomColors[1],
name: roomNames.tartuyk,
},
{
width: 6,
height: 0.5,
depth: 2,
x: -5.8,
z: -1.2,
color: fuajeeRoomColors[3],
name: roomNames.estoniagamedev,
},
{
width: 2,
height: 0.5,
depth: 2,
x: -1.5,
z: -1.5,
color: fuajeeRoomColors[6],
name: roomNames.info,
},
{
width: 2,
height: 0.5,
depth: 1.5,
x: 6,
z: -1.7,
color: fuajeeRoomColors[2],
name: roomNames.tly,
},
{
width: 2,
height: 0.5,
depth: 1.5,
x: 11,
z: -1.7,
color: fuajeeRoomColors[4],
name: roomNames.ittk,
},
{
width: 2,
height: 0.5,
depth: 1.5,
x: 13.5,
z: -1.7,
color: fuajeeRoomColors[7],
name: roomNames.photobooth,
},
{
width: 2,
height: 0.5,
depth: 1.5,
x: 8.5,
z: -1.7,
color: fuajeeRoomColors[5],
name: roomNames.gameup,
},
];
fuajeeRoomDefinitions.forEach((roomDef) => {
const geometry = new THREE.BoxGeometry(
roomDef.width,
roomDef.height,
roomDef.depth,
);
const material = new THREE.MeshLambertMaterial({
color: roomDef.color,
});
const room = new THREE.Mesh(geometry, material);
room.position.set(roomDef.x, roomDef.height / 2 + 2, roomDef.z);
room.castShadow = true;
room.receiveShadow = true;
room.userData = { name: roomDef.name, originalColor: roomDef.color };
room.visible = false; // Initially hidden
scene.add(room);
fuajeeRooms.push(room);
roomData.push({
mesh: room,
name: roomDef.name,
originalColor: roomDef.color,
originalScale: room.scale.clone(),
view: "fuajee",
});
});
};
// Resize handler
const handleResize = () => {
const { width: newWidth, height: newHeight } = getResponsiveDimensions();
// Update camera
const newAspect = newWidth / newHeight;
const newFrustumSize = baseFrustumSize;
camera.left = (newFrustumSize * newAspect) / -2;
camera.right = (newFrustumSize * newAspect) / 2;
camera.top = newFrustumSize / 2;
camera.bottom = newFrustumSize / -2;
camera.updateProjectionMatrix();
// Update renderer
renderer.setSize(newWidth, newHeight);
};
// Add resize event listener
window.addEventListener("resize", handleResize);
// Mouse event handlers
const onMouseMove = (event: MouseEvent) => {
const rect = renderer.domElement.getBoundingClientRect();
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
// Update mouse position for tooltip
setMousePosition({ x: event.clientX, y: event.clientY });
// Handle mouse interactions based on current view
if (currentViewRef.current === "tudengimaja") {
// Update raycaster
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(rooms);
// Reset all tudengimaja rooms to original state
roomData
.filter((r) => r.view === "tudengimaja")
.forEach(({ mesh, originalColor, originalScale }) => {
(mesh.material as THREE.MeshLambertMaterial).color.setHex(
originalColor,
);
mesh.scale.copy(originalScale);
});
if (intersects.length > 0) {
const hoveredMesh = intersects[0].object as THREE.Mesh;
const roomInfo = roomData.find((r) => r.mesh === hoveredMesh);
if (roomInfo) {
// Apply hover effects
(hoveredMesh.material as THREE.MeshLambertMaterial).color.setHex(
0xffffff,
);
hoveredMesh.scale.multiplyScalar(1.02);
setHoveredRoom(roomInfo.name);
}
} else {
setHoveredRoom(null);
}
} else if (currentViewRef.current === "fuajee") {
// Update raycaster for fuajee rooms
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(fuajeeRooms);
// Reset all fuajee rooms to original state
roomData
.filter((r) => r.view === "fuajee")
.forEach(({ mesh, originalColor, originalScale }) => {
(mesh.material as THREE.MeshLambertMaterial).color.setHex(
originalColor,
);
mesh.scale.copy(originalScale);
});
if (intersects.length > 0) {
const hoveredMesh = intersects[0].object as THREE.Mesh;
const roomInfo = roomData.find((r) => r.mesh === hoveredMesh);
if (roomInfo) {
// Apply hover effects with better visibility
(hoveredMesh.material as THREE.MeshLambertMaterial).color.setHex(
0xffffff,
);
hoveredMesh.scale.multiplyScalar(1.1);
setHoveredRoom(roomInfo.name);
}
} else {
setHoveredRoom(null);
}
} else {
setHoveredRoom(null);
}
};
// Add mouse event listener
renderer.domElement.addEventListener("mousemove", onMouseMove);
// Function to switch camera views
const switchView = (view: "tudengimaja" | "fuajee") => {
const targetPosition = cameraPositions[view].position;
const targetLookAt = cameraPositions[view].lookAt;
// Animate camera transition
const startPosition = camera.position.clone();
const startLookAt = new THREE.Vector3();
camera.getWorldDirection(startLookAt);
startLookAt.multiplyScalar(-1).add(camera.position);
let progress = 0;
const animateCamera = () => {
progress += 0.05;
if (progress >= 1) {
progress = 1;
}
// Smooth interpolation
const easeProgress = 1 - Math.cos(progress * Math.PI * 0.5);
camera.position.lerpVectors(
startPosition,
targetPosition,
easeProgress,
);
const currentLookAt = new THREE.Vector3().lerpVectors(
startLookAt,
targetLookAt,
easeProgress,
);
camera.lookAt(currentLookAt);
if (progress < 1) {
requestAnimationFrame(animateCamera);
}
};
animateCamera();
// Reset hover state when switching views
setHoveredRoom(null);
// Reset all room states to original
roomData.forEach(({ mesh, originalColor, originalScale }) => {
(mesh.material as THREE.MeshLambertMaterial).color.setHex(
originalColor,
);
mesh.scale.copy(originalScale);
});
// Toggle visibility of objects based on view
if (view === "fuajee") {
tudengimajaObjects.forEach((obj) => (obj.visible = false));
if (fuajeeMesh) {
fuajeeMesh.visible = true;
}
fuajeeRooms.forEach((room) => (room.visible = true));
} else {
tudengimajaObjects.forEach((obj) => (obj.visible = true));
if (fuajeeMesh) {
fuajeeMesh.visible = false;
}
fuajeeRooms.forEach((room) => (room.visible = false));
// Re-apply divider visibility state
if (mountElement.toggleDividers) {
mountElement.toggleDividers(showDividers);
}
}
};
// Animation loop
const animate = () => {
requestAnimationFrame(animate);
// Gentle floating animation for rooms
if (currentViewRef.current === "tudengimaja") {
rooms.forEach((room, index) => {
const originalY = 0.25; // height / 2 for the room height of 0.5
const baseY = originalY + Math.sin(Date.now() * 0.001 + index) * 0.05;
// Maintain current scale while updating Y position
room.position.y = baseY;
});
} else if (currentViewRef.current === "fuajee") {
fuajeeRooms.forEach((room, index) => {
const originalY = 2.25; // height / 2 for the room height of 0.5 + 2 offset
const baseY = originalY + Math.sin(Date.now() * 0.001 + index) * 0.05;
// Maintain current scale while updating Y position
room.position.y = baseY;
});
}
renderer.render(scene, camera);
};
animate();
// Function to toggle dividers
const toggleDividers = (show: boolean) => {
dividersRef.forEach((divider) => {
divider.visible = show;
(divider.material as THREE.MeshLambertMaterial).opacity = show
? 0.4
: 0;
});
};
// Expose functions to parent scope
mountElement.toggleDividers = toggleDividers;
mountElement.switchView = switchView;
// Cleanup
return () => {
window.removeEventListener("resize", handleResize);
renderer.domElement.removeEventListener("mousemove", onMouseMove);
if (mountElement && renderer.domElement) {
mountElement.removeChild(renderer.domElement);
}
renderer.dispose();
};
}, [roomNames]);
// Update dividers when showDividers state changes
useEffect(() => {
if (mountRef.current?.toggleDividers) {
mountRef.current.toggleDividers(showDividers);
}
}, [showDividers]);
// Handle view switching
const handleViewSwitch = (view: "tudengimaja" | "fuajee") => {
setCurrentView(view);
currentViewRef.current = view; // Update ref immediately
setHoveredRoom(null); // Clear any existing hover state
if (mountRef.current?.switchView) {
mountRef.current.switchView(view);
}
};
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 text-[#2A2C3F] dark:text-[#EEE5E5] mt-8 md:mt-16 mb-4 uppercase`}
>
{t("expo.title")}
</h1>
<div className="mb-6">
<h2 className="text-2xl text-[#2A2C3F] dark:text-[#EEE5E5] mb-3">
{currentView === "tudengimaja"
? t("schedule.locations.studentHouse")
: t("schedule.locations.entranceHall")}
</h2>
{currentView === "tudengimaja" && (
<div className="flex flex-wrap gap-4 pb-4">
<div className="flex items-center gap-2">
<div
className="w-4 h-4 border border-gray-300"
style={{ backgroundColor: "#4ecdc4" }}
></div>
<span className="text-sm text-[#2A2C3F] dark:text-[#EEE5E5]">
{t("expo.areas.bar")}
</span>
</div>
<div className="flex items-center gap-2">
<div
className="w-4 h-4 border border-gray-300"
style={{ backgroundColor: "#ffe66d" }}
></div>
<span className="text-sm text-[#2A2C3F] dark:text-[#EEE5E5]">
EVAL
</span>
</div>
<div className="flex items-center gap-2">
<div
className="w-4 h-4 border border-gray-300"
style={{ backgroundColor: "#343434" }}
></div>
<span className="text-sm text-[#2A2C3F] dark:text-[#EEE5E5]">
{t("expo.areas.boardGames")}
</span>
</div>
<div className="flex items-center gap-2">
<div
className="w-4 h-4 border border-gray-300"
style={{ backgroundColor: "#080682" }}
></div>
<span className="text-sm text-[#2A2C3F] dark:text-[#EEE5E5]">
LVLup!
</span>
</div>
<div className="flex items-center gap-2">
<div
className="w-4 h-4 border border-gray-300"
style={{ backgroundColor: "#C02841" }}
></div>
<span className="text-sm text-[#2A2C3F] dark:text-[#EEE5E5]">
Red Bull
</span>
</div>
<div className="flex items-center gap-2">
<div
className="w-4 h-4 border border-gray-300"
style={{ backgroundColor: "#ff6600" }}
></div>
<span className="text-sm text-[#2A2C3F] dark:text-[#EEE5E5]">
{t("expo.areas.simRacing")}
</span>
</div>
<div className="items-center gap-2 hidden">
<div
className="w-4 h-4 border border-gray-300"
style={{ backgroundColor: "#3498db" }}
></div>
<span className="text-sm text-[#2A2C3F] dark:text-[#EEE5E5]">
Sony
</span>
</div>
<div className="flex items-center gap-2">
<div
className="w-4 h-4 border border-gray-300"
style={{ backgroundColor: "#ff1493" }}
></div>
<span className="text-sm text-[#2A2C3F] dark:text-[#EEE5E5]">
{t("expo.areas.fighting")}
</span>
</div>
</div>
)}
{currentView === "fuajee" && (
<div className="flex flex-wrap gap-4 pb-4">
<div className="flex items-center gap-2">
<div
className="w-4 h-4 border border-gray-300"
style={{ backgroundColor: "#7b1642" }}
></div>
<span className="text-sm text-[#2A2C3F] dark:text-[#EEE5E5]">
{t("expo.areas.ityk")}
</span>
</div>
<div className="flex items-center gap-2">
<div
className="w-4 h-4 border border-gray-300"
style={{ backgroundColor: "#365591" }}
></div>
<span className="text-sm text-[#2A2C3F] dark:text-[#EEE5E5]">
{t("expo.areas.tartuyk")}
</span>
</div>
<div className="flex items-center gap-2">
<div
className="w-4 h-4 border border-gray-300"
style={{ backgroundColor: "#183bbf" }}
></div>
<span className="text-sm text-[#2A2C3F] dark:text-[#EEE5E5]">
{t("expo.areas.estoniagamedev")}
</span>
</div>
<div className="flex items-center gap-2">
<div
className="w-4 h-4 border border-gray-300"
style={{ backgroundColor: "#a82838" }}
></div>
<span className="text-sm text-[#2A2C3F] dark:text-[#EEE5E5]">
{t("expo.areas.tly")}
</span>
</div>
<div className="flex items-center gap-2">
<div
className="w-4 h-4 border border-gray-300"
style={{ backgroundColor: "#d12e7d" }}
></div>
<span className="text-sm text-[#2A2C3F] dark:text-[#EEE5E5]">
{t("expo.areas.ittk")}
</span>
</div>
<div className="flex items-center gap-2">
<div
className="w-4 h-4 border border-gray-300"
style={{ backgroundColor: "#ff6347" }}
></div>
<span className="text-sm text-[#2A2C3F] dark:text-[#EEE5E5]">
{t("expo.areas.info")}
</span>
</div>
<div className="flex items-center gap-2">
<div
className="w-4 h-4 border border-gray-300"
style={{ backgroundColor: "#228b22" }}
></div>
<span className="text-sm text-[#2A2C3F] dark:text-[#EEE5E5]">
{t("expo.areas.gameup")}
</span>
</div>
<div className="flex items-center gap-2">
<div
className="w-4 h-4 border border-gray-300"
style={{ backgroundColor: "#20b2aa" }}
></div>
<span className="text-sm text-[#2A2C3F] dark:text-[#EEE5E5]">
{t("expo.areas.photobooth")}
</span>
</div>
</div>
)}
<div className="flex flex-col lg:flex-row gap-8 items-start">
<div className="relative w-full max-w-[800px]">
<div className="flex-shrink-0 border-3 border-[#1F5673] w-full relative">
<div ref={mountRef} className="w-full" />
{/* Left Arrow - Only show when on fuajee to go back to tudengimaja */}
{currentView === "fuajee" && (
<button
onClick={() => handleViewSwitch("tudengimaja")}
className="group absolute left-4 bottom-4 p-4 md:p-6 transition-all duration-300 hover:scale-110 z-20 touch-manipulation min-h-[48px] min-w-[48px] flex items-center justify-center"
title="Switch to Tudengimaja"
aria-label="Switch to Tudengimaja view"
>
<span className="material-symbols-outlined !text-[clamp(2.5rem,2rem+2vw,4rem)] !font-bold text-[#007CAB] dark:text-[#00A3E0] group-hover:-translate-x-2 dark:group-hover:text-[#EEE5E5] group-hover:text-[#EEE5E5] transition transform rotate-180">
arrow_right_alt
</span>
</button>
)}
{/* Right Arrow - Only show when on tudengimaja to go to fuajee */}
{currentView === "tudengimaja" && (
<button
onClick={() => handleViewSwitch("fuajee")}
className="group absolute right-4 bottom-4 p-4 md:p-6 transition-all duration-300 hover:scale-110 z-20 touch-manipulation min-h-[48px] min-w-[48px] flex items-center justify-center"
title="Switch to Fuajee"
aria-label="Switch to Fuajee view"
>
<span className="material-symbols-outlined !text-[clamp(2.5rem,2rem+2vw,4rem)] !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>
</button>
)}
{currentView === "tudengimaja" && (
<button
onClick={() => setShowDividers(!showDividers)}
className={`absolute top-2 right-2 px-3 py-2 bg-[#1F5673] text-white hover:bg-[#2A7A9B] ${vipnagorgialla.className} uppercase italic text-sm font-semibold flex items-center transition-colors shadow-lg z-10`}
>
{showDividers ? (
<EyeClosed className="w-6 h-6 mr-2" />
) : (
<Eye className="w-6 h-6 mr-2" />
)}
{showDividers ? t("expo.hide") : t("expo.show")}
</button>
)}
</div>
</div>
</div>
{/* Tooltip - only show for current view */}
{hoveredRoom &&
((currentView === "tudengimaja" &&
[
roomNames.boardGames,
roomNames.bar,
roomNames.eval,
roomNames.simRacing,
roomNames.fighting,
roomNames.lvlup,
roomNames.redbull,
].includes(hoveredRoom)) ||
(currentView === "fuajee" &&
[
roomNames.ityk,
roomNames.tartuyk,
roomNames.estoniagamedev,
roomNames.info,
roomNames.tly,
roomNames.ittk,
roomNames.photobooth,
roomNames.gameup,
].includes(hoveredRoom))) && (
<div
className="fixed bg-black bg-opacity-80 text-white px-3 py-2 rounded-lg text-sm pointer-events-none z-50"
style={{
left: mousePosition.x + 10,
top: mousePosition.y - 10,
}}
>
{hoveredRoom}
</div>
)}
</div>
</div>
<SectionDivider />
</div>
);
}

@ -0,0 +1,19 @@
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`}
>
{t("title")}
</h1>
<p className="text-2xl text-[#2A2C3F] dark:text-[#EEE5E5] mb-8">
{t("message")}
</p>
</div>
);
}

@ -1,8 +1,18 @@
import { vipnagorgialla } from "@/components/Vipnagorgialla"; import { vipnagorgialla } from "@/components/Vipnagorgialla";
import Link from "next/link"; import { Link } from "@/i18n/routing";
import { getTranslations, setRequestLocale } from "next-intl/server";
import Image from "next/image"; import Image from "next/image";
import NextLink from "next/link";
export default async function Home({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
setRequestLocale(locale);
const t = await getTranslations({ locale });
export default function Home() {
return ( return (
<div> <div>
{/* Title */} {/* Title */}
@ -25,7 +35,7 @@ export default function Home() {
<h3 <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]`} className={`text-[clamp(1.25rem,0.75rem+2.5vw,3.75rem)] ${vipnagorgialla.className} leading-[90%] font-bold italic uppercase dark:text-[#EEE5E5] text-[#2A2C3F]`}
> >
Auhinnafond {t("tournaments.prizePool")}
</h3> </h3>
<h2 <h2
className={`text-[clamp(2rem,1.2rem+4vw,6rem)] ${vipnagorgialla.className} leading-[90%] font-bold italic text-[#007CAB] dark:text-[#00A3E0]`} className={`text-[clamp(2rem,1.2rem+4vw,6rem)] ${vipnagorgialla.className} leading-[90%] font-bold italic text-[#007CAB] dark:text-[#00A3E0]`}
@ -44,7 +54,7 @@ export default function Home() {
<h2 <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]`} 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]`}
> >
Ajakava {t("navigation.schedule")}
</h2> </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"> <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 arrow_right_alt
@ -55,8 +65,7 @@ export default function Home() {
event_note event_note
</span> </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"> <p className="text-[clamp(0.875rem,0.75rem+0.5vw,1.25rem)] tracking-[-0.045rem] dark:group-hover:text-[#2A2C3F] group-hover:text-black">
TipiLAN on pungil põnevatest turniiridest, mini-võistlustest ja {t("home.sections.schedule.description")}
paljust muust.
</p> </p>
</div> </div>
</Link> </Link>
@ -66,9 +75,9 @@ export default function Home() {
> >
<div className="cursor-pointer flex flex-row justify-between gap-4 items-center"> <div className="cursor-pointer flex flex-row justify-between gap-4 items-center">
<h2 <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`} className={`text-[clamp(2rem,1.8rem+1vw,3rem)] ${vipnagorgialla.className} font-bold italic break-all uppercase dark:text-[#EEE5E5] text-[#2A2C3F] dark:group-hover:text-[#2A2C3F] group-hover:text-black`}
> >
Turniirid {t("navigation.tournaments")}
</h2> </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"> <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 arrow_right_alt
@ -80,8 +89,7 @@ export default function Home() {
trophy trophy
</span> </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"> <p className="text-[clamp(0.875rem,0.75rem+0.5vw,1.25rem)] tracking-[-0.045rem] dark:group-hover:text-[#2A2C3F] group-hover:text-black">
TipiLANil toimuvad suurejoonelised CS2 ja LoL turniirid, mille {t("home.sections.tournaments.description")}
auhinnafond on 10 000.
</p> </p>
</div> </div>
</Link> </Link>
@ -93,7 +101,7 @@ export default function Home() {
<h2 <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`} 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`}
> >
Messiala {t("navigation.expo")}
</h2> </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"> <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 arrow_right_alt
@ -104,8 +112,7 @@ export default function Home() {
weekend weekend
</span> </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"> <p className="text-[clamp(0.875rem,0.75rem+0.5vw,1.25rem)] tracking-[-0.045rem] dark:group-hover:text-[#2A2C3F] group-hover:text-black">
TipiLANi messialal paiknevad ettevõtted, lisategevused ja toimuvad {t("home.sections.expo.description")}
loengud.
</p> </p>
</div> </div>
</Link> </Link>
@ -117,7 +124,7 @@ export default function Home() {
> >
<div className="cursor-pointer text-left flex flex-row justify-between xl:justify-start gap-8"> <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"> <h3 className="text-4xl md:text-5xl dark:text-[#EEE5E5] dark:group-hover:text-[#2A2C3F] text-[#2A2C3F] group-hover:text-black">
Bro&shy;neeri oma koht juba täna! {t("home.sections.reserveSpot")}
</h3> </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"> <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 arrow_right_alt
@ -133,10 +140,10 @@ export default function Home() {
> >
<div className="text-left flex flex-col justify-between xl:justify-start"> <div className="text-left flex flex-col justify-between xl:justify-start">
<h3 className="text-4xl md:text-5xl dark:text-[#EEE5E5] text-[#2A2C3F] group-hover:text-black pb-8"> <h3 className="text-4xl md:text-5xl dark:text-[#EEE5E5] text-[#2A2C3F] group-hover:text-black pb-8">
TipiLANi tõmbab käima... {t("home.sections.poweredBy")}
</h3> </h3>
<div className="flex flex-row flex-wrap gap-8 md:gap-18 items-center"> <div className="flex flex-row flex-wrap gap-8 md:gap-18 items-center justify-center">
<Link href="https://taltech.ee" target="_blank"> <NextLink href="https://taltech.ee" target="_blank">
<Image <Image
src="/sponsors/taltech-color.png" src="/sponsors/taltech-color.png"
alt="Taltech (Tallinna Tehnikaülikool)" alt="Taltech (Tallinna Tehnikaülikool)"
@ -144,8 +151,8 @@ export default function Home() {
height={192} height={192}
className="object-contain" className="object-contain"
/> />
</Link> </NextLink>
<Link href="https://www.redbull.com/ee-et/" target="_blank"> <NextLink href="https://www.redbull.com/ee-et/" target="_blank">
<Image <Image
src="/sponsors/redbull.png" src="/sponsors/redbull.png"
alt="Redbull" alt="Redbull"
@ -153,8 +160,8 @@ export default function Home() {
height={80} height={80}
className="object-contain" className="object-contain"
/> />
</Link> </NextLink>
<Link href="https://www.alecoq.ee" target="_blank"> <NextLink href="https://www.alecoq.ee" target="_blank">
<Image <Image
src="/sponsors/alecoq.svg" src="/sponsors/alecoq.svg"
alt="Alecoq" alt="Alecoq"
@ -162,8 +169,8 @@ export default function Home() {
height={200} height={200}
className="object-contain" className="object-contain"
/> />
</Link> </NextLink>
<Link href="https://www.simracing.ee/" target="_blank"> <NextLink href="https://www.simracing.ee/" target="_blank">
<Image <Image
src="/sponsors/EVAL.png" src="/sponsors/EVAL.png"
alt="EVAL" alt="EVAL"
@ -171,8 +178,8 @@ export default function Home() {
height={200} height={200}
className="object-contain" className="object-contain"
/> />
</Link> </NextLink>
<Link href="https://balsnack.ee" target="_blank"> <NextLink href="https://balsnack.ee" target="_blank">
<Image <Image
src="/sponsors/balsnack.svg" src="/sponsors/balsnack.svg"
alt="Balsnack" alt="Balsnack"
@ -180,8 +187,8 @@ export default function Home() {
height={200} height={200}
className="object-contain" className="object-contain"
/> />
</Link> </NextLink>
<Link <NextLink
href="https://www.rara.ee/sundmused/interaktiivne-videomangude-muuseum-lvlup/" href="https://www.rara.ee/sundmused/interaktiivne-videomangude-muuseum-lvlup/"
target="_blank" target="_blank"
> >
@ -192,8 +199,11 @@ export default function Home() {
height={192} height={192}
className="object-contain" className="object-contain"
/> />
</Link> </NextLink>
<Link href="https://www.facebook.com/bfglOfficial" target="_blank"> <NextLink
href="https://www.facebook.com/bfglOfficial"
target="_blank"
>
<Image <Image
src="/sponsors/BFGL.png" src="/sponsors/BFGL.png"
alt="BFGL" alt="BFGL"
@ -201,7 +211,16 @@ export default function Home() {
height={192} height={192}
className="object-contain" className="object-contain"
/> />
</Link> </NextLink>
<NextLink href="https://www.tallinn.ee/et/haridus" target="_blank">
<Image
src="/sponsors/tallinnaharidusamet.jpg"
alt="Tallinna Haridusamet"
width={192}
height={192}
className="object-contain"
/>
</NextLink>
</div> </div>
</div> </div>
</div> </div>

@ -0,0 +1,114 @@
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 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 italic text-[#EEE5E5] pb-2`}
>
{t("tickets.computerParticipant.price")}
</h2>
<h3
className={`text-3xl ${vipnagorgialla.className} font-bold italic 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 italic">
{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 italic`}
>
{t("tickets.buyTicket")}
</button>
</Link>
</div>
<div className="bg-[#1F5673] -skew-x-2 md:-skew-x-5 text-white 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 italic text-[#EEE5E5] pb-2`}
>
{t("tickets.competitor.price")}
</h2>
<h3
className={`text-3xl ${vipnagorgialla.className} font-bold italic text-[#EEE5E5] pb-4`}
>
{t("tickets.competitor.title")}
</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-xl">
{feature}
</li>
))}
</ul>
<Link href="https://fienta.com/et/tipilan" target="_blank">
<button
className={`px-4 py-2 bg-[#007CAB] cursor-pointer ${vipnagorgialla.className} font-bold italic`}
>
{t("tickets.buyTicket")}
</button>
</Link>
</div>
<div className="bg-[#007CAB] -skew-x-2 md:-skew-x-5 text-white 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 italic text-[#EEE5E5] pb-2`}
>
{t("tickets.visitor.price")}
</h2>
<h3
className={`text-3xl ${vipnagorgialla.className} font-bold italic 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 italic`}
>
{t("tickets.buyTicket")}
</button>
</Link>
</div>
</div>
</div>
<SectionDivider />
</div>
);
}

@ -0,0 +1,106 @@
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";
// Map of valid slugs to their corresponding file paths and translation keys
const rulesMap = {
lol: {
filePath: "src/data/rules/lol.md",
titleKey: "rules.lolRules",
},
cs2: {
filePath: "src/data/rules/cs2.md",
titleKey: "rules.cs2Rules",
},
} as const;
type RuleSlug = keyof typeof rulesMap;
interface PageProps {
params: Promise<{ slug: string; locale: string }>;
}
async function getRuleContent(slug: string) {
if (!Object.keys(rulesMap).includes(slug)) {
return null;
}
const ruleConfig = rulesMap[slug as RuleSlug];
try {
const file = Bun.file(ruleConfig.filePath);
const content = await file.text();
return {
content,
titleKey: ruleConfig.titleKey,
};
} catch (error) {
console.error(`Error reading rule file for slug ${slug}:`, 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);
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,
}));
}

@ -0,0 +1,63 @@
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-3xl ${vipnagorgialla.className} font-bold uppercase text-[#EEE5E5] pb-2`;
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="">*/}
<div className={`${boxStyle} bg-[#1F5673] py-16 px-8`}>
<h2 className={`${boxTextStyle}`}>
{t("rules.miniRules")}
</h2>
</div>
{/*</Link>*/}
</div>
</div>
<SectionDivider />
</div>
);
}

@ -0,0 +1,224 @@
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 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 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]">
24.-26. okt.
</h2>
</Link>
{/* Sponsors */}
<div
className={`p-12 flex flex-col ${vipnagorgialla.className} font-bold italic border-b-3 border-[#1F5673]`}
>
<div className="text-left flex flex-col justify-between xl:justify-start">
<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-row flex-wrap gap-8 md:gap-18 items-center justify-center">
<Link href="https://taltech.ee" target="_blank">
<Image
src="/sponsors/taltech-color.png"
alt="Taltech (Tallinna Tehnikaülikool)"
width={192}
height={192}
className="object-contain"
/>
</Link>
<Link href="https://www.redbull.com/ee-et/" target="_blank">
<Image
src="/sponsors/redbull.png"
alt="Redbull"
width={80}
height={80}
className="object-contain"
/>
</Link>
<Link href="https://www.alecoq.ee" target="_blank">
<Image
src="/sponsors/alecoq.svg"
alt="Alecoq"
width={200}
height={200}
className="object-contain"
/>
</Link>
<Link href="https://www.simracing.ee/" target="_blank">
<Image
src="/sponsors/EVAL.png"
alt="EVAL"
width={200}
height={200}
className="object-contain"
/>
</Link>
<Link href="https://balsnack.ee" target="_blank">
<Image
src="/sponsors/balsnack.svg"
alt="Balsnack"
width={200}
height={200}
className="object-contain"
/>
</Link>
<Link
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"
/>
</Link>
<Link href="https://www.facebook.com/bfglOfficial" target="_blank">
<Image
src="/sponsors/BFGL.png"
alt="BFGL"
width={192}
height={192}
className="object-contain"
/>
</Link>
<Link href="https://www.tallinn.ee/et/haridus" target="_blank">
<Image
src="/sponsors/tallinnaharidusamet.jpg"
alt="Tallinna Haridusamet"
width={192}
height={192}
className="object-contain"
/>
</Link>
</div>
</div>
</div>
</div>
);
}

@ -0,0 +1,188 @@
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`;
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] md:m-16`}
>
{t("tournaments.title")}
</h1>
<div className="flex flex-col">
{/* 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>
<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-[#007CAB] 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-[#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-[#007CAB] group-hover:bg-black cursor-pointer ${vipnagorgialla.className} font-bold italic text-[#ECE5E5]`}
>
{t("tournaments.lol.buyTicket")}
</button>
</a>
</div>
</div>
</div>
</div>
{/* Mini-turniirid */}
<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="-skew-x-2 md:-skew-x-5">
<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>
<br />
<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-[#007CAB] group-hover:bg-black cursor-pointer ${vipnagorgialla.className} font-bold italic text-[#ECE5E5]`}
>
{t("tournaments.mini.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/minitournament_logo.png"
alt="mini tournaments"
width={600}
height={400}
/>
</div>
</div>
</div>
</div>
</div>
</div>
);
}

@ -1,72 +0,0 @@
"use client";
import { useState } from "react";
import { vipnagorgialla } from "@/components/Vipnagorgialla";
import { scheduleData } from "@/data/timetable";
const tabs = Object.keys(scheduleData);
export default function Timetable() {
const [activeTab, setActiveTab] = useState(tabs[0]);
const schedule = scheduleData[activeTab];
return (
<div className="flex flex-col min-h-[90vh] m-6 mt-16 md:m-16">
<h1
className={`text-5xl sm:text-6xl ${vipnagorgialla.className} font-bold italic uppercase text-[#2A2C3F] dark:text-[#EEE5E5] mt-8 md:mt-16 mb-8`}
>
Ajakava
</h1>
{/* Tab menu */}
<div className="flex space-x-4 mb-8">
{tabs.map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={`${vipnagorgialla.className} 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`}
>
{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-row gap-12"
>
<div
className={` ${vipnagorgialla.className} text-[#00A3E0] text-5xl font-bold italic`}
>
{item.time}
</div>
<div>
<div
className={`${vipnagorgialla.className} text-4xl italic font-bold text-[#2A2C3F] dark:text-[#EEE5E5]`}
>
{item.title}
</div>
{item.description && (
<div className="text-2xl text-[#938BA1] dark:text-[#938BA1]">
{item.description}
</div>
)}
{item.location && (
<div className="text-2xl text-[#938BA1] dark:text-[#938BA1]">
{item.location}
</div>
)}
</div>
</div>
))}
</div>
</div>
);
}

@ -1,63 +0,0 @@
import { vipnagorgialla } from "@/components/Vipnagorgialla";
export default function Rulebook() {
return (
<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 text-[#2A2C3F] dark:text-[#EEE5E5] mt-8 md:mt-16 mb-4 uppercase`}
>
Kodukord
</h1>
<ol className="list-decimal ml-6 md:text-xl text-[#2A2C3F] dark:text-[#EEE5E5] y-4">
<li>
Keelatud on:
<ol className="list-[lower-alpha] ml-6 y-2">
<li>alkoholi ja uimastite omamine ja tarbimine ürituse vältel</li>
<li>alkoholijoobes või uimastite mõju all viibimine üritusel</li>
<li>
suitsetamine (ka e-sigaret) selleks mitte ettenähtud kohtades
suitsetada võib suitsuruumis või õues vastava prügikasti juures
<ul className="list-disc ml-6">
<li>suitsetamine alaealistel</li>
<li>mokatubaka kasutamine TalTech-i territooriumil</li>
</ul>
</li>
<li>külm- ja imitatsioonrelvad</li>
<li>
ürituse alal igasuguse vägivalla kasutamine teiste ja teiste vara
suhtes
</li>
<li>teiste vara omavoliline kasutamine, näppimine</li>
<li>turniiri reeglitele mittevastavalt mängimine</li>
<li>
omavoliline taristu (võrgu) näppimine võrguprobleemidega tuleb
pöörduda korraldajate poole
</li>
<li>
mängimiseks ebavajalike seadmete ühendamine vooluvõrku (nt
veekeetja, puhur, sirgendaja)
</li>
</ol>
</li>
<li>Iga külastaja vastutab enda asjade ja vara eest ise</li>
<li>
Korraldajale varalise kahju tekitanud külastaja on kohustatud korvama
täies ulatuses tekitatud kahju
</li>
<li>
Magamiseks ettenähtud ajal ja magamiseks ettenähtud ruumis tuleb olla
vaikselt ja võimaldada kaas mängijatel magada
</li>
<li>Korraldajad ei vastuta külastajate eest</li>
<li>
Mängijad on TipiLAN-il kohustatud kinni pidama mängule seatud
vanusepiirangutest
</li>
</ol>
<p className="md:text-xl text-[#2A2C3F] dark:text-[#EEE5E5] y-4 mt-4">
NB! Reeglite rikkumise puhul on korraldajatel õigus mängija (koos tema
meeskonnaga) eemaldada ja rakendada edasist keeldu TipiLAN-i üritustelt.
</p>
</div>
);
}

@ -1,18 +1,8 @@
// Head metadata
import type { Metadata } from "next"; import type { Metadata } from "next";
import Head from "next/head"; import { Work_Sans } from "next/font/google";
// Provides the theme context to the app
import { ThemeProvider } from "@/components/Theme-provider";
import "./globals.css"; import "./globals.css";
import "material-symbols"; import "material-symbols";
// Fonts
import { Work_Sans } from "next/font/google";
import SidebarParent from "@/components/SidebarParent";
import Footer from "@/components/Footer";
const workSans = Work_Sans({ const workSans = Work_Sans({
subsets: ["latin"], subsets: ["latin"],
}); });
@ -24,32 +14,15 @@ export const metadata: Metadata = {
export default function RootLayout({ export default function RootLayout({
children, children,
}: Readonly<{ }: {
children: React.ReactNode; children: React.ReactNode;
}>) { }) {
return ( return (
<html lang="en" suppressHydrationWarning> <html suppressHydrationWarning>
<Head>
<title>TipiLAN</title>
<meta property="og:title" content="TipiLAN 2025" key="title" />
<meta
name="description"
content="TipiLAN 2025 – Eesti suurim tudengite korraldatud LAN!"
/>
</Head>
<body <body
className={`${workSans.className} antialiased bg-[#EEE5E5] dark:bg-[#0E0F19]`} className={`${workSans.className} antialiased bg-[#EEE5E5] dark:bg-[#0E0F19]`}
> >
<ThemeProvider {children}
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<SidebarParent />
{children}
<Footer />
</ThemeProvider>
</body> </body>
</html> </html>
); );

@ -1,480 +0,0 @@
"use client";
import { vipnagorgialla } from "@/components/Vipnagorgialla";
import * as THREE from "three";
import { useEffect, useRef, useState } from "react";
import { EyeClosed, Eye } from "lucide-react";
// Define interface for the ref with toggle function
interface MountRefCurrent extends HTMLDivElement {
toggleDividers?: (show: boolean) => void;
}
export default function Expo() {
const mountRef = useRef<MountRefCurrent | null>(null);
const [hoveredRoom, setHoveredRoom] = useState<string | null>(null);
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
const [showDividers, setShowDividers] = useState<boolean>(true);
useEffect(() => {
if (!mountRef.current) return;
// Copy ref to variable to avoid stale closure in cleanup
const mountElement = mountRef.current;
let dividersRef: THREE.Mesh[] = [];
// Scene setup
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0e0f19);
// Get responsive dimensions
const getResponsiveDimensions = () => {
const container = mountRef.current;
if (!container) return { width: 800, height: 600 };
const containerWidth = container.offsetWidth;
const maxWidth = Math.min(containerWidth, 800);
const width = Math.max(maxWidth, 300); // Minimum width
const height = (width * 600) / 800; // Maintain aspect ratio
return { width, height };
};
const { width, height } = getResponsiveDimensions();
// Isometric camera setup with responsive sizing
const aspect = width / height;
const baseFrustumSize = 14;
const frustumSize = width < 600 ? baseFrustumSize * 0.8 : baseFrustumSize; // Smaller frustum for mobile
const camera = new THREE.OrthographicCamera(
(frustumSize * aspect) / -2,
(frustumSize * aspect) / 2,
frustumSize / 2,
frustumSize / -2,
1,
1000,
);
// Position camera for isometric view
camera.position.set(10, 10, 14);
camera.lookAt(-1.4, 0, 0);
// Renderer
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(width, height);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
mountElement.appendChild(renderer.domElement);
// Raycaster for mouse interactions
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
// Lighting
const ambientLight = new THREE.AmbientLight(0x404040, 1.2);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5);
directionalLight.position.set(10, 10, 5);
directionalLight.castShadow = false;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
scene.add(directionalLight);
// Room colors and names
const roomColors = [
0xff6b35, // Orange - Mänguklubi
0x4ecdc4, // Turquoise - Baariala
0xffe66d, // Yellow - EVAL
0xe74c3c, // Red - Redbull
0x9b59b6, // Purple - Võitlusmängu ala
0x3498db, // Blue - Sony
0x2ecc71, // Green - Chillimisala
];
const roomNames = [
"Mänguklubi",
"Baariala",
"EVAL",
"Redbull",
"Võitlusmängu ala",
"Sony",
"Chillimisala",
];
// Create individual rooms as rectangles with custom positions
const rooms: THREE.Mesh[] = [];
const roomData: Array<{
mesh: THREE.Mesh;
name: string;
originalColor: number;
originalScale: THREE.Vector3;
}> = [];
const dividers: THREE.Mesh[] = [];
// Define rooms with custom positions, sizes and colors
const roomDefinitions = [
{
width: 7,
height: 0.7,
depth: 3,
x: 2.5,
z: 4,
color: roomColors[0],
name: roomNames[0],
}, // Mänguklubi
// {
// width: 2.5,
// height: 0.7,
// depth: 0.7,
// x: 1,
// z: 0,
// color: roomColors[1],
// name: roomNames[1],
// }, // Baariala
{
width: 1.8,
height: 0.7,
depth: 1.5,
x: 2.5,
z: -3.5,
color: roomColors[2],
name: roomNames[2],
}, // EVAL
{
width: 2.2,
height: 0.7,
depth: 4.5,
x: 5,
z: -2,
color: roomColors[3],
name: roomNames[3],
}, // Redbull
{
width: 3,
height: 0.7,
depth: 1.3,
x: 0,
z: -3.5,
color: roomColors[4],
name: roomNames[4],
}, // Võitlusmängu ala
{
width: 1.8,
height: 0.7,
depth: 1.5,
x: -2.55,
z: -3.5,
color: roomColors[5],
name: roomNames[5],
}, // Sony
{
width: 4,
height: 0.7,
depth: 4,
x: -5.5,
z: -2.3,
color: roomColors[6],
name: roomNames[6],
}, // Chillimisala
];
roomDefinitions.forEach((roomDef) => {
const geometry = new THREE.BoxGeometry(
roomDef.width,
roomDef.height,
roomDef.depth,
);
const material = new THREE.MeshLambertMaterial({
color: roomDef.color,
});
const room = new THREE.Mesh(geometry, material);
room.position.set(roomDef.x, roomDef.height / 2, roomDef.z);
room.castShadow = true;
room.receiveShadow = true;
room.userData = { name: roomDef.name, originalColor: roomDef.color };
scene.add(room);
rooms.push(room);
roomData.push({
mesh: room,
name: roomDef.name,
originalColor: roomDef.color,
originalScale: room.scale.clone(),
});
});
// Create toggleable room dividers
const createTogglableDivider = (
width: number,
height: number,
depth: number,
x: number,
z: number,
) => {
const wallGeometry = new THREE.BoxGeometry(width, height, depth);
const wallMaterial = new THREE.MeshLambertMaterial({
color: 0x555555,
transparent: true,
opacity: 0,
});
const wall = new THREE.Mesh(wallGeometry, wallMaterial);
wall.position.set(x, height / 2, z);
wall.visible = false;
scene.add(wall);
dividers.push(wall);
};
// Add strategic dividers between major areas
createTogglableDivider(10, 2, 2, -2.5, 1.5); // Wall between main entrance
createTogglableDivider(2, 2, 2, 5.5, 1.5); // Wall right next to Mänguklubi & Redbull
// Store dividers reference for later access
dividersRef = [...dividers];
// Ground plane
const groundGeometry = new THREE.PlaneGeometry(14, 10.5);
const groundMaterial = new THREE.MeshLambertMaterial({ color: 0xcccccc });
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
ground.position.x = -1.1;
ground.position.y = -0.5;
ground.receiveShadow = true;
scene.add(ground);
// Second ground plane
const groundGeometry2 = new THREE.PlaneGeometry(2, 7);
const groundMaterial2 = new THREE.MeshLambertMaterial({
color: 0xcccccc,
});
const ground2 = new THREE.Mesh(groundGeometry2, groundMaterial2);
ground2.rotation.x = -Math.PI / 2;
ground2.position.x = -12.2;
ground2.position.y = -5;
ground2.receiveShadow = true;
scene.add(ground2);
// Resize handler
const handleResize = () => {
const { width: newWidth, height: newHeight } = getResponsiveDimensions();
// Update camera
const newAspect = newWidth / newHeight;
const newFrustumSize =
newWidth < 600 ? baseFrustumSize * 0.8 : baseFrustumSize;
camera.left = (newFrustumSize * newAspect) / -2;
camera.right = (newFrustumSize * newAspect) / 2;
camera.top = newFrustumSize / 2;
camera.bottom = newFrustumSize / -2;
camera.updateProjectionMatrix();
// Update renderer
renderer.setSize(newWidth, newHeight);
};
// Add resize event listener
window.addEventListener("resize", handleResize);
// Mouse event handlers
const onMouseMove = (event: MouseEvent) => {
const rect = renderer.domElement.getBoundingClientRect();
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
// Update mouse position for tooltip
setMousePosition({ x: event.clientX, y: event.clientY });
// Update raycaster
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(rooms);
// Reset all rooms to original state
roomData.forEach(({ mesh, originalColor, originalScale }) => {
(mesh.material as THREE.MeshLambertMaterial).color.setHex(
originalColor,
);
mesh.scale.copy(originalScale);
});
if (intersects.length > 0) {
const hoveredMesh = intersects[0].object as THREE.Mesh;
const roomInfo = roomData.find((r) => r.mesh === hoveredMesh);
if (roomInfo) {
// Apply hover effects
(hoveredMesh.material as THREE.MeshLambertMaterial).color.setHex(
0xffffff,
);
hoveredMesh.scale.multiplyScalar(1.02);
setHoveredRoom(roomInfo.name);
}
} else {
setHoveredRoom(null);
}
};
// Add mouse event listener
renderer.domElement.addEventListener("mousemove", onMouseMove);
// Animation loop
const animate = () => {
requestAnimationFrame(animate);
// Gentle floating animation for rooms
rooms.forEach((room, index) => {
const originalY = 0.25; // height / 2 for the room height of 0.5
const baseY = originalY + Math.sin(Date.now() * 0.001 + index) * 0.05;
// Maintain current scale while updating Y position
room.position.y = baseY;
});
renderer.render(scene, camera);
};
animate();
// Function to toggle dividers
const toggleDividers = (show: boolean) => {
dividersRef.forEach((divider) => {
divider.visible = show;
(divider.material as THREE.MeshLambertMaterial).opacity = show
? 0.4
: 0;
});
};
// Expose toggle function to parent scope
mountElement.toggleDividers = toggleDividers;
// Cleanup
return () => {
window.removeEventListener("resize", handleResize);
renderer.domElement.removeEventListener("mousemove", onMouseMove);
if (mountElement && renderer.domElement) {
mountElement.removeChild(renderer.domElement);
}
renderer.dispose();
};
}, []);
// Update dividers when showDividers state changes
useEffect(() => {
if (mountRef.current?.toggleDividers) {
mountRef.current.toggleDividers(showDividers);
}
}, [showDividers]);
return (
<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 text-[#2A2C3F] dark:text-[#EEE5E5] mt-8 md:mt-16 mb-4 uppercase`}
>
Messiala
</h1>
<div className="mb-6">
<h2 className="text-2xl text-[#2A2C3F] dark:text-[#EEE5E5] mb-3">
Tudengimaja
</h2>
<div className="flex flex-wrap gap-4 pb-4">
<div className="flex items-center gap-2">
<div
className="w-4 h-4 border border-gray-300"
style={{ backgroundColor: "#ff6b35" }}
></div>
<span className="text-sm text-[#2A2C3F] dark:text-[#EEE5E5]">
Mänguklubi
</span>
</div>
<div className="items-center gap-2 hidden">
<div
className="w-4 h-4 border border-gray-300"
style={{ backgroundColor: "#4ecdc4" }}
></div>
<span className="text-sm text-[#2A2C3F] dark:text-[#EEE5E5]">
Baariala
</span>
</div>
<div className="flex items-center gap-2">
<div
className="w-4 h-4 border border-gray-300"
style={{ backgroundColor: "#ffe66d" }}
></div>
<span className="text-sm text-[#2A2C3F] dark:text-[#EEE5E5]">
EVAL
</span>
</div>
<div className="flex items-center gap-2">
<div
className="w-4 h-4 border border-gray-300"
style={{ backgroundColor: "#e74c3c" }}
></div>
<span className="text-sm text-[#2A2C3F] dark:text-[#EEE5E5]">
Redbull
</span>
</div>
<div className="flex items-center gap-2">
<div
className="w-4 h-4 border border-gray-300"
style={{ backgroundColor: "#9b59b6" }}
></div>
<span className="text-sm text-[#2A2C3F] dark:text-[#EEE5E5]">
Võitlusmängu ala
</span>
</div>
<div className="flex items-center gap-2">
<div
className="w-4 h-4 border border-gray-300"
style={{ backgroundColor: "#3498db" }}
></div>
<span className="text-sm text-[#2A2C3F] dark:text-[#EEE5E5]">
Sony
</span>
</div>
<div className="flex items-center gap-2">
<div
className="w-4 h-4 border border-gray-300"
style={{ backgroundColor: "#2ecc71" }}
></div>
<span className="text-sm text-[#2A2C3F] dark:text-[#EEE5E5]">
Chillimisala
</span>
</div>
</div>
<div className="flex flex-col lg:flex-row gap-8 items-start">
<div className="flex-shrink-0 border-3 border-[#1F5673] w-full max-w-[800px] relative">
<div ref={mountRef} className="w-full" />
<button
onClick={() => setShowDividers(!showDividers)}
className={`absolute top-2 right-2 px-3 py-2 bg-[#1F5673] text-white hover:bg-[#2A7A9B] ${vipnagorgialla.className} uppercase italic text-sm font-semibold flex items-center transition-colors shadow-lg z-10`}
>
{showDividers ? (
<EyeClosed className="w-6 h-6 mr-2" />
) : (
<Eye className="w-6 h-6 mr-2" />
)}
{showDividers ? "Peida" : "Näita"}
</button>
</div>
</div>
{/* Tooltip */}
{hoveredRoom && (
<div
className="fixed bg-black bg-opacity-80 text-white px-3 py-2 rounded-lg text-sm pointer-events-none z-50"
style={{
left: mousePosition.x + 10,
top: mousePosition.y - 10,
}}
>
{hoveredRoom}
</div>
)}
</div>
</div>
);
}

@ -1,10 +1,29 @@
import { vipnagorgialla } from "@/components/Vipnagorgialla"; import { vipnagorgialla } from "@/components/Vipnagorgialla";
import { ThemeProvider } from "@/components/Theme-provider";
export default function NotFound() { export default function NotFound() {
return ( return (
<div className="flex flex-col min-h-[90vh] p-12 justify-center items-center"> <ThemeProvider
<h1 className={`text-7xl ${vipnagorgialla.className} font-bold italic uppercase text-[#2A2C3F] dark:text-[#EEE5E5] mt-8 mb-4`}>404</h1> attribute="class"
<p className="text-2xl text-[#2A2C3F] dark:text-[#EEE5E5]">Seda lehte me ei leidnud.</p> 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>
); </div>
</ThemeProvider>
);
} }

@ -1,96 +0,0 @@
import { vipnagorgialla } from "@/components/Vipnagorgialla";
import Link from "next/link";
export default function Tickets() {
return (
<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 text-[#2A2C3F] dark:text-[#EEE5E5] mt-8 md:mt-16 mb-4`}
>
PILETID JA REGIS&shy;TREERIMINE
</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 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 italic text-[#EEE5E5] pb-2`}
>
8
</h2>
<h3
className={`text-3xl ${vipnagorgialla.className} font-bold italic text-[#EEE5E5] pb-4`}
>
Arvutiga osaleja
</h3>
<ul className="pl-4 mb-8 list-[square] marker:text-[#1F5673]">
<li className="text-xl italic">
Isiklik laud, voolu- ja internetiühendus
</li>
<li className="text-xl">Ligipääs demoalale</li>
<li className="text-xl">Turniiride pealt vaatamine</li>
<li className="text-xl">Võimalus osaleda miniturniiridel</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 italic`}
>
OSTA PILET
</button>
</Link>
</div>
<div className="bg-[#1F5673] -skew-x-2 md:-skew-x-5 text-white 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 italic text-[#EEE5E5] pb-2`}
>
12-15
</h2>
<h3
className={`text-3xl ${vipnagorgialla.className} font-bold italic text-[#EEE5E5] pb-4`}
>
Võistleja
</h3>
<ul className="pl-4 mb-8 list-[square] marker:text-[#007CAB]">
<li className="text-xl">Võimalus osaleda CS2 või LoL turniiril</li>
<li className="text-xl">
Isiklik laud, voolu- ja internetiühendus
</li>
<li className="text-xl">Ligipääs demoalale</li>
<li className="text-xl">Turniiride pealt vaatamine</li>
<li className="text-xl">Võimalus osaleda miniturniiridel</li>
</ul>
<Link href="https://fienta.com/et/tipilan" target="_blank">
<button
className={`px-4 py-2 bg-[#007CAB] cursor-pointer ${vipnagorgialla.className} font-bold italic`}
>
OSTA PILET
</button>
</Link>
</div>
<div className="bg-[#007CAB] -skew-x-2 md:-skew-x-5 text-white 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 italic text-[#EEE5E5] pb-2`}
>
6
</h2>
<h3
className={`text-3xl ${vipnagorgialla.className} font-bold italic text-[#EEE5E5] pb-4`}
>
Külastaja
</h3>
<ul className="pl-4 mb-8 list-[square] marker:text-[#1F5673]">
<li className="text-xl">Ligipääs demoalale</li>
<li className="text-xl">Turniiride pealt vaatamine</li>
<li className="text-xl">Võimalus osaleda miniturniiridel</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 italic`}
>
OSTA PILET
</button>
</Link>
</div>
</div>
</div>
);
}

@ -1,44 +0,0 @@
import { vipnagorgialla } from "@/components/Vipnagorgialla";
import path from "node:path";
import fs from "node:fs/promises";
import ReactMarkdown from "react-markdown";
type Props = {
params: Promise<{ slug: string }>;
};
export default async function RulePage({ params }: Props) {
const { slug } = await params;
const filePath = path.join(process.cwd(), "src/data/rules", `${slug}.md`);
let file: string;
try {
file = await fs.readFile(filePath, "utf8");
} catch {
file = `# ${slug.toUpperCase()} REEGLID\n\nSisu hetkel puudub.`;
}
const data = { title: undefined as string | undefined };
return (
<>
<h1
className={`not-prose ${vipnagorgialla.className} font-bold italic uppercase text-[64px] leading-[96px] tracking-[-0.02em] text-[#2A2C3F] dark:text-[#EEE5E5] mx-auto mt-16 mb-6 px-8`}
>
{data.title || `${slug.toUpperCase()} REEGLID`}
</h1>
<div
className={`mx-auto px-8 font-worksans
[&_ol]:ml-6
[&_ol_ol]:ml-10
[&_ol_ol_ol]:ml-14
[&_h2]:font-bold
`}
>
<ReactMarkdown>{file}</ReactMarkdown>
</div>
</>
);
}

@ -1,63 +0,0 @@
import {vipnagorgialla} from "@/components/Vipnagorgialla";
import Link from "next/link";
export default function RulesMenu() {
const headingStyle = `text-5xl sm:text-6xl ${vipnagorgialla.className} font-bold italic text-[#2A2C3F] dark:text-[#EEE5E5]`;
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-3xl ${vipnagorgialla.className} font-bold uppercase text-[#EEE5E5] pb-2`;
const SectionDivider = () => <div className="border-b-[3px] border-[#1F5673] w-full"/>;
return (
<div>
<div className="flex flex-col md:m-16">
<h1 className={`${headingStyle} mt-8 md:mt-16`}>
REEGLID
</h1>
<div className='flex flex-wrap flex-row lg:mt-16 justify-center lg:items-start gap-12 flex-grow mb-8'>
<Link href="/kodukord">
<div className={`${boxStyle} bg-[#007CAB] py-20`}>
<h2 className={`${boxTextStyle}`}>
Kodukord
</h2>
</div>
</Link>
<Link href="/reeglid/cs2">
<div className={`${boxStyle} bg-[#1F5673] py-20`}>
<h2 className={`${boxTextStyle}`}>
CS2 reeglid
</h2>
</div>
</Link>
<Link href="reeglid/lol">
<div className={`${boxStyle} bg-[#007CAB] py-20`}>
<h2 className={`${boxTextStyle}`}>
LoL reeglid
</h2>
</div>
</Link>
{/* Minitourn. link coming soon*/}
{/*<Link href="">*/}
<div
className={`${boxStyle} bg-[#1F5673] py-16`}>
<h2 className={`${boxTextStyle}`}>
Miniturniiride reeglid
</h2>
</div>
{/*</Link>*/}
</div>
</div>
<SectionDivider />
</div>
);
}

@ -1,184 +0,0 @@
import { vipnagorgialla } from "@/components/Vipnagorgialla";
import Link from "next/link";
import Image from "next/image";
export default function Tourney() {
const headingStyle = `text-3xl md:text-5xl lg:text-5xl ${vipnagorgialla.className} font-bold italic uppercase text-[#2A2C3F] dark:text-[#EEE5E5] -skew-x-2 md:-skew-x-5`;
const SectionDivider = () => <hr className="border-t-[3px] border-[#1F5673]" />;
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-4 m-6 md:m-16`}
>
Turniirid
</h1>
{/*<p className="text-2xl text-[#2A2C3F] dark:text-[#EEE5E5]">*/}
{/* Kui tahate oma oskusi proovile panna, siis vaadake siia tagasi! Rohkem*/}
{/* infot lähiajal.*/}
{/*</p>*/}
<div className="flex flex-col gap-8 md:gap-16">
{/* CS2 turniir */}
<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}`}>
CS2 turniir
</h2>
<p className={"text-2xl mb-4 text-neutral-500"}>
Toimumisaeg veel selgumisel
</p>
<p className="text-balance">
TipiLANil toimub Eesti ühe suurima auhinnafondiga CS2 turniire juba sel sügisel. Haara kaasa
sõbrad ja
saa osa adrenaliinirohkest kogemusest!
</p>
<br />
<p className="text-balance">
Auhinnafond on suuruses 5250, mis jaotatakse TOP3 meeskonna vahel ära. Iga tiimiliige saab
vastavalt
saavutatud kohale auhinnaks kas 600, 300 või 150.
</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`}
>
LOE REEGLEID
</button>
</Link>
<a href="https://fienta.com/et/tipilan" target="_blank">
<button
className={`px-4 py-2 bg-[#007CAB] cursor-pointer ${vipnagorgialla.className} font-bold italic`}
>
OSTA PILET
</button>
</a>
</div>
</div>
<div className="hidden md:block">
<div className="-skew-x-2 md:-skew-x-5">
{/* Image needs to be the div that has the skew. Outside div needs to remain so that overflow wont occur*/}
<Image
src="/images/cs2_tournament_logo.png"
alt="CS2 tournament"
width={600}
height={400}
/>
</div>
</div>
</div>
<SectionDivider />
{/* LoL turniir */}
<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 className="-skew-x-2 md:-skew-x-5">
{/* Image needs to be the div that has the skew. Outside div needs to remain so that overflow wont 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}`}>
LoL turniir
</h2>
<p className={"text-2xl mb-4 text-neutral-500"}>
Toimumisaeg veel selgumisel
</p>
<p className="text-balance">
TipiLANil toimub Eesti ühe suurima auhinnafondiga LoL turniire juba sel sügisel.
Haara kaasa sõbrad ja saa osa adrenaliinirohkest kogemusest!
</p>
<br />
<p className="text-balance">
Auhinnafond on suuruses 3500, mis jaotatakse TOP3 meeskonna vahel ära. Iga tiimiliige saab
vastavalt saavutatud kohale auhinnaks kas 400, 200 või 100.
</p>
<br />
<div className="flex flex-row flex-wrap gap-4 md:gap-8 justify-end">
<Link href="/kodukord" target="_blank">
<button
className={`px-4 py-2 bg-[#1F5673] cursor-pointer ${vipnagorgialla.className} font-bold italic`}
>
LOE REEGLEID
</button>
</Link>
<a href="https://fienta.com/et/tipilan" target="_blank">
<button
className={`px-4 py-2 bg-[#007CAB] cursor-pointer ${vipnagorgialla.className} font-bold italic`}
>
OSTA PILET
</button>
</a>
</div>
</div>
</div>
<SectionDivider />
{/* Mini-turniirid */}
<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}`}>
Mini&shy;turniirid
</h2>
<p className={"text-2xl mb-4 text-neutral-500"}>
Toimumisaeg veel selgumisel
</p>
<p className="text-balance">
TipiLANil toimub mitmeid erinevaid lõbusaid ja võistlushimu tekitavaid miniturniire.
Miniturniirid toimuvad järgnevates mängudes: SimRacing, Tekken, FIFA, Minecraft Bedwars,
Buckshot Roulette, LostGamer ja palju muud.
</p>
<br />
<p className="text-balance">
Auhinnafond on kõigi turniiride peale 1250 ja reeglina saab rahalise auhinna miniturniiri võitja.
</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`}
>
LOE REEGLEID
</button>
</Link>
<a href="https://fienta.com/et/tipilan" target="_blank">
<button
className={`px-4 py-2 bg-[#007CAB] cursor-pointer ${vipnagorgialla.className} font-bold italic`}
>
OSTA PILET
</button>
</a>
</div>
</div>
<div className="hidden md:block">
<div className="-skew-x-2 md:-skew-x-5">
{/* Image needs to be the div that has the skew. Outside div needs to remain so that overflow wont occur*/}
<Image
src="/images/minitournament_logo.png"
alt="mini tournaments"
width={600}
height={400}
/>
</div>
</div>
</div>
<SectionDivider />
</div>
</div>
);
}

@ -1,111 +1,134 @@
import { SiDiscord, SiInstagram, SiFacebook } from "react-icons/si"; import { SiDiscord, SiInstagram, SiFacebook } from "react-icons/si";
import Image from "next/image"; import Image from "next/image";
import { useTranslations } from "next-intl";
// Fonts // Fonts
import { vipnagorgialla } from "@/components/Vipnagorgialla"; import { vipnagorgialla } from "@/components/Vipnagorgialla";
const Footer = () => ( const Footer = () => {
<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"> const t = useTranslations();
<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"> return (
<Image <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">
src="/tipilan-white.svg" <div className="flex md:items-center gap-8 md:gap-0 justify-between flex-col md:flex-row">
width={250} <div className="flex flex-col items-start md:items-center">
height={36} <Image
alt="TipiLAN Logo" src="/tipilan-white.svg"
className="h-9 dark:hidden" width={250}
/> height={36}
<Image alt="TipiLAN Logo"
src="/tipilan-dark.svg" className="h-9 dark:hidden"
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/eB7sVqgJ9b"
target="_blank"
className="mx-4 ml-0 md:ml-4"
rel="noopener noreferrer"
>
<SiDiscord
title="Discord"
size={"2em"}
className="text-[#2A2C3F] dark:text-[#EEE5E5]"
/>
</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]"
/> />
</a> <Image
<a src="/tipilan-dark.svg"
href="https://facebook.com/tipilan.ee" width={250}
target="_blank" height={36}
className="mx-4" alt="TipiLAN Logo"
rel="noopener noreferrer" className="h-9 not-dark:hidden"
>
<SiFacebook
title="Facebook"
size={"2em"}
className="text-[#2A2C3F] dark:text-[#EEE5E5]"
/> />
</a> </div>
{/* Social media */}
<div className="flex flex-row">
<a
href="https://discord.gg/eB7sVqgJ9b"
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>
</div> <div className="flex flex-col">
<div className="flex flex-col"> <h2
<h2 className={`text-3xl sm:text-4xl ${vipnagorgialla.className} font-bold italic uppercase text-[#2A2C3F] dark:text-[#EEE5E5] mt-8 mb-4`}
className={`text-3xl sm:text-4xl ${vipnagorgialla.className} font-bold italic uppercase text-[#2A2C3F] dark:text-[#EEE5E5] mt-8 mb-4`} >
> {t("footer.contact")}
Kontakt </h2>
</h2> <div className="flex flex-row justify-between gap-4 items-center">
<div className="flex flex-row justify-between gap-4 items-center"> <div>
<div> <h3 className="text-xl font-bold">{t("footer.studentUnion")}</h3>
<h3 className="text-xl font-bold">IT-teaduskonna üliõpilaskogu</h3> <div className="flex flex-col gap-2 mt-2">
<div className="flex flex-col gap-2 mt-2"> <div className="flex flex-row gap-2">
<div className="flex flex-row gap-2"> <span className="material-symbols-outlined !font-bold text-[#007CAB] dark:text-[#00A3E0]">
<span className="material-symbols-outlined !font-bold text-[#007CAB] dark:text-[#00A3E0]"> mail
mail </span>
</span> <a href="mailto:kontakt@ituk.ee" className="underline">
<a href="mailto:kontakt@ituk.ee" className="underline"> tipilan@ituk.ee
tipilan@ituk.ee </a>
</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> </div>
<div className="flex flex-row gap-2"> <h3 className="text-xl font-bold pt-4">
<span className="material-symbols-outlined !font-bold text-[#007CAB] dark:text-[#00A3E0]"> {t("footer.organization")}
phone </h3>
</span> <div>
<a href="tel:+37256931193" className="underline"> <p>
+372 5693 1193 {t("footer.registrationCode")}:{" "}
</a> <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>
<h3 className="text-xl font-bold pt-4">MTÜ For Tsükkel</h3> </div>
<div> <div className="block align-middle text-center pt-16">
<p className="text-[#aaa]"> {t("footer.madeBy")}{" "}
Registrikood:{" "} <a
<span className="font-semibold text-[#007CAB] dark:text-[#00A3E0]"> target="_blank"
80391807 href="https://lapikud.ee/"
</span> className="text-[#E3983E] font-bold"
</p> >
<p className="text-[#aaa]"> MTÜ Lapikud
ICO-210, Raja tn 4c, Tallinn, Harjumaa, 12616 </a>{" "}
</p> {t("footer.withHelpFrom")}{" "}
</div> <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> </div>
</div> </div>
</div> );
); };
export default Footer; export default Footer;

@ -12,6 +12,8 @@ import {
// Theme Provider // Theme Provider
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import LanguageSwitcher from "./LanguageSwitcher";
// Shadcn UI // Shadcn UI
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@ -24,65 +26,74 @@ import {
// Fonts // Fonts
// import { vipnagorgialla } from "@/components/Vipnagorgialla"; // import { vipnagorgialla } from "@/components/Vipnagorgialla";
const Header = ({ interface HeaderProps {
isOpen,
toggleSidebar,
}: {
isOpen: boolean; isOpen: boolean;
toggleSidebar: () => void; onToggle: () => void;
}) => { themeLabels: {
light: string;
dark: string;
system: string;
};
}
const Header = ({ isOpen, onToggle, themeLabels }: HeaderProps) => {
const { theme, setTheme } = useTheme(); const { theme, setTheme } = useTheme();
return ( 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]"> <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={toggleSidebar}> <button onClick={onToggle}>
{isOpen ? ( {isOpen ? (
<MdClose className="h-12 w-12 text-[#2A2C3F] dark:text-[#EEE5E5] cursor-pointer" /> <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" /> <MdMenu className="h-12 w-12 text-[#2A2C3F] dark:text-[#EEE5E5] cursor-pointer" />
)} )}
</button> </button>
<DropdownMenu> <div className="flex items-center gap-2">
<DropdownMenuTrigger asChild> <LanguageSwitcher />
<Button <DropdownMenu>
variant="ghost" <DropdownMenuTrigger asChild>
size="icon" <Button
className="size-10 cursor-pointer" variant="ghost"
> size="icon"
<MdSunny className="scale-135 text-[#2A2C3F] dark:hidden" /> className="size-10 cursor-pointer"
<MdModeNight className="scale-135 dark:text-[#EEE5E5] not-dark:hidden" /> >
<span className="sr-only">Toggle theme</span> <MdSunny className="scale-135 text-[#2A2C3F] dark:hidden" />
</Button> <MdModeNight className="scale-135 dark:text-[#EEE5E5] not-dark:hidden" />
</DropdownMenuTrigger> <span className="sr-only">Toggle theme</span>
<DropdownMenuContent align="end" className="w-48 translate-y-4"> </Button>
<DropdownMenuItem </DropdownMenuTrigger>
className={`text-lg ${theme === "light" ? "bg-accent/50 font-medium" : ""}`} <DropdownMenuContent align="end" className="w-48 translate-y-4">
onClick={() => setTheme("light")} <DropdownMenuItem
disabled={theme === "light"} className={`text-lg ${theme === "light" ? "bg-accent/50 font-medium" : ""}`}
> onClick={() => setTheme("light")}
<MdSunny className={theme === "light" ? "text-amber-500" : ""} /> disabled={theme === "light"}
<span>Hele</span> >
</DropdownMenuItem> <MdSunny className={theme === "light" ? "text-amber-500" : ""} />
<DropdownMenuItem <span>{themeLabels.light}</span>
className={`text-lg ${theme === "dark" ? "bg-accent/50 font-medium" : ""}`} </DropdownMenuItem>
onClick={() => setTheme("dark")} <DropdownMenuItem
disabled={theme === "dark"} className={`text-lg ${theme === "dark" ? "bg-accent/50 font-medium" : ""}`}
> onClick={() => setTheme("dark")}
<MdModeNight className={theme === "dark" ? "text-blue-500" : ""} /> disabled={theme === "dark"}
<span>Tume</span> >
</DropdownMenuItem> <MdModeNight
<DropdownMenuItem className={theme === "dark" ? "text-blue-500" : ""}
className={`text-lg ${theme === "system" ? "bg-accent/50 font-medium" : ""}`} />
onClick={() => setTheme("system")} <span>{themeLabels.dark}</span>
disabled={theme === "system"} </DropdownMenuItem>
> <DropdownMenuItem
<MdComputer className={`text-lg ${theme === "system" ? "bg-accent/50 font-medium" : ""}`}
className={theme === "system" ? "text-green-500" : ""} onClick={() => setTheme("system")}
/> disabled={theme === "system"}
<span>Süsteemipõhine</span> >
</DropdownMenuItem> <MdComputer
</DropdownMenuContent> className={theme === "system" ? "text-green-500" : ""}
</DropdownMenu> />
<span>{themeLabels.system}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</header> </header>
); );
}; };

@ -0,0 +1,47 @@
"use client";
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";
export default function LanguageSwitcher() {
const locale = useLocale();
const router = useRouter();
const pathname = usePathname();
const getNextLocale = (): "et" | "en" => {
const currentIndex = routing.locales.indexOf(locale as "et" | "en");
const nextIndex = (currentIndex + 1) % routing.locales.length;
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
onClick={handleLanguageSwitch}
variant="ghost"
size="lg"
className={`${vipnagorgialla.className} text-3xl font-bold italic uppercase hover:bg-[#007CAB]/10 dark:hover:bg-[#00A3E0]/10 text-[#007CAB] dark:text-[#00A3E0] hover:text-[#2A2C3F] dark:hover:text-[#EEE5E5] transition-colors`}
>
{getNextLanguageName()}
</Button>
);
}

@ -0,0 +1,7 @@
import React from 'react';
export default function SectionDivider() {
return (
<hr className="border-t-[3px] border-[#1F5673]" />
);
};

@ -1,88 +0,0 @@
"use client";
// Fonts
import { vipnagorgialla } from "@/components/Vipnagorgialla";
// Use effect to handle route changes and close the sidebar if it's open
// usePathName to listen to route changes in Next.js
import { useEffect } from "react";
import { usePathname } from "next/navigation";
import Link from "next/link";
const Sidebar = ({
isOpen,
toggleSidebar,
}: {
isOpen: boolean;
toggleSidebar: () => void;
}) => {
const pathname = usePathname();
useEffect(() => {
if (isOpen) {
toggleSidebar();
}
}, [pathname]);
return (
<>
<div
className="fixed inset-0 backdrop-blur mt-16 z-20"
style={{ display: isOpen ? "block" : "none" }}
onClick={toggleSidebar} // Close sidebar when clicking outside
></div>
<div
className={`text-3xl md:text-5xl ${vipnagorgialla.className} font-bold 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)",
}}
>
<Link href="/" className="hover:text-[#00A3E0] transition duration-150">
Avaleht
</Link>
<Link
href="/messiala"
className="hover:text-[#00A3E0] transition duration-150"
>
Messiala
</Link>
<Link
href="/piletid"
className="hover:text-[#00A3E0] transition duration-150"
>
Piletid
</Link>
<Link
href="/ajakava"
className="hover:text-[#00A3E0] transition duration-150"
>
Ajakava
</Link>
<Link
href="/turniirid"
className="hover:text-[#00A3E0] transition duration-150"
>
Turniirid
</Link>
<Link
href="/kodukord"
className="hover:text-[#00A3E0] transition duration-150"
>
Kodukord
</Link>
<Link
href="/reeglid"
className="hover:text-[#00A3E0] transition duration-150"
>
Reeglid
</Link>
</div>
</>
);
};
export default Sidebar;

@ -0,0 +1,84 @@
"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>
</>
</>
);
}

@ -0,0 +1,26 @@
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,22 +1,14 @@
'use client'; import SidebarLayoutServer from "./SidebarLayoutServer";
import { useState } from "react";
import Header from "./Header";
import Sidebar from "./Sidebar";
const SidebarParent = () => { const SidebarParent = () => {
const [isOpen, setIsOpen] = useState(false); return (
const toggleSidebar = () => setIsOpen(!isOpen); <div className="fixed w-screen top-0 z-9999">
<SidebarLayoutServer />
return ( </div>
<div className="fixed w-screen top-0 z-9999"> );
<Header isOpen={isOpen} toggleSidebar={toggleSidebar} />
<Sidebar isOpen={isOpen} toggleSidebar={toggleSidebar}/>
</div>
);
}; };
// This component is responsible for rendering the sidebar and header together. // This component is responsible for rendering the sidebar and header together.
// It manages the state of the sidebar (open/closed) and passes the necessary props to both the Header and Sidebar components. // Server-side translations are handled by SidebarLayoutServer.
export default SidebarParent; export default SidebarParent;

@ -1,8 +1,8 @@
import * as React from "react" import * as React from "react";
import { Slot } from "@radix-ui/react-slot" import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const buttonVariants = cva( 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", "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",
@ -32,8 +32,8 @@ const buttonVariants = cva(
variant: "default", variant: "default",
size: "default", size: "default",
}, },
} },
) );
function Button({ function Button({
className, className,
@ -43,17 +43,25 @@ function Button({
...props ...props
}: React.ComponentProps<"button"> & }: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & { VariantProps<typeof buttonVariants> & {
asChild?: boolean asChild?: boolean;
}) { }) {
const Comp = asChild ? Slot : "button" if (asChild) {
return (
<Slot
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...(props as React.ComponentProps<typeof Slot>)}
/>
);
}
return ( return (
<Comp <button
data-slot="button" data-slot="button"
className={cn(buttonVariants({ variant, size, className }))} className={cn(buttonVariants({ variant, size, className }))}
{...props} {...props}
/> />
) );
} }
export { Button, buttonVariants } export { Button, buttonVariants };

@ -0,0 +1,13 @@
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 }

@ -0,0 +1,30 @@
Ü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.

@ -30,7 +30,7 @@ Korraldajal on õigus eemaldada võistleja turniirilt nende reeglite rikkumise k
## 3. Mängule eelnev ## 3. Mängule eelnev
1. **3.1** Turniiril osalemine, matchid ja turniiripuu toimib **challengermode.com** keskkonna kaudu: 1. **3.1** Turniiril osalemine, matchid ja turniiripuu toimib **[challengermode.com](https://www.challengermode.com/s/TipiLAN/tournaments/4b7d832b-7cf3-406a-8425-08ddd311ac8a)** keskkonna kaudu:
1. **3.1.1** Turniirile peab olema registreeritud kogu meeskond, sh varumängija 1. **3.1.1** Turniirile peab olema registreeritud kogu meeskond, sh varumängija
2. **3.1.2** Turniir toimub **EU West** serveris 2. **3.1.2** Turniir toimub **EU West** serveris
2. **3.2** Challengermode keskkonnas on match’id automaatsed. Uue match’i puhul on valmisolekuks aega **10 minutit**, seejärel lobby’sse jõudmiseks veel **10 minutit**. 2. **3.2** Challengermode keskkonnas on match’id automaatsed. Uue match’i puhul on valmisolekuks aega **10 minutit**, seejärel lobby’sse jõudmiseks veel **10 minutit**.
@ -42,12 +42,12 @@ Korraldajal on õigus eemaldada võistleja turniirilt nende reeglite rikkumise k
## 4. Mängusisesed protseduurid ## 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). 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. 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: 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 oskuse (ability) kasutamine käsilaste, jungle creep’ide, ehitiste või vastaste vastu 1. **4.1.1** Kummalgi tiimil õnnestub rünnak või oskuse (ability) kasutamine käsilaste, jungle creep’ide, ehitiste või vastaste vastu
2. **4.1.2** Vastased näevad teineteist (Clairvoyance ei loe) 2. **4.1.2** Vastased näevad teineteist (Clairvoyance ei loe)
3. **4.1.3** Sisenetakse vastase territooriumile 3. **4.1.3** Sisenetakse vastase territooriumile
4. **4.1.4** Mäng on kestnud vähemalt 2 minutit 4. **4.1.4** Mäng on kestnud vähemalt 2 minutit
Pärast GOR-i staatuseni jõudmist ei või mängu uuesti alustada, välja arvatud mõjuval põhjusel (bug, ühenduse probleemid jne). Pärast GOR-i staatuseni jõudmist ei või mängu uuesti alustada, välja arvatud mõjuval põhjusel (bug, ühenduse probleemid jne).
2. **4.2** Mängu seiskamine: 2. **4.2** Mängu seiskamine:
1. **4.2.1** Mängu pausile panemise ajal ei tohi lahkuda matši alalt, v.a juhul kui see on ametlikult autoriseeritud 1. **4.2.1** Mängu pausile panemise ajal ei tohi lahkuda matši alalt, v.a juhul kui see on ametlikult autoriseeritud
@ -86,53 +86,53 @@ Peale GOR staatuseni jõudmist on võimalik mängu restartida vaid juhtudel, kui
2. **7.10.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. 2. **7.10.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.10.3 Mängu kaotus:** tiim saab automaatse kaotuse ühel mängul. 3. **7.10.3 Mängu kaotus:** tiim saab automaatse kaotuse ühel mängul.
4. **7.10.4 Match’i kaotus:** tiim saab automaatse match’i kaotuse. 4. **7.10.4 Match’i kaotus:** tiim saab automaatse match’i kaotuse.
5. **7.10.5 Diskvalifitseerimine:** diskvalifikatsioon kehtib tervele tiimile. Sellel juhul loobub tiim kõigist võitudest. Kui diskvalifikatsioon on saadud eskaleeruvate eksimuste tulemusel, saab tiim selle osa võitudest, mis neil oli selleks hetkeks välja teenitud. 5. **7.10.5 Diskvalifitseerimine:** diskvalifikatsioon kehtib tervele tiimile. Sellel juhul loobub tiim kõigist võitudest. Kui diskvalifikatsioon 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. - 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.
11. **7.11** Karistuste eskaleerimine toimub järgmises järjekorras: 11. **7.11** Karistuste eskaleerimine toimub järgmises järjekorras:
Hoiatus → hoiatus → ban’i valimise õiguse kaotus → mängu kaotus → match’i kaotus → diskvalifikatsioon. Hoiatus → hoiatus → ban’i valimise õiguse kaotus → mängu kaotus → match’i kaotus → diskvalifikatsioon.
12. **7.12** Turniiri eksimused jagunevad järgmiselt: 12. **7.12** Turniiri eksimused jagunevad järgmiselt:
1. **7.12.1 Välise abi kasutamine:** eksimus läheb kirja, kui tiim suhtleb mängu ajal ükskõik kellega 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. 1. **7.12.1 Välise abi kasutamine:** eksimus läheb kirja, kui tiim suhtleb mängu ajal ükskõik kellega 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.12.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. 2. **7.12.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.
- Spetsiifiliselt ühele tiimile või mängijale tehtud korralduse eiramine on eraldi eksimus ning kuulub *Mittesobilik käitumine – Keskmine eksimus* alla. - Spetsiifiliselt ühele tiimile või mängijale tehtud korralduse eiramine on eraldi eksimus ning kuulub *Mittesobilik käitumine – Keskmine eksimus* alla.
13. **7.13** Mittesobilik käitumine. 13. **7.13** 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. See ei ole sama, mis konkurentsihimuline 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. See ei ole sama, mis konkurentsihimuline käitumine.
Mittesobiliku käitumise eksimused jagunevad: Mittesobiliku käitumise eksimused jagunevad:
1. **7.13.1 Kerge eksimus:** käitumine, mis on ebameeldiv, ebaeetiline või häiriv, näiteks liigne ropendamine; nõudmine, et vastane saaks karistuse peale kohtuniku otsust; lõugamine; prügi maha loopimine jne. Karistuseks on hoiatus. 1. **7.13.1 Kerge eksimus:** käitumine, mis on ebameeldiv, ebaeetiline või häiriv, näiteks liigne ropendamine; nõudmine, et vastane saaks karistuse peale kohtuniku otsust; lõugamine; prügi maha loopimine jne. Karistuseks on hoiatus.
2. **7.13.2 Keskmine eksimus:** kolm tüüpi juhtumeid: 2. **7.13.2 Keskmine eksimus:** kolm tüüpi juhtumeid:
- Eirab kohtuniku või Korraldaja juhiseid, mis on mõeldud spetsiaalselt ühele tiimile või ühele mängijale - Eirab kohtuniku või Korraldaja juhiseid, mis on mõeldud spetsiaalselt ühele tiimile või ühele mängijale
- Kasutab avalikult vihakõnet kellegi suunas - Kasutab avalikult vihakõnet kellegi suunas
- On agressiivne või vägivaldne, kuid see ei ole suunatud teise inimese vastu - On agressiivne või vägivaldne, kuid see ei ole suunatud teise inimese vastu
Karistuseks on mängu kaotus. Karistuseks on mängu kaotus.
3. **7.13.3 Raske eksimus:** käitumine, mis on selgelt vastuolus turniiri reeglite ja heade tavadega, näiteks tahtlikult turniiri vahendite lõhkumine või ruumi määrimine/lõhkumine. Karistuseks on diskvalifikatsioon, turniiri toimumiskohast eemaldamine või ekstreemsematel juhtudel politsei teavitamine. 3. **7.13.3 Raske eksimus:** käitumine, mis on selgelt vastuolus turniiri reeglite ja heade tavadega, näiteks tahtlikult turniiri vahendite lõhkumine või ruumi määrimine/lõhkumine. Karistuseks on diskvalifikatsioon, turniiri toimumiskohast eemaldamine või ekstreemsematel juhtudel politsei teavitamine.
4. **7.13.4 Kokkumäng:** kahe tiimi kokkulepe ebaausalt teiste tiimide vastu mängida ja püüda mõjutada turniiri tulemusi. Karistuseks on mõlema tiimi diskvalifitseerimine. 4. **7.13.4 Kokkumäng:** kahe tiimi kokkulepe ebaausalt teiste tiimide vastu mängida ja püüda mõjutada turniiri tulemusi. Karistuseks on mõlema tiimi diskvalifitseerimine.
5. **7.13.5 Altkäemaks ja panustamine:** keelatud on meelehea (mitte ainult rahaline) nimel loobuda turniirist või püüda muuta match’ide tulemusi. Samuti on keelatud pakkuda kohtunikule stiimulit mängu tulemuse mõjutamiseks või teha panuseid mängude tulemustele. Karistuseks on diskvalifitseerimine. 5. **7.13.5 Altkäemaks ja panustamine:** keelatud on meelehea (mitte ainult rahaline) nimel loobuda turniirist või püüda muuta match’ide tulemusi. Samuti on keelatud pakkuda kohtunikule stiimulit mängu tulemuse mõjutamiseks või teha panuseid mängude tulemustele. Karistuseks on diskvalifitseerimine.
6. **7.13.6 Agressiivne käitumine:** kõik inimeste vastu suunatud agressiooni ilmingud, kaasa arvatud ähvardamine ja reaalne vägivald. Karistuseks on diskvalifitseerimine ja toimumiskohast eemaldamine, ekstreemsematel juhtudel politsei teavitamine. 6. **7.13.6 Agressiivne käitumine:** kõik inimeste vastu suunatud agressiooni ilmingud, kaasa arvatud ähvardamine ja reaalne vägivald. Karistuseks on diskvalifitseerimine ja toimumiskohast eemaldamine, ekstreemsematel juhtudel politsei teavitamine.
7. **7.13.7 Vargus:** kuigi igal osalejal on kohustus oma varal silma peal hoida, eeldatakse heade tavade järgimist. Karistuseks on diskvalifitseerimine ja toimumiskohast eemaldamine, vajadusel politsei teavitamine. 7. **7.13.7 Vargus:** kuigi igal osalejal on kohustus oma varal silma peal hoida, eeldatakse heade tavade järgimist. Karistuseks on diskvalifitseerimine ja toimumiskohast eemaldamine, vajadusel politsei teavitamine.
8. **7.13.8 Alkohol ja joove:** alkoholi tarbimine ürituse raames on keelatud. Liigse joobe korral on Korraldajal õigus osaleja toimumiskohast eemaldada. 8. **7.13.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. - Kui joobes osaleja on alaealine, informeeritakse sellest tema vanemaid ning politseid.
9. **7.13.9 Sohk:** teadlik tegevus mängus eelise saavutamiseks, isegi kui see ei ole edukas. 9. **7.13.9 Sohk:** teadlik tegevus mängus eelise saavutamiseks, isegi kui see ei ole edukas.
Sohki tegemise näited: Sohki tegemise näited:
- Püüab näha enda mängu spectator mode’s või saab infot kelleltki, kes saab mängu spectator mode’s vaadata - Püüab näha enda mängu spectator mode’s või saab infot kelleltki, kes saab mängu spectator mode’s vaadata
- Mängu modifitseerimine või lisatarkvara kasutamine, mis ei ole tavapärane (nt löögiraadiuse või torni laskeraadiuse nähtavaks tegemine, spawn-taimerid jms; VOIP-programmide kasutamine ei kuulu siia alla) - Mängu modifitseerimine või lisatarkvara kasutamine, mis ei ole tavapärane (nt löögiraadiuse või torni laskeraadiuse nähtavaks tegemine, spawn-taimerid jms; VOIP-programmide kasutamine ei kuulu siia alla)
- Teise mängijana või vale nime all esinemine, konto jagamine - Teise mängijana või vale nime all esinemine, konto jagamine
- Varustuse tahtlik rikkumine või moonutamine, et tekitada viivitusi või mõjutada mängu kulgu - Varustuse tahtlik rikkumine või moonutamine, et tekitada viivitusi või mõjutada mängu kulgu
- Mängusiseste vigade tahtlik ärakasutamine (glitchid) eelise saamiseks - Mängusiseste vigade tahtlik ärakasutamine (glitchid) eelise saamiseks
Karistuseks on diskvalifikatsioon. Karistuseks on diskvalifikatsioon.
## 8. Double Elimination ## 8. Double Elimination
1. **8.1** Double Elimination on turniiri formaat, kus esimeses voorus mängivad kõik 16 tiimi. 1. **8.1** Double Elimination on turniiri formaat, kus esimeses voorus mängivad kõik 16 tiimi.
2. **8.2** Pärast avavooru jagunevad tiimid kaheks: 2. **8.2** Pärast avavooru jagunevad tiimid kaheks:
1. **8.2.1** Võitjate elimineerimispuu (*upper bracket*) – sinna liiguvad esimeses voorus võitnud tiimid. 1. **8.2.1** Võitjate elimineerimispuu (*upper bracket*) – sinna liiguvad esimeses voorus võitnud tiimid.
2. **8.2.2** Kaotajate elimineerimispuu (*lower bracket*) – sinna liiguvad esimeses voorus kaotanud tiimid. 2. **8.2.2** Kaotajate elimineerimispuu (*lower bracket*) – sinna liiguvad esimeses voorus kaotanud tiimid.
3. **8.3** Väljalangemine: 3. **8.3** Väljalangemine:
1. **8.3.1** Iga järgmises voorus kaotav tiim langeb turniirilt välja. 1. **8.3.1** Iga järgmises voorus kaotav tiim langeb turniirilt välja.
4. **8.4** Mänguformaat: 4. **8.4** Mänguformaat:
1. **8.4.1** Esimesed kolm vooru: *Best of 1* – võitja liigub edasi järgmisesse vooru. 1. **8.4.1** Esimesed kolm vooru: *Best of 1* – võitja liigub edasi järgmisesse vooru.
2. **8.4.2** Upper- ja lower-finaalid: *Best of 3* – võidab see, kes võidab esimesena kaks mängu. 2. **8.4.2** Upper- ja lower-finaalid: *Best of 3* – võidab see, kes võidab esimesena kaks mängu.
3. **8.4.3** Finaal: *Best of 5* – võidab see, kes võidab esimesena kolm mängu. 3. **8.4.3** Finaal: *Best of 5* – võidab see, kes võidab esimesena kolm mängu.
5. **8.5** III koha määramine: 5. **8.5** III koha määramine:
1. **8.5.1** Finaali kaotaja mängib kaotajate elimineerimispuu võitjaga. 1. **8.5.1** Finaali kaotaja mängib kaotajate elimineerimispuu võitjaga.
2. **8.5.2** Selle kohtumise võitja saab turniiri **III koha**. 2. **8.5.2** Selle kohtumise võitja saab turniiri **III koha**.

@ -1,33 +1,68 @@
export type ScheduleItem = { export type ScheduleItem = {
time?: string; // Aeg on ajutine praegu kuna pole 100% kindlalt paigas time?: string; // Aeg on ajutine praegu kuna pole 100% kindlalt paigas
title: string; titleKey: string;
location: string; locationKey: string;
description?: string; description?: string;
}; };
export const scheduleData: Record<string, ScheduleItem[]> = { export const scheduleData: Record<string, ScheduleItem[]> = {
"24. oktoober": [ oct24: [
{ {
title: "League of Legends põhiturniir", titleKey: "schedule.events.doorsOpen",
location: "Aula", locationKey: "schedule.locations.registrationSetup",
time: "-", time: "17:00",
}, },
{ {
title: "Miniturniirid", titleKey: "schedule.events.mainTournamentsStart",
location: "Tudengimaja", locationKey: "schedule.locations.auditorium",
time: "-", time: "20:00",
},
{
titleKey: "schedule.events.miniTournamentsKickoff",
locationKey: "schedule.locations.studentHouse",
time: "18:00",
},
{
titleKey: "schedule.events.fightingGamesStart",
locationKey: "schedule.locations.studentHouse",
time: "18:30",
},
{
titleKey: "schedule.events.doorsClose",
locationKey: "schedule.locations.auditoriumAndStudentHouse",
time: "*01:00",
}, },
], ],
"25. oktoober": [ oct25: [
{
titleKey: "schedule.events.doorsOpen",
locationKey: "schedule.locations.auditoriumAndStudentHouse",
time: "10:00",
},
{
titleKey: "schedule.events.miniTournamentsStart",
locationKey: "schedule.locations.studentHouse",
time: "11:00",
},
{
titleKey: "schedule.events.granblue",
locationKey: "schedule.locations.studentHouse",
time: "11:30",
},
{
titleKey: "schedule.events.mainTournamentsStart",
locationKey: "schedule.locations.auditorium",
time: "12:00",
},
{ {
title: "Counter-Strike 2 põhiturniir", titleKey: "schedule.events.granTurismo",
location: "Aula", locationKey: "schedule.locations.studentHouse",
time: "-", time: "20:00",
}, },
{ {
title: "Miniturniirid", titleKey: "schedule.events.doorsClose",
location: "Tudengimaja", locationKey: "schedule.locations.auditoriumAndStudentHouse",
time: "-", time: "*01:00",
}, },
], ],
}; };

@ -0,0 +1,17 @@
import { getRequestConfig } from "next-intl/server";
import { routing } from "./routing";
export default getRequestConfig(async ({ requestLocale }) => {
// This typically corresponds to the `[locale]` segment
let locale = await requestLocale;
// Ensure that a valid locale is used
if (!locale || !routing.locales.includes(locale as "et" | "en")) {
locale = routing.defaultLocale;
}
return {
locale: locale!,
messages: (await import(`../../translations/${locale}.json`)).default,
};
});

@ -0,0 +1,55 @@
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);

@ -0,0 +1,20 @@
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|.*\\..*).*)'
]
};

@ -0,0 +1,227 @@
{
"navigation": {
"home": "Home",
"schedule": "Schedule",
"admin": "Admin",
"houserules": "House rules",
"expo": "Expo",
"tickets": "Tickets",
"rules": "Game rules",
"stream": "Stream",
"tournaments": "Tournaments"
},
"common": {
"loading": "Loading...",
"error": "Error",
"success": "Success",
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
"edit": "Edit",
"close": "Close",
"next": "Next",
"previous": "Previous",
"search": "Search",
"filter": "Filter",
"reset": "Reset",
"theme": {
"light": "Light",
"dark": "Dark",
"system": "System"
},
"language": {
"et": "Eesti",
"en": "English"
}
},
"home": {
"title": "TipiLAN 2025",
"subtitle": "Estonia's largest student-organized LAN event!",
"welcome": "Welcome to TipiLAN 2025!",
"description": "Join us at Estonia's largest student-organized LAN event. Games, competitions and much more await you!",
"sections": {
"schedule": {
"description": "TipiLAN is packed with exciting tournaments, mini-competitions and much more."
},
"tournaments": {
"description": "TipiLAN features massive CS2 and LoL tournaments with a prize pool of €10,000."
},
"expo": {
"description": "The TipiLAN expo area hosts companies, additional activities and lectures."
},
"reserveSpot": "Reserve your spot today!",
"poweredBy": "TipiLAN is powered by..."
}
},
"tickets": {
"title": "TICKETS AND REGISTRATION",
"buyNow": "Buy now",
"soldOut": "Sold out",
"available": "Available",
"price": "Price",
"includes": "Includes",
"computerParticipant": {
"title": "Computer Participant",
"price": "8€",
"features": [
"Personal desk, power and internet connection",
"Access to demo area",
"Tournament spectating",
"Ability to participate in mini-tournaments"
]
},
"competitor": {
"title": "Competitor",
"price": "12-15€",
"features": [
"Ability to participate in the CS2 or LoL tournament",
"Personal desk, power and internet connection",
"Access to demo area",
"Tournament spectating",
"Ability to participate in mini-tournaments"
]
},
"visitor": {
"title": "Visitor",
"price": "6€",
"features": [
"Access to demo area",
"Tournament spectating",
"Ability to participate in mini-tournaments"
]
},
"buyTicket": "BUY TICKETS"
},
"tournaments": {
"title": "Tournaments",
"register": "Register",
"participants": "Participants",
"prizePool": "Prize pool",
"schedule": "Schedule",
"rules": "Rules",
"cs2": {
"title": "CS2 Tournament",
"timing": "Timing to be announced",
"description1": "TipiLAN hosts one of Estonia's largest CS2 tournaments with a significant prize pool this fall. Grab your friends and experience the adrenaline rush!",
"description2": "The prize pool is €5,250, distributed among the TOP3 teams. Each team member receives €600, €300, or €150 based on their placement.",
"readRules": "READ RULES",
"buyTicket": "BUY TICKETS"
},
"lol": {
"title": "LoL Tournament",
"timing": "Timing to be announced",
"description1": "TipiLAN hosts one of Estonia's largest LoL tournaments with a significant prize pool this fall. Grab your friends and experience the adrenaline rush!",
"description2": "The prize pool is €3,500, distributed among the TOP3 teams. Each team member receives €400, €200, or €100 based on their placement.",
"readRules": "READ RULES",
"buyTicket": "BUY TICKETS"
},
"mini": {
"title": "Mini-tournaments",
"timing": "Timing to be announced",
"description1": "TipiLAN hosts various fun and competitive mini-tournaments. Mini-tournaments take place in the following games: SimRacing, Tekken, FIFA, Minecraft Bedwars, Buckshot Roulette, LostGamer and many more.",
"description2": "The total prize pool for all tournaments is €1,250 and typically the mini-tournament winner receives a cash prize.",
"readRules": "READ RULES",
"buyTicket": "BUY TICKETS"
}
},
"schedule": {
"title": "Schedule",
"day": "Day",
"time": "Time",
"event": "Event",
"location": "Location",
"oct24": "October 24th",
"oct25": "October 25th",
"events": {
"doorsOpen": "Doors open",
"mainTournamentsStart": "Main tournaments begin",
"miniTournamentsKickoff": "Mini-tournaments kick-off",
"fightingGamesStart": "Fighting game tournaments start",
"doorsClose": "Doors close",
"miniTournamentsStart": "Mini-tournaments begin",
"granblue": "Granblue tournament",
"granTurismo": "Gran Turismo tournament"
},
"locations": {
"registrationSetup": "Registration and setup in auditorium",
"auditorium": "Auditorium",
"studentHouse": "Student House (Tudengimaja)",
"auditoriumAndStudentHouse": "Auditorium and Student House",
"entranceHall": "Entrance Hall"
}
},
"stream": {
"title": "Stream",
"live": "Live",
"offline": "Offline",
"watchNow": "Watch now"
},
"footer": {
"copyright": "© 2025 TipiLAN. All rights reserved.",
"contact": "Contact",
"privacy": "Privacy",
"terms": "Terms",
"studentUnion": "IT Faculty Student Council",
"organization": "MTÜ For Tsükkel",
"registrationCode": "Registration code",
"madeBy": "The TipiLAN website is made with love by",
"withHelpFrom": "with the help of"
},
"notFound": {
"title": "404",
"message": "We couldn't find this page."
},
"expo": {
"title": "Expo Area",
"description": "The TipiLAN expo area hosts companies, additional activities and lectures.",
"areas": {
"bar": "Bar Area",
"boardGames": "Board Games Area",
"simRacing": "Red Bull Sim Racing",
"fighting": "Fighting Games Area",
"photobooth": "Photo booth",
"ityk": "TalTech IT Faculty Student Council",
"tartuyk": "Tartu University",
"estoniagamedev": "Estonia Gamedev",
"info": "Information booth",
"tly": "Tallinn University",
"ittk": "TalTech School of Information Technologies",
"gameup": "GameUP!"
},
"hide": "Hide walls",
"show": "Show walls"
},
"rules": {
"title": "Rules",
"houseRules": "House Rules",
"cs2Rules": "CS2 Rules",
"lolRules": "LoL Rules",
"miniRules": "Mini-tournament Rules"
},
"admin": {
"title": "Admin",
"users": "Users",
"teams": "Teams",
"success": {
"title": "Operation was successful!",
"description": "Database data has been updated."
},
"sync": {
"title": "Do you want to update the database?",
"description1": "This will pull current data from Fienta and replace",
"all": "ALL",
"description2": "existing data in the database!",
"warning": "If you're not sure, click \"Cancel\".",
"update": "Update"
},
"roles": {
"captain": "Captain",
"teammate": "Teammate"
},
"table": {
"name": "Name",
"members": "Members",
"noMembers": "No members"
}
}
}

@ -0,0 +1,227 @@
{
"navigation": {
"home": "Avaleht",
"schedule": "Ajakava",
"admin": "Haldus",
"houserules": "Kodukord",
"expo": "Messiala",
"tickets": "Piletid",
"rules": "Reeglid",
"stream": "Striim",
"tournaments": "Turniirid"
},
"common": {
"loading": "Laadimine...",
"error": "Viga",
"success": "Õnnestus",
"save": "Salvesta",
"cancel": "Tühista",
"delete": "Kustuta",
"edit": "Muuda",
"close": "Sulge",
"next": "Järgmine",
"previous": "Eelmine",
"search": "Otsi",
"filter": "Filtreeri",
"reset": "Lähtesta",
"theme": {
"light": "Hele",
"dark": "Tume",
"system": "Süsteemipõhine"
},
"language": {
"et": "Eesti",
"en": "English"
}
},
"home": {
"title": "TipiLAN 2025",
"subtitle": "Eesti suurim tudengite korraldatud LAN!",
"welcome": "Tere tulemast TipiLAN 2025 sündmusele!",
"description": "Liitu meiega Eesti suurimal tudengite korraldatud LAN-üritusel. Mängud, võistlused ja palju muud ootavad sind!",
"sections": {
"schedule": {
"description": "TipiLAN on pungil põnevatest turniiridest, mini-võistlustest ja paljust muust."
},
"tournaments": {
"description": "TipiLANil toimuvad suurejoonelised CS2 ja LoL turniirid, mille auhinnafond on 10 000€."
},
"expo": {
"description": "TipiLANi messialal paiknevad ettevõtted, lisategevused ja toimuvad loengud."
},
"reserveSpot": "Broneeri oma koht juba täna!",
"poweredBy": "TipiLANi tõmbab käima..."
}
},
"tickets": {
"title": "PILETID JA REGISTREERIMINE",
"buyNow": "Osta nüüd",
"soldOut": "Välja müüdud",
"available": "Saadaval",
"price": "Hind",
"includes": "Sisaldab",
"computerParticipant": {
"title": "Arvutiga osaleja",
"price": "8€",
"features": [
"Isiklik laud, voolu- ja internetiühendus",
"Ligipääs demoalale",
"Turniiride pealt vaatamine",
"Võimalus osaleda miniturniiridel"
]
},
"competitor": {
"title": "Võistleja",
"price": "12-15€",
"features": [
"Võimalus osaleda CS2 või LoL turniiril",
"Isiklik laud, voolu- ja internetiühendus",
"Ligipääs demoalale",
"Turniiride pealt vaatamine",
"Võimalus osaleda miniturniiridel"
]
},
"visitor": {
"title": "Külastaja",
"price": "6€",
"features": [
"Ligipääs demoalale",
"Turniiride pealt vaatamine",
"Võimalus osaleda miniturniiridel"
]
},
"buyTicket": "OSTA PILET"
},
"tournaments": {
"title": "Turniirid",
"register": "Registreeru",
"participants": "Osalejad",
"prizePool": "Auhinnafond",
"schedule": "Ajakava",
"rules": "Reeglid",
"cs2": {
"title": "CS2 turniir",
"timing": "Toimumisaeg veel selgumisel",
"description1": "TipiLANil toimub Eesti ühe suurima auhinnafondiga CS2 turniire juba sel sügisel. Haara kaasa sõbrad ja saa osa adrenaliinirohkest kogemusest!",
"description2": "Auhinnafond on suuruses 5250€, mis jaotatakse TOP3 meeskonna vahel ära. Iga tiimiliige saab vastavalt saavutatud kohale auhinnaks kas 600€, 300€ või 150€.",
"readRules": "LOE REEGLEID",
"buyTicket": "OSTA PILET"
},
"lol": {
"title": "LoL turniir",
"timing": "Toimumisaeg veel selgumisel",
"description1": "TipiLANil toimub Eesti ühe suurima auhinnafondiga LoL turniire juba sel sügisel. Haara kaasa sõbrad ja saa osa adrenaliinirohkest kogemusest!",
"description2": "Auhinnafond on suuruses 3500€, mis jaotatakse TOP3 meeskonna vahel ära. Iga tiimiliige saab vastavalt saavutatud kohale auhinnaks kas 400€, 200€ või 100€.",
"readRules": "LOE REEGLEID",
"buyTicket": "OSTA PILET"
},
"mini": {
"title": "Miniturniirid",
"timing": "Toimumisaeg veel selgumisel",
"description1": "TipiLANil toimub mitmeid erinevaid lõbusaid ja võistlushimu tekitavaid miniturniire. Miniturniirid toimuvad järgnevates mängudes: SimRacing, Tekken, FIFA, Minecraft Bedwars, Buckshot Roulette, LostGamer ja palju muud.",
"description2": "Auhinnafond on kõigi turniiride peale 1250€ ja reeglina saab rahalise auhinna miniturniiri võitja.",
"readRules": "LOE REEGLEID",
"buyTicket": "OSTA PILET"
}
},
"schedule": {
"title": "Ajakava",
"day": "Päev",
"time": "Aeg",
"event": "Sündmus",
"location": "Asukoht",
"oct24": "24. oktoober",
"oct25": "25. oktoober",
"events": {
"doorsOpen": "Uksed avatakse",
"mainTournamentsStart": "Põhiturniirid algavad",
"miniTournamentsKickoff": "Miniturniiride kick-off",
"fightingGamesStart": "Fighting games turniiride algus",
"doorsClose": "Uksed suletakse",
"miniTournamentsStart": "Miniturniirid algavad",
"granblue": "Granblue turniir",
"granTurismo": "Gran Turismo turniir"
},
"locations": {
"registrationSetup": "Registreerimine ja setup aulas",
"auditorium": "Aula",
"studentHouse": "Tudengimaja",
"auditoriumAndStudentHouse": "Aula ja Tudengimaja",
"entranceHall": "Fuajee"
}
},
"stream": {
"title": "Striim",
"live": "Otse-eetris",
"offline": "Väljas",
"watchNow": "Vaata nüüd"
},
"footer": {
"copyright": "© 2025 TipiLAN. Kõik õigused kaitstud.",
"contact": "Kontakt",
"privacy": "Privaatsus",
"terms": "Tingimused",
"studentUnion": "IT-teaduskonna üliõpilaskogu",
"organization": "MTÜ For Tsükkel",
"registrationCode": "Registrikood",
"madeBy": "TipiLANi veebileht on tehtud armastusega",
"withHelpFrom": "poolt, kellele oli abiks"
},
"notFound": {
"title": "404",
"message": "Seda lehte me ei leidnud."
},
"expo": {
"title": "Messiala",
"description": "TipiLANi messialal paiknevad ettevõtted, lisategevused ja toimuvad loengud.",
"areas": {
"bar": "Baariala",
"boardGames": "Lauamängude ala",
"simRacing": "Red Bull Sim Racing",
"fighting": "Võitlusmängu ala",
"photobooth": "Fotoboks",
"ityk": "IT-teaduskonna üliõpilaskogu",
"tartuyk": "Tartu Ülikool",
"estoniagamedev": "Eesti Gamedev",
"info": "Infoboks",
"tly": "Tallinna Ülikool",
"ittk": "TalTech IT-Teaduskond",
"gameup": "GameUP!"
},
"hide": "Peida seinad",
"show": "Näita seinu"
},
"rules": {
"title": "Reeglid",
"houseRules": "Kodukord",
"cs2Rules": "CS2 Reeglid",
"lolRules": "LoL Reeglid",
"miniRules": "Miniturniiride Reeglid"
},
"admin": {
"title": "Haldus",
"users": "Kasutajaid",
"teams": "Meeskondasid",
"success": {
"title": "Toiming oli edukas!",
"description": "Andmebaasi andmed on uuendatud."
},
"sync": {
"title": "Kas soovite värskendada andmebaasi?",
"description1": "See tõmbab Fientast praegused andmed ning asendab",
"all": "KÕIK",
"description2": "olemasolevad andmed andmebaasis!",
"warning": "Kui sa ei ole kindel, vajuta \"Tühista\".",
"update": "Värskenda"
},
"roles": {
"captain": "Kapten",
"teammate": "Meeskonnaliige"
},
"table": {
"name": "Nimi",
"members": "Liikmed",
"noMembers": "Liikmeid puuduvad"
}
}
}
Loading…
Cancel
Save