Add English translation to page with next-intl

pull/48/head
v4ltages 4 months ago
parent 02b2cce115
commit c22afa28d9
No known key found for this signature in database
GPG Key ID: DC7BC38E0DC642B
  1. 5
      next.config.ts
  2. 1
      package.json
  3. 81
      src/app/[locale]/ajakava/page.tsx
  4. 32
      src/app/[locale]/haldus/meeskonnad/page.tsx
  5. 48
      src/app/[locale]/haldus/page.tsx
  6. 12
      src/app/[locale]/kodukord/page.tsx
  7. 38
      src/app/[locale]/layout.tsx
  8. 258
      src/app/[locale]/messiala/page.tsx
  9. 19
      src/app/[locale]/not-found.tsx
  10. 68
      src/app/[locale]/page.tsx
  11. 114
      src/app/[locale]/piletid/page.tsx
  12. 17
      src/app/[locale]/reeglid/[slug]/page.tsx
  13. 38
      src/app/[locale]/reeglid/page.tsx
  14. 29
      src/app/[locale]/striim/page.tsx
  15. 72
      src/app/[locale]/turniirid/page.tsx
  16. 81
      src/app/ajakava/page.tsx
  17. 37
      src/app/layout.tsx
  18. 29
      src/app/not-found.tsx
  19. 105
      src/app/piletid/page.tsx
  20. 195
      src/components/Footer.tsx
  21. 107
      src/components/Header.tsx
  22. 47
      src/components/LanguageSwitcher.tsx
  23. 88
      src/components/Sidebar.tsx
  24. 84
      src/components/SidebarLayoutClient.tsx
  25. 26
      src/components/SidebarLayoutServer.tsx
  26. 26
      src/components/SidebarParent.tsx
  27. 53
      src/data/timetable.ts
  28. 17
      src/i18n/request.ts
  29. 55
      src/i18n/routing.ts
  30. 20
      src/middleware.ts
  31. 216
      translations/en.json
  32. 215
      translations/et.json

@ -1,4 +1,7 @@
import type { NextConfig } from "next";
import createNextIntlPlugin from "next-intl/plugin";
const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts");
const nextConfig: NextConfig = {
async headers() {
@ -21,4 +24,4 @@ const nextConfig: NextConfig = {
},
};
export default nextConfig;
export default withNextIntl(nextConfig);

@ -30,6 +30,7 @@
"lucide-react": "^0.522.0",
"material-symbols": "^0.31.8",
"next": "15.3.0",
"next-intl": "^4.3.4",
"next-themes": "^0.4.6",
"react": "^19.1.1",
"react-dom": "^19.1.1",

@ -0,0 +1,81 @@
"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-row flex-wrap gap-5 items-stretch"
>
<div
className={`${vipnagorgialla.className} text-[#00A3E0] text-4xl font-bold italic flex-shrink-0 flex items-center justify-center`}
style={{ width: "180px", minWidth: "180px" }}
>
{item.time}
</div>
<div
className="flex-1 flex flex-col justify-center min-h-[120px]"
style={{ minWidth: "0" }}
>
<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-2xl text-[#938BA1] dark:text-[#938BA1] text-balance">
{item.description}
</div>
)}
<div className="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>

@ -2,8 +2,16 @@
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() {
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();
@ -14,7 +22,7 @@ export default async function Page() {
<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
{t("rules.houseRules")}
</h1>
<div className="prose prose-lg dark:prose-invert max-w-none">

@ -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>
);
}

@ -2,9 +2,10 @@
import { vipnagorgialla } from "@/components/Vipnagorgialla";
import * as THREE from "three";
import { useEffect, useRef, useState } from "react";
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 {
@ -16,6 +17,21 @@ export default function Expo() {
const [hoveredRoom, setHoveredRoom] = useState<string | null>(null);
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
const [showDividers, setShowDividers] = useState<boolean>(true);
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",
}),
[t],
);
useEffect(() => {
if (!mountRef.current) return;
@ -114,7 +130,7 @@ export default function Expo() {
x: 2.5,
z: 4,
color: roomColors[0],
name: "Lauamängude ala",
name: roomNames.boardGames,
},
{
width: 3.5,
@ -123,7 +139,7 @@ export default function Expo() {
x: 0.7,
z: -0.3,
color: roomColors[1],
name: "Baariala",
name: roomNames.bar,
},
{
width: 1.8,
@ -132,7 +148,7 @@ export default function Expo() {
x: 1,
z: -3.5,
color: roomColors[2],
name: "EVAL",
name: roomNames.eval,
},
{
width: 2,
@ -141,7 +157,7 @@ export default function Expo() {
x: 5.2,
z: -2,
color: roomColors[3],
name: "Red Bull Sim Racing",
name: roomNames.simRacing,
},
{
width: 3,
@ -150,7 +166,7 @@ export default function Expo() {
x: -1.7,
z: -3.5,
color: roomColors[4],
name: "Võitlusmängu ala",
name: roomNames.fighting,
},
// {
// width: 1.8,
@ -168,7 +184,7 @@ export default function Expo() {
x: -3.5,
z: -0.5,
color: roomColors[7],
name: "LVLup!",
name: roomNames.lvlup,
},
//{
// width: 2,
@ -186,7 +202,7 @@ export default function Expo() {
x: 3,
z: -3.5,
color: roomColors[8],
name: "Red Bull",
name: roomNames.redbull,
},
];
@ -370,7 +386,7 @@ export default function Expo() {
}
renderer.dispose();
};
}, []);
}, [roomNames]);
// Update dividers when showDividers state changes
useEffect(() => {
@ -380,124 +396,124 @@ export default function Expo() {
}, [showDividers]);
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`}
>
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: "#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: "#343434" }}
></div>
<span className="text-sm text-[#2A2C3F] dark:text-[#EEE5E5]">
Lauamängude ala
</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]">
Red Bull Sim Racing
</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]">
Võitlusmängu ala
</span>
</div>
<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">
{t("schedule.locations.studentHouse")}
</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: "#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 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 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>
{/* Tooltip */}
{hoveredRoom && (
<div className="flex items-center gap-2">
<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,
}}
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>
<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`}
>
{hoveredRoom}
</div>
)}
{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>
{/* 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>
<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">
<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,7 @@ export default function Home() {
height={192}
className="object-contain"
/>
</Link>
</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 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>
);
}

@ -2,23 +2,24 @@ 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 titles
// Map of valid slugs to their corresponding file paths and translation keys
const rulesMap = {
lol: {
filePath: "src/data/rules/lol.md",
title: "LOL Reeglid",
titleKey: "rules.lolRules",
},
cs2: {
filePath: "src/data/rules/cs2.md",
title: "CS2 Reeglid",
titleKey: "rules.cs2Rules",
},
} as const;
type RuleSlug = keyof typeof rulesMap;
interface PageProps {
params: Promise<{ slug: string }>;
params: Promise<{ slug: string; locale: string }>;
}
async function getRuleContent(slug: string) {
@ -33,7 +34,7 @@ async function getRuleContent(slug: string) {
const content = await file.text();
return {
content,
title: ruleConfig.title,
titleKey: ruleConfig.titleKey,
};
} catch (error) {
console.error(`Error reading rule file for slug ${slug}:`, error);
@ -42,7 +43,9 @@ async function getRuleContent(slug: string) {
}
export default async function RulePage({ params }: PageProps) {
const { slug } = await params;
const { slug, locale } = await params;
setRequestLocale(locale);
const t = await getTranslations({ locale });
const ruleData = await getRuleContent(slug);
if (!ruleData) {
@ -55,7 +58,7 @@ export default async function RulePage({ params }: PageProps) {
<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`}>
{ruleData.title}
{t(ruleData.titleKey)}
</h1>
<div className="prose prose-lg dark:prose-invert max-w-none">

@ -1,8 +1,16 @@
import { vipnagorgialla } from "@/components/Vipnagorgialla";
import Link from "next/link";
import SectionDivider from "@/components/SectionDivider";
export default function RulesMenu() {
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]`;
@ -15,32 +23,34 @@ export default function RulesMenu() {
<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`}>
REEGLID
{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">
<Link href="/kodukord">
<NextLink href="/kodukord">
<div className={`${boxStyle} bg-[#007CAB] py-20 px-8`}>
<h2 className={`${boxTextStyle}`}>Kodukord</h2>
<h2 className={`${boxTextStyle}`}>{t("rules.houseRules")}</h2>
</div>
</Link>
</NextLink>
<Link href="/reeglid/cs2">
<NextLink href="/reeglid/cs2">
<div className={`${boxStyle} bg-[#1F5673] py-20 px-8`}>
<h2 className={`${boxTextStyle}`}>CS2 reeglid</h2>
<h2 className={`${boxTextStyle}`}>{t("rules.cs2Rules")}</h2>
</div>
</Link>
</NextLink>
<Link href="reeglid/lol">
<NextLink href="reeglid/lol">
<div className={`${boxStyle} bg-[#007CAB] py-20 px-8`}>
<h2 className={`${boxTextStyle}`}>LoL reeglid</h2>
<h2 className={`${boxTextStyle}`}>{t("rules.lolRules")}</h2>
</div>
</Link>
</NextLink>
{/* Minitourn. link coming soon*/}
{/*<Link href="">*/}
<div className={`${boxStyle} bg-[#1F5673] py-16 px-8`}>
<h2 className={`${boxTextStyle}`}>Miniturniiride reeglid</h2>
<h2 className={`${boxTextStyle}`}>
{t("tournaments.mini.titleSingular")} {t("rules.title")}
</h2>
</div>
{/*</Link>*/}
</div>

@ -1,8 +1,16 @@
import { vipnagorgialla } from "@/components/Vipnagorgialla";
import Link from "next/link";
import Image from "next/image";
import { getTranslations, setRequestLocale } from "next-intl/server";
export default function Home() {
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">
@ -30,7 +38,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
@ -41,8 +49,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>
@ -68,7 +75,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`}
>
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 +87,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 +99,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 +110,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 +122,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,7 +138,7 @@ 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">

@ -1,42 +1,48 @@
import { vipnagorgialla } from "@/components/Vipnagorgialla";
import Link from "next/link";
import Image from "next/image";
import { getTranslations, setRequestLocale } from "next-intl/server";
export default function Tourney() {
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
className={`text-4xl md:text-5xl lg:text-6xl ${vipnagorgialla.className} font-bold italic uppercase
text-[#2A2C3F] dark:text-[#EEE5E5] md:m-16`}
>
Turniirid
{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}`}>CS2 turniir</h2>
<h2 className={`${headingStyle}`}>
{t("tournaments.cs2.title")}
</h2>
<p
className={
"text-2xl mb-4 text-neutral-500 group-hover:text-black"
}
>
Toimumisaeg veel selgumisel
{t("tournaments.cs2.timing")}
</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!
{t("tournaments.cs2.description1")}
</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.
{t("tournaments.cs2.description2")}
</p>
<br />
@ -45,14 +51,14 @@ export default function Tourney() {
<button
className={`px-4 py-2 bg-[#1F5673] cursor-pointer ${vipnagorgialla.className} font-bold italic text-[#ECE5E5]`}
>
LOE REEGLEID
{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]`}
>
OSTA PILET
{t("tournaments.cs2.buyTicket")}
</button>
</a>
</div>
@ -86,24 +92,22 @@ export default function Tourney() {
</div>
</div>
<div className="flex-auto text-right -skew-x-2 md:-skew-x-5">
<h2 className={`${headingStyle}`}>LoL turniir</h2>
<h2 className={`${headingStyle}`}>
{t("tournaments.lol.title")}
</h2>
<p
className={
"text-2xl mb-4 text-neutral-500 group-hover:text-black"
}
>
Toimumisaeg veel selgumisel
{t("tournaments.lol.timing")}
</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!
{t("tournaments.lol.description1")}
</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.
{t("tournaments.lol.description2")}
</p>
<br />
<div className="flex flex-row flex-wrap gap-4 md:gap-8 justify-end">
@ -111,14 +115,14 @@ export default function Tourney() {
<button
className={`px-4 py-2 bg-[#1F5673] cursor-pointer ${vipnagorgialla.className} font-bold italic text-[#ECE5E5]`}
>
LOE REEGLEID
{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]`}
>
OSTA PILET
{t("tournaments.lol.buyTicket")}
</button>
</a>
</div>
@ -130,24 +134,22 @@ export default function Tourney() {
<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}`}>Mini&shy;turniirid</h2>
<h2 className={`${headingStyle}`}>
{t("tournaments.mini.title")}
</h2>
<p
className={
"text-2xl mb-4 text-neutral-500 group-hover:text-black"
}
>
Toimumisaeg veel selgumisel
{t("tournaments.mini.timing")}
</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.
{t("tournaments.mini.description1")}
</p>
<br />
<p className="text-balance">
Auhinnafond on kõigi turniiride peale 1250 ja reeglina saab
rahalise auhinna miniturniiri võitja.
{t("tournaments.mini.description2")}
</p>
<br />
<div className="flex flex-row flex-wrap gap-4 md:gap-8">
@ -155,14 +157,14 @@ export default function Tourney() {
<button
className={`px-4 py-2 bg-[#1F5673] cursor-pointer ${vipnagorgialla.className} font-bold italic text-[#ECE5E5]`}
>
LOE REEGLEID
{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]`}
>
OSTA PILET
{t("tournaments.mini.buyTicket")}
</button>
</a>
</div>

@ -1,81 +0,0 @@
"use client";
import {useState} from "react";
import {vipnagorgialla} from "@/components/Vipnagorgialla";
import {scheduleData} from "@/data/timetable";
import SectionDivider from "@/components/SectionDivider";
const tabs = Object.keys(scheduleData);
export default function Timetable() {
const [activeTab, setActiveTab] = useState(tabs[0]);
const schedule = scheduleData[activeTab];
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`}
>
Ajakava
</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`}
>
{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 flex-wrap gap-5 items-stretch"
>
<div
className={`${vipnagorgialla.className} text-[#00A3E0] text-4xl font-bold italic flex-shrink-0 flex items-center justify-center`}
style={{ width: "180px", minWidth: "180px" }}
>
{item.time}
</div>
<div
className="flex-1 flex flex-col justify-center min-h-[120px]"
style={{ minWidth: "0" }}
>
<div
className={`${vipnagorgialla.className} text-3xl italic font-bold text-[#2A2C3F] dark:text-[#EEE5E5] text-balance`}
>
{item.title}
</div>
{item.description && (
<div className="text-2xl text-[#938BA1] dark:text-[#938BA1] text-balance">
{item.description}
</div>
)}
{item.location && (
<div className="text-2xl text-[#938BA1] dark:text-[#938BA1] text-balance">
{item.location}
</div>
)}
</div>
</div>
))}
</div>
</div>
<SectionDivider/>
</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>
{children}
</body>
</html>
);

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

@ -1,105 +0,0 @@
import {vipnagorgialla} from "@/components/Vipnagorgialla";
import Link from "next/link";
import SectionDivider from "@/components/SectionDivider";
export default function Tickets() {
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`}
>
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>
<SectionDivider />
</div>
);
}

@ -1,111 +1,118 @@
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 = () => (
<div className="flex flex-col justify-center sm:justify-between px-6 py-8 md:px-12 md:py-16 gap-4 md:gap-8">
<div className="flex md:items-center gap-8 md:gap-0 justify-between flex-col md:flex-row">
<div className="flex flex-col items-start md:items-center">
<Image
src="/tipilan-white.svg"
width={250}
height={36}
alt="TipiLAN Logo"
className="h-9 dark:hidden"
/>
<Image
src="/tipilan-dark.svg"
width={250}
height={36}
alt="TipiLAN Logo"
className="h-9 not-dark:hidden"
/>
</div>
{/* Social media */}
<div className="flex flex-row">
<a
href="https://discord.gg/eB7sVqgJ9b"
target="_blank"
className="mx-4 ml-0 md:ml-4"
rel="noopener noreferrer"
>
<SiDiscord
title="Discord"
size={"2em"}
className="text-[#2A2C3F] dark:text-[#EEE5E5]"
/>
</a>
<a
href="https://instagram.com/tipilan.ee"
target="_blank"
className="mx-4"
rel="noopener noreferrer"
>
<SiInstagram
title="Instagram"
size={"2em"}
className="text-[#2A2C3F] dark:text-[#EEE5E5]"
const Footer = () => {
const t = useTranslations();
return (
<div className="flex flex-col justify-center sm:justify-between px-6 py-8 md:px-12 md:py-16 gap-4 md:gap-8">
<div className="flex md:items-center gap-8 md:gap-0 justify-between flex-col md:flex-row">
<div className="flex flex-col items-start md:items-center">
<Image
src="/tipilan-white.svg"
width={250}
height={36}
alt="TipiLAN Logo"
className="h-9 dark:hidden"
/>
</a>
<a
href="https://facebook.com/tipilan.ee"
target="_blank"
className="mx-4"
rel="noopener noreferrer"
>
<SiFacebook
title="Facebook"
size={"2em"}
className="text-[#2A2C3F] dark:text-[#EEE5E5]"
<Image
src="/tipilan-dark.svg"
width={250}
height={36}
alt="TipiLAN Logo"
className="h-9 not-dark:hidden"
/>
</a>
</div>
{/* Social media */}
<div className="flex flex-row">
<a
href="https://discord.gg/eB7sVqgJ9b"
target="_blank"
className="mx-4 ml-0 md:ml-4"
rel="noopener noreferrer"
>
<SiDiscord
title="Discord"
size={"2em"}
className="text-[#2A2C3F] dark:text-[#EEE5E5]"
/>
</a>
<a
href="https://instagram.com/tipilan.ee"
target="_blank"
className="mx-4"
rel="noopener noreferrer"
>
<SiInstagram
title="Instagram"
size={"2em"}
className="text-[#2A2C3F] dark:text-[#EEE5E5]"
/>
</a>
<a
href="https://facebook.com/tipilan.ee"
target="_blank"
className="mx-4"
rel="noopener noreferrer"
>
<SiFacebook
title="Facebook"
size={"2em"}
className="text-[#2A2C3F] dark:text-[#EEE5E5]"
/>
</a>
</div>
</div>
</div>
<div className="flex flex-col">
<h2
className={`text-3xl sm:text-4xl ${vipnagorgialla.className} font-bold italic uppercase text-[#2A2C3F] dark:text-[#EEE5E5] mt-8 mb-4`}
>
Kontakt
</h2>
<div className="flex flex-row justify-between gap-4 items-center">
<div>
<h3 className="text-xl font-bold">IT-teaduskonna üliõpilaskogu</h3>
<div className="flex flex-col gap-2 mt-2">
<div className="flex flex-row gap-2">
<span className="material-symbols-outlined !font-bold text-[#007CAB] dark:text-[#00A3E0]">
mail
</span>
<a href="mailto:kontakt@ituk.ee" className="underline">
tipilan@ituk.ee
</a>
<div className="flex flex-col">
<h2
className={`text-3xl sm:text-4xl ${vipnagorgialla.className} font-bold italic uppercase text-[#2A2C3F] dark:text-[#EEE5E5] mt-8 mb-4`}
>
{t("footer.contact")}
</h2>
<div className="flex flex-row justify-between gap-4 items-center">
<div>
<h3 className="text-xl font-bold">{t("footer.studentUnion")}</h3>
<div className="flex flex-col gap-2 mt-2">
<div className="flex flex-row gap-2">
<span className="material-symbols-outlined !font-bold text-[#007CAB] dark:text-[#00A3E0]">
mail
</span>
<a href="mailto:kontakt@ituk.ee" className="underline">
tipilan@ituk.ee
</a>
</div>
<div className="flex flex-row gap-2">
<span className="material-symbols-outlined !font-bold text-[#007CAB] dark:text-[#00A3E0]">
phone
</span>
<a href="tel:+37256931193" className="underline">
+372 5693 1193
</a>
</div>
</div>
<div className="flex flex-row gap-2">
<span className="material-symbols-outlined !font-bold text-[#007CAB] dark:text-[#00A3E0]">
phone
</span>
<a href="tel:+37256931193" className="underline">
+372 5693 1193
</a>
<h3 className="text-xl font-bold pt-4">
{t("footer.organization")}
</h3>
<div>
<p className="text-[#aaa]">
{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>
</div>
</div>
<h3 className="text-xl font-bold pt-4">MTÜ For Tsükkel</h3>
<div>
<p className="text-[#aaa]">
Registrikood:{" "}
<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>
</div>
</div>
</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,65 +26,74 @@ 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>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="size-10 cursor-pointer"
>
<MdSunny className="scale-135 text-[#2A2C3F] dark:hidden" />
<MdModeNight className="scale-135 dark:text-[#EEE5E5] not-dark:hidden" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48 translate-y-4">
<DropdownMenuItem
className={`text-lg ${theme === "light" ? "bg-accent/50 font-medium" : ""}`}
onClick={() => setTheme("light")}
disabled={theme === "light"}
>
<MdSunny className={theme === "light" ? "text-amber-500" : ""} />
<span>Hele</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>
</DropdownMenuItem>
<DropdownMenuItem
className={`text-lg ${theme === "system" ? "bg-accent/50 font-medium" : ""}`}
onClick={() => setTheme("system")}
disabled={theme === "system"}
>
<MdComputer
className={theme === "system" ? "text-green-500" : ""}
/>
<span>Süsteemipõhine</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<div className="flex items-center gap-2">
<LanguageSwitcher />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="size-10 cursor-pointer"
>
<MdSunny className="scale-135 text-[#2A2C3F] dark:hidden" />
<MdModeNight className="scale-135 dark:text-[#EEE5E5] not-dark:hidden" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48 translate-y-4">
<DropdownMenuItem
className={`text-lg ${theme === "light" ? "bg-accent/50 font-medium" : ""}`}
onClick={() => setTheme("light")}
disabled={theme === "light"}
>
<MdSunny className={theme === "light" ? "text-amber-500" : ""} />
<span>{themeLabels.light}</span>
</DropdownMenuItem>
<DropdownMenuItem
className={`text-lg ${theme === "dark" ? "bg-accent/50 font-medium" : ""}`}
onClick={() => setTheme("dark")}
disabled={theme === "dark"}
>
<MdModeNight
className={theme === "dark" ? "text-blue-500" : ""}
/>
<span>{themeLabels.dark}</span>
</DropdownMenuItem>
<DropdownMenuItem
className={`text-lg ${theme === "system" ? "bg-accent/50 font-medium" : ""}`}
onClick={() => setTheme("system")}
disabled={theme === "system"}
>
<MdComputer
className={theme === "system" ? "text-green-500" : ""}
/>
<span>{themeLabels.system}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</header>
);
};

@ -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>
);
}

@ -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] 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}/>
</div>
);
return (
<div className="fixed w-screen top-0 z-9999">
<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.
// This component is responsible for rendering the sidebar and header together.
// Server-side translations are handled by SidebarLayoutServer.
export default SidebarParent;
export default SidebarParent;

@ -1,68 +1,67 @@
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: "Uksed avatakse",
location: "Registreerimine ja setup aulas",
titleKey: "schedule.events.doorsOpen",
locationKey: "schedule.locations.registrationSetup",
time: "17:00",
},
{
title: "Põhiturniirid algavad",
location: "Aula",
titleKey: "schedule.events.mainTournamentsStart",
locationKey: "schedule.locations.auditorium",
time: "20:00",
},
{
title: "Miniturniiride kick-off",
location: "Tudengimaja",
titleKey: "schedule.events.miniTournamentsKickoff",
locationKey: "schedule.locations.studentHouse",
time: "18:00",
},
{
title: "Fighting games turniiride algus",
location: "Tudengimaja",
titleKey: "schedule.events.fightingGamesStart",
locationKey: "schedule.locations.studentHouse",
time: "18:30",
},
{
title: "Uksed suletakse",
location: "Aula ja Tudengimaja",
titleKey: "schedule.events.doorsClose",
locationKey: "schedule.locations.auditoriumAndStudentHouse",
time: "*01:00",
},
],
"25. oktoober": [
oct25: [
{
title: "Uksed avatakse",
location: "Aula ja Tudengimaja",
titleKey: "schedule.events.doorsOpen",
locationKey: "schedule.locations.auditoriumAndStudentHouse",
time: "10:00",
},
{
title: "Miniturniirid algavad",
location: "Tudengimaja",
titleKey: "schedule.events.miniTournamentsStart",
locationKey: "schedule.locations.studentHouse",
time: "11:00",
},
{
title: "Granblue turniir",
location: "Tudengimaja",
titleKey: "schedule.events.granblue",
locationKey: "schedule.locations.studentHouse",
time: "11:30",
},
{
title: "Põhiturniirid algavad",
location: "Aula",
titleKey: "schedule.events.mainTournamentsStart",
locationKey: "schedule.locations.auditorium",
time: "12:00",
},
{
title: " Gran Turismo turniir",
location: "Tudengimaja",
titleKey: "schedule.events.granTurismo",
locationKey: "schedule.locations.studentHouse",
time: "20:00",
},
{
title: "Uksed suletakse",
location: "Aula ja Tudengimaja",
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: "/rules",
},
"/messiala": {
et: "/messiala",
en: "/expo",
},
"/piletid": {
et: "/piletid",
en: "/tickets",
},
"/reeglid": {
et: "/reeglid",
en: "/regulations",
},
"/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,216 @@
{
"navigation": {
"home": "Home",
"schedule": "Schedule",
"admin": "Admin",
"houserules": "House rules",
"expo": "Expo",
"tickets": "Tickets",
"rules": "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": {
"titleSingular": "Mini-tournament",
"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"
}
},
"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"
},
"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"
},
"hide": "Hide",
"show": "Show"
},
"rules": {
"title": "Rules",
"houseRules": "House Rules",
"cs2Rules": "CS2 Rules",
"lolRules": "LoL 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,215 @@
{
"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"
}
},
"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"
},
"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"
},
"hide": "Peida",
"show": "Näita"
},
"rules": {
"title": "Reeglid",
"houseRules": "Kodukord",
"cs2Rules": "CS2 Reeglid",
"lolRules": "LoL 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