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. 35
      src/app/layout.tsx
  21. 480
      src/app/messiala/page.tsx
  22. 23
      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. 47
      src/components/Footer.tsx
  28. 33
      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. 14
      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. 2
      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 createNextIntlPlugin from "next-intl/plugin";
const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts");
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"
},
"dependencies": {
"@libsql/client": "^0.15.9",
"@libsql/client": "^0.15.12",
"@paralleldrive/cuid2": "^2.2.2",
"@radix-ui/react-alert-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-select": "^2.2.6",
"@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",
"@types/three": "^0.178.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"drizzle-orm": "^0.44.2",
"gray-matter": "^4.0.3",
"drizzle-orm": "^0.44.4",
"lucide-react": "^0.522.0",
"material-symbols": "^0.31.8",
"material-symbols": "^0.31.9",
"next": "15.3.0",
"next-intl": "^4.3.4",
"next-themes": "^0.4.6",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"remark-gfm": "^4.0.1",
"tailwind-merge": "^3.3.1",
"three": "^0.178.0",
"tw-animate-css": "^1.3.4"
"tw-animate-css": "^1.3.7"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@tailwindcss/postcss": "^4.1.10",
"@types/bun": "^1.2.18",
"@types/node": "^20.19.1",
"@types/react": "^19.1.9",
"@tailwindcss/postcss": "^4.1.12",
"@types/bun": "^1.2.20",
"@types/node": "^20.19.11",
"@types/react": "^19.1.10",
"@types/react-dom": "^19.1.7",
"autoprefixer": "^10.4.21",
"dotenv": "^16.3.1",
"dotenv": "^16.6.1",
"drizzle-kit": "^0.31.4",
"eslint": "^9.29.0",
"eslint": "^9.33.0",
"eslint-config-next": "15.3.0",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.10",
"tailwindcss": "^4.1.12",
"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
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
import {
@ -19,19 +20,26 @@ import {
TableRow,
} from "@/components/ui/table";
// Later on we can use a i8 solution?
function translateRole(role: string): string {
// Function to translate roles using i18n
function translateRole(role: string, t: (key: string) => string): string {
switch (role) {
case "CAPTAIN":
return "Kapten";
return t("admin.roles.captain");
case "TEAMMATE":
return "Meeskonnaliige";
return t("admin.roles.teammate");
default:
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
const teams = await db.query.teams.findMany({
with: {
@ -54,7 +62,7 @@ export default async function AdminTeams() {
<h1
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>
</div>
<div className="text-2xl text-[#2A2C3F] dark:text-[#EEE5E5]">
@ -63,8 +71,8 @@ export default async function AdminTeams() {
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">ID</TableHead>
<TableHead>Nimi</TableHead>
<TableHead>Liikmed</TableHead>
<TableHead>{t("admin.table.name")}</TableHead>
<TableHead>{t("admin.table.members")}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@ -84,12 +92,14 @@ export default async function AdminTeams() {
{member.user.firstName} {member.user.lastName}
</span>
<span className="text-gray-500">
({translateRole(member.role)})
({translateRole(member.role, t)})
</span>
</div>
))
) : (
<span className="text-gray-500">Liikmeid puuduvad</span>
<span className="text-gray-500">
{t("admin.table.noMembers")}
</span>
)}
</div>
</TableCell>

@ -17,9 +17,10 @@ import {
X,
} from "lucide-react";
import Link from "next/link";
import { getTranslations, setRequestLocale } from "next-intl/server";
import { revalidatePath } from "next/cache";
import { redirect, RedirectType } from "next/navigation";
import NextLink from "next/link";
import { Button } from "@/components/ui/button";
@ -53,13 +54,13 @@ async function dismissAlert() {
redirect("/haldus", RedirectType.replace);
}
const SuccessAlertDB = () => {
const SuccessAlertDB = ({ t }: { t: (key: string) => string }) => {
return (
<Alert className="flex items-start mt-8">
<CheckCircle2Icon className="mt-0.5" />
<div className="flex-1">
<AlertTitle>Toiming oli edukas!</AlertTitle>
<AlertDescription>Andmebaasi andmed on uuendatud.</AlertDescription>
<AlertTitle>{t("admin.success.title")}</AlertTitle>
<AlertDescription>{t("admin.success.description")}</AlertDescription>
</div>
<form action={dismissAlert} className="ml-2">
<Button
@ -76,10 +77,15 @@ const SuccessAlertDB = () => {
};
export default async function Admin({
params,
searchParams,
}: {
params: Promise<{ locale: string }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}) {
const { locale } = await params;
setRequestLocale(locale);
const t = await getTranslations({ locale });
const alarmStatus = await searchParams;
const showSuccess = alarmStatus.success === "true";
@ -97,31 +103,31 @@ export default async function Admin({
return (
<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">
<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">
arrow_left_alt
</span>
</Link>
</NextLink>
<h1
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>
</div>
<div className="text-2xl text-[#2A2C3F] dark:text-[#EEE5E5]">
<div className="pl-2 flex gap-8 pb-4">
<div className="flex text-lg md:text-2xl flex-row items-center">
<Users className="mr-2" />
Kasutajaid: {usersData.length}
{t("admin.users")}: {usersData.length}
</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">
<IdCardLanyard className="mr-2" />
Meeskondasid: {teamsData.length}
{t("admin.teams")}: {teamsData.length}
</div>
</Link>
</NextLink>
<AlertDialog>
<AlertDialogTrigger asChild>
<div className="ml-auto">
@ -135,24 +141,24 @@ export default async function Admin({
</div>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogTitle>
Kas soovite värskendada andmebaasi?
</AlertDialogTitle>
<AlertDialogTitle>{t("admin.sync.title")}</AlertDialogTitle>
<AlertDialogDescription>
See tõmbab Fientast praegused andmed ning asendab{" "}
<span className="text-red-600 font-semibold">KÕIK</span>{" "}
olemasolevad andmed andmebaasis!
{t("admin.sync.description1")}{" "}
<span className="text-red-600 font-semibold">
{t("admin.sync.all")}
</span>{" "}
{t("admin.sync.description2")}
<br />
<br />
Kui sa ei ole kindel, vajuta &quot;Tühista&quot;.
{t("admin.sync.warning")}
</AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel className="cursor-pointer">
Tühista
{t("common.cancel")}
</AlertDialogCancel>
<form action={syncAction}>
<AlertDialogAction type="submit" className="cursor-pointer">
Värskenda
{t("admin.sync.update")}
</AlertDialogAction>
</form>
</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 Link from "next/link";
import { Link } from "@/i18n/routing";
import { getTranslations, setRequestLocale } from "next-intl/server";
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 (
<div>
{/* Title */}
@ -25,7 +35,7 @@ export default function Home() {
<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]`}
>
Auhinnafond
{t("tournaments.prizePool")}
</h3>
<h2
className={`text-[clamp(2rem,1.2rem+4vw,6rem)] ${vipnagorgialla.className} leading-[90%] font-bold italic text-[#007CAB] dark:text-[#00A3E0]`}
@ -44,7 +54,7 @@ export default function Home() {
<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]`}
>
Ajakava
{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
@ -55,8 +65,7 @@ export default function Home() {
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">
TipiLAN on pungil põnevatest turniiridest, mini-võistlustest ja
paljust muust.
{t("home.sections.schedule.description")}
</p>
</div>
</Link>
@ -66,9 +75,9 @@ export default function Home() {
>
<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`}
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>
<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
@ -80,8 +89,7 @@ export default function Home() {
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">
TipiLANil toimuvad suurejoonelised CS2 ja LoL turniirid, mille
auhinnafond on 10 000.
{t("home.sections.tournaments.description")}
</p>
</div>
</Link>
@ -93,7 +101,7 @@ export default function Home() {
<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`}
>
Messiala
{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
@ -104,8 +112,7 @@ export default function Home() {
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">
TipiLANi messialal paiknevad ettevõtted, lisategevused ja toimuvad
loengud.
{t("home.sections.expo.description")}
</p>
</div>
</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">
<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>
<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
@ -133,10 +140,10 @@ export default function Home() {
>
<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">
TipiLANi tõmbab käima...
{t("home.sections.poweredBy")}
</h3>
<div className="flex flex-row flex-wrap gap-8 md:gap-18 items-center">
<Link href="https://taltech.ee" target="_blank">
<div className="flex flex-row flex-wrap gap-8 md:gap-18 items-center justify-center">
<NextLink href="https://taltech.ee" target="_blank">
<Image
src="/sponsors/taltech-color.png"
alt="Taltech (Tallinna Tehnikaülikool)"
@ -144,8 +151,8 @@ export default function Home() {
height={192}
className="object-contain"
/>
</Link>
<Link href="https://www.redbull.com/ee-et/" target="_blank">
</NextLink>
<NextLink href="https://www.redbull.com/ee-et/" target="_blank">
<Image
src="/sponsors/redbull.png"
alt="Redbull"
@ -153,8 +160,8 @@ export default function Home() {
height={80}
className="object-contain"
/>
</Link>
<Link href="https://www.alecoq.ee" target="_blank">
</NextLink>
<NextLink href="https://www.alecoq.ee" target="_blank">
<Image
src="/sponsors/alecoq.svg"
alt="Alecoq"
@ -162,8 +169,8 @@ export default function Home() {
height={200}
className="object-contain"
/>
</Link>
<Link href="https://www.simracing.ee/" target="_blank">
</NextLink>
<NextLink href="https://www.simracing.ee/" target="_blank">
<Image
src="/sponsors/EVAL.png"
alt="EVAL"
@ -171,8 +178,8 @@ export default function Home() {
height={200}
className="object-contain"
/>
</Link>
<Link href="https://balsnack.ee" target="_blank">
</NextLink>
<NextLink href="https://balsnack.ee" target="_blank">
<Image
src="/sponsors/balsnack.svg"
alt="Balsnack"
@ -180,8 +187,8 @@ export default function Home() {
height={200}
className="object-contain"
/>
</Link>
<Link
</NextLink>
<NextLink
href="https://www.rara.ee/sundmused/interaktiivne-videomangude-muuseum-lvlup/"
target="_blank"
>
@ -192,8 +199,11 @@ export default function Home() {
height={192}
className="object-contain"
/>
</Link>
<Link href="https://www.facebook.com/bfglOfficial" target="_blank">
</NextLink>
<NextLink
href="https://www.facebook.com/bfglOfficial"
target="_blank"
>
<Image
src="/sponsors/BFGL.png"
alt="BFGL"
@ -201,7 +211,16 @@ export default function Home() {
height={192}
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>

@ -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 Head from "next/head";
// Provides the theme context to the app
import { ThemeProvider } from "@/components/Theme-provider";
import { Work_Sans } from "next/font/google";
import "./globals.css";
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({
subsets: ["latin"],
});
@ -24,32 +14,15 @@ export const metadata: Metadata = {
export default function RootLayout({
children,
}: Readonly<{
}: {
children: React.ReactNode;
}>) {
}) {
return (
<html lang="en" 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>
<html suppressHydrationWarning>
<body
className={`${workSans.className} antialiased bg-[#EEE5E5] dark:bg-[#0E0F19]`}
>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<SidebarParent />
{children}
<Footer />
</ThemeProvider>
</body>
</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 { ThemeProvider } from "@/components/Theme-provider";
export default function NotFound() {
return (
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<div className="flex flex-col min-h-[90vh] p-12 justify-center items-center">
<h1 className={`text-7xl ${vipnagorgialla.className} font-bold italic uppercase text-[#2A2C3F] dark:text-[#EEE5E5] mt-8 mb-4`}>404</h1>
<p className="text-2xl text-[#2A2C3F] dark:text-[#EEE5E5]">Seda lehte me ei leidnud.</p>
<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>
</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,10 +1,14 @@
import { SiDiscord, SiInstagram, SiFacebook } from "react-icons/si";
import Image from "next/image";
import { useTranslations } from "next-intl";
// Fonts
import { vipnagorgialla } from "@/components/Vipnagorgialla";
const Footer = () => (
const Footer = () => {
const t = useTranslations();
return (
<div className="flex flex-col justify-center sm:justify-between px-6 py-8 md:px-12 md:py-16 gap-4 md:gap-8">
<div className="flex md:items-center gap-8 md:gap-0 justify-between flex-col md:flex-row">
<div className="flex flex-col items-start md:items-center">
@ -34,7 +38,7 @@ const Footer = () => (
<SiDiscord
title="Discord"
size={"2em"}
className="text-[#2A2C3F] dark:text-[#EEE5E5]"
className="text-[#2A2C3F] dark:text-[#EEE5E5] hover:text-[#007CAB] hover:dark:text-[#00A3E0] transition"
/>
</a>
<a
@ -46,7 +50,7 @@ const Footer = () => (
<SiInstagram
title="Instagram"
size={"2em"}
className="text-[#2A2C3F] dark:text-[#EEE5E5]"
className="text-[#2A2C3F] dark:text-[#EEE5E5] hover:text-[#007CAB] hover:dark:text-[#00A3E0] transition"
/>
</a>
<a
@ -58,7 +62,7 @@ const Footer = () => (
<SiFacebook
title="Facebook"
size={"2em"}
className="text-[#2A2C3F] dark:text-[#EEE5E5]"
className="text-[#2A2C3F] dark:text-[#EEE5E5] hover:text-[#007CAB] hover:dark:text-[#00A3E0] transition"
/>
</a>
</div>
@ -67,11 +71,11 @@ const Footer = () => (
<h2
className={`text-3xl sm:text-4xl ${vipnagorgialla.className} font-bold italic uppercase text-[#2A2C3F] dark:text-[#EEE5E5] mt-8 mb-4`}
>
Kontakt
{t("footer.contact")}
</h2>
<div className="flex flex-row justify-between gap-4 items-center">
<div>
<h3 className="text-xl font-bold">IT-teaduskonna üliõpilaskogu</h3>
<h3 className="text-xl font-bold">{t("footer.studentUnion")}</h3>
<div className="flex flex-col gap-2 mt-2">
<div className="flex flex-row gap-2">
<span className="material-symbols-outlined !font-bold text-[#007CAB] dark:text-[#00A3E0]">
@ -90,22 +94,41 @@ const Footer = () => (
</a>
</div>
</div>
<h3 className="text-xl font-bold pt-4">MTÜ For Tsükkel</h3>
<h3 className="text-xl font-bold pt-4">
{t("footer.organization")}
</h3>
<div>
<p className="text-[#aaa]">
Registrikood:{" "}
<p>
{t("footer.registrationCode")}:{" "}
<span className="font-semibold text-[#007CAB] dark:text-[#00A3E0]">
80391807
</span>
</p>
<p className="text-[#aaa]">
ICO-210, Raja tn 4c, Tallinn, Harjumaa, 12616
</p>
<p className="">ICO-210, Raja tn 4c, Tallinn, Harjumaa, 12616</p>
</div>
</div>
</div>
<div className="block align-middle text-center pt-16">
{t("footer.madeBy")}{" "}
<a
target="_blank"
href="https://lapikud.ee/"
className="text-[#E3983E] font-bold"
>
MTÜ Lapikud
</a>{" "}
{t("footer.withHelpFrom")}{" "}
<a
target="_blank"
href="https://ituk.ee/"
className="bg-[#7B1642] font-bold not-dark:text-white"
>
MTÜ For Tsükkel/ITÜK
</a>
</div>
</div>
</div>
);
};
export default Footer;

@ -12,6 +12,8 @@ import {
// Theme Provider
import { useTheme } from "next-themes";
import LanguageSwitcher from "./LanguageSwitcher";
// Shadcn UI
import { Button } from "@/components/ui/button";
import {
@ -24,24 +26,30 @@ import {
// Fonts
// import { vipnagorgialla } from "@/components/Vipnagorgialla";
const Header = ({
isOpen,
toggleSidebar,
}: {
interface HeaderProps {
isOpen: boolean;
toggleSidebar: () => void;
}) => {
onToggle: () => void;
themeLabels: {
light: string;
dark: string;
system: string;
};
}
const Header = ({ isOpen, onToggle, themeLabels }: HeaderProps) => {
const { theme, setTheme } = useTheme();
return (
<header className="px-8 py-2 md:px-12 flex items-center bg-[#EEE5E5] dark:bg-[#0E0F19] border-b-3 border-[#1F5673] justify-between text-[#2A2C3F] dark:text-[#EEE5E5]">
<button onClick={toggleSidebar}>
<button onClick={onToggle}>
{isOpen ? (
<MdClose className="h-12 w-12 text-[#2A2C3F] dark:text-[#EEE5E5] cursor-pointer" />
) : (
<MdMenu className="h-12 w-12 text-[#2A2C3F] dark:text-[#EEE5E5] cursor-pointer" />
)}
</button>
<div className="flex items-center gap-2">
<LanguageSwitcher />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
@ -61,15 +69,17 @@ const Header = ({
disabled={theme === "light"}
>
<MdSunny className={theme === "light" ? "text-amber-500" : ""} />
<span>Hele</span>
<span>{themeLabels.light}</span>
</DropdownMenuItem>
<DropdownMenuItem
className={`text-lg ${theme === "dark" ? "bg-accent/50 font-medium" : ""}`}
onClick={() => setTheme("dark")}
disabled={theme === "dark"}
>
<MdModeNight className={theme === "dark" ? "text-blue-500" : ""} />
<span>Tume</span>
<MdModeNight
className={theme === "dark" ? "text-blue-500" : ""}
/>
<span>{themeLabels.dark}</span>
</DropdownMenuItem>
<DropdownMenuItem
className={`text-lg ${theme === "system" ? "bg-accent/50 font-medium" : ""}`}
@ -79,10 +89,11 @@ const Header = ({
<MdComputer
className={theme === "system" ? "text-green-500" : ""}
/>
<span>Süsteemipõhine</span>
<span>{themeLabels.system}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</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 { useState } from "react";
import Header from "./Header";
import Sidebar from "./Sidebar";
import SidebarLayoutServer from "./SidebarLayoutServer";
const SidebarParent = () => {
const [isOpen, setIsOpen] = useState(false);
const toggleSidebar = () => setIsOpen(!isOpen);
return (
<div className="fixed w-screen top-0 z-9999">
<Header isOpen={isOpen} toggleSidebar={toggleSidebar} />
<Sidebar isOpen={isOpen} toggleSidebar={toggleSidebar}/>
<SidebarLayoutServer />
</div>
);
};
// 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;

@ -1,8 +1,8 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
@ -32,8 +32,8 @@ const buttonVariants = cva(
variant: "default",
size: "default",
},
}
)
},
);
function Button({
className,
@ -43,17 +43,25 @@ function Button({
...props
}: React.ComponentProps<"button"> &
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 (
<Comp
<button
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...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
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
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**.

@ -1,33 +1,68 @@
export type ScheduleItem = {
time?: string; // Aeg on ajutine praegu kuna pole 100% kindlalt paigas
title: string;
location: string;
titleKey: string;
locationKey: string;
description?: string;
};
export const scheduleData: Record<string, ScheduleItem[]> = {
"24. oktoober": [
oct24: [
{
title: "League of Legends põhiturniir",
location: "Aula",
time: "-",
titleKey: "schedule.events.doorsOpen",
locationKey: "schedule.locations.registrationSetup",
time: "17:00",
},
{
title: "Miniturniirid",
location: "Tudengimaja",
time: "-",
titleKey: "schedule.events.mainTournamentsStart",
locationKey: "schedule.locations.auditorium",
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",
location: "Aula",
time: "-",
titleKey: "schedule.events.granTurismo",
locationKey: "schedule.locations.studentHouse",
time: "20:00",
},
{
title: "Miniturniirid",
location: "Tudengimaja",
time: "-",
titleKey: "schedule.events.doorsClose",
locationKey: "schedule.locations.auditoriumAndStudentHouse",
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