diff --git a/next.config.ts b/next.config.ts index d6a1ca2..d71ac07 100644 --- a/next.config.ts +++ b/next.config.ts @@ -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); diff --git a/package.json b/package.json index 5f2e22a..ee82a4e 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/app/[locale]/ajakava/page.tsx b/src/app/[locale]/ajakava/page.tsx new file mode 100644 index 0000000..0615ffc --- /dev/null +++ b/src/app/[locale]/ajakava/page.tsx @@ -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 ( +
+
+

+ {t("schedule.title")} +

+ + {/* Tab menu */} +
+ {tabs.map((tab) => ( + + ))} +
+ + {/* Schedule entries */} +
+ {schedule.map((item, idx) => ( +
+
+ {item.time} +
+
+
+ {t(item.titleKey)} +
+ {item.description && ( +
+ {item.description} +
+ )} +
+ {t(item.locationKey)} +
+
+
+ ))} +
+
+ + +
+ ); +} diff --git a/src/app/haldus/meeskonnad/page.tsx b/src/app/[locale]/haldus/meeskonnad/page.tsx similarity index 75% rename from src/app/haldus/meeskonnad/page.tsx rename to src/app/[locale]/haldus/meeskonnad/page.tsx index d86a6a3..7dfe269 100644 --- a/src/app/haldus/meeskonnad/page.tsx +++ b/src/app/[locale]/haldus/meeskonnad/page.tsx @@ -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() {

- Haldus - Meeskonnad + {t("admin.title")} - {t("admin.teams")}

@@ -63,8 +71,8 @@ export default async function AdminTeams() { ID - Nimi - Liikmed + {t("admin.table.name")} + {t("admin.table.members")} @@ -84,12 +92,14 @@ export default async function AdminTeams() { {member.user.firstName} {member.user.lastName} - ({translateRole(member.role)}) + ({translateRole(member.role, t)})
)) ) : ( - Liikmeid puuduvad + + {t("admin.table.noMembers")} + )} diff --git a/src/app/haldus/page.tsx b/src/app/[locale]/haldus/page.tsx similarity index 77% rename from src/app/haldus/page.tsx rename to src/app/[locale]/haldus/page.tsx index bccd5b6..ee04ea7 100644 --- a/src/app/haldus/page.tsx +++ b/src/app/[locale]/haldus/page.tsx @@ -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 (
- Toiming oli edukas! - Andmebaasi andmed on uuendatud. + {t("admin.success.title")} + {t("admin.success.description")}
+ -
+ + {/* Tooltip */} + {hoveredRoom && (
- - Võitlusmängu ala - -
- -
-
-
- -
+ {hoveredRoom} +
+ )}
- - {/* Tooltip */} - {hoveredRoom && ( -
- {hoveredRoom} -
- )} + + ); } diff --git a/src/app/[locale]/not-found.tsx b/src/app/[locale]/not-found.tsx new file mode 100644 index 0000000..8330bb3 --- /dev/null +++ b/src/app/[locale]/not-found.tsx @@ -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 ( +
+

+ {t("title")} +

+

+ {t("message")} +

+
+ ); +} diff --git a/src/app/page.tsx b/src/app/[locale]/page.tsx similarity index 84% rename from src/app/page.tsx rename to src/app/[locale]/page.tsx index e4ecb0c..dd64748 100644 --- a/src/app/page.tsx +++ b/src/app/[locale]/page.tsx @@ -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 (
{/* Title */} @@ -25,7 +35,7 @@ export default function Home() {

- Auhinnafond + {t("tournaments.prizePool")}

- Ajakava + {t("navigation.schedule")}

arrow_right_alt @@ -55,8 +65,7 @@ export default function Home() { event_note

- TipiLAN on pungil põnevatest turniiridest, mini-võistlustest ja - paljust muust. + {t("home.sections.schedule.description")}

@@ -66,9 +75,9 @@ export default function Home() { >

- Turniirid + {t("navigation.tournaments")}

arrow_right_alt @@ -80,8 +89,7 @@ export default function Home() { trophy

- TipiLANil toimuvad suurejoonelised CS2 ja LoL turniirid, mille - auhinnafond on 10 000€. + {t("home.sections.tournaments.description")}

@@ -93,7 +101,7 @@ export default function Home() {

- Messiala + {t("navigation.expo")}

arrow_right_alt @@ -104,8 +112,7 @@ export default function Home() { weekend

- TipiLANi messialal paiknevad ettevõtted, lisategevused ja toimuvad - loengud. + {t("home.sections.expo.description")}

@@ -117,7 +124,7 @@ export default function Home() { >

- Bro­neeri oma koht juba täna! + {t("home.sections.reserveSpot")}

arrow_right_alt @@ -133,10 +140,10 @@ export default function Home() { >

- TipiLANi tõmbab käima... + {t("home.sections.poweredBy")}

- + Taltech (Tallinna Tehnikaülikool) - - + + Redbull - - + + Alecoq - - + + EVAL - - + + Balsnack - - + @@ -192,8 +199,11 @@ export default function Home() { height={192} className="object-contain" /> - - + + BFGL - +
diff --git a/src/app/[locale]/piletid/page.tsx b/src/app/[locale]/piletid/page.tsx new file mode 100644 index 0000000..d7fb889 --- /dev/null +++ b/src/app/[locale]/piletid/page.tsx @@ -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 ( +
+
+

+ {t("tickets.title")} +

+
+
+

+ {t("tickets.computerParticipant.price")} +

+

+ {t("tickets.computerParticipant.title")} +

+
    + {t + .raw("tickets.computerParticipant.features") + .map((feature: string, index: number) => ( +
  • + {feature} +
  • + ))} +
+ + + +
+
+

+ {t("tickets.competitor.price")} +

+

+ {t("tickets.competitor.title")} +

+
    + {t + .raw("tickets.competitor.features") + .map((feature: string, index: number) => ( +
  • + {feature} +
  • + ))} +
+ + + +
+ +
+

+ {t("tickets.visitor.price")} +

+

+ {t("tickets.visitor.title")} +

+
    + {t + .raw("tickets.visitor.features") + .map((feature: string, index: number) => ( +
  • + {feature} +
  • + ))} +
+ + + +
+
+
+ + +
+ ); +} diff --git a/src/app/reeglid/[slug]/page.tsx b/src/app/[locale]/reeglid/[slug]/page.tsx similarity index 84% rename from src/app/reeglid/[slug]/page.tsx rename to src/app/[locale]/reeglid/[slug]/page.tsx index d58f47e..3cdaaf0 100644 --- a/src/app/reeglid/[slug]/page.tsx +++ b/src/app/[locale]/reeglid/[slug]/page.tsx @@ -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) {

- {ruleData.title} + {t(ruleData.titleKey)}

diff --git a/src/app/reeglid/page.tsx b/src/app/[locale]/reeglid/page.tsx similarity index 61% rename from src/app/reeglid/page.tsx rename to src/app/[locale]/reeglid/page.tsx index 167893e..ba27e02 100644 --- a/src/app/reeglid/page.tsx +++ b/src/app/[locale]/reeglid/page.tsx @@ -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() {

- REEGLID + {t("rules.title")}

- +
-

Kodukord

+

{t("rules.houseRules")}

- +
- +
-

CS2 reeglid

+

{t("rules.cs2Rules")}

- +
- +
-

LoL reeglid

+

{t("rules.lolRules")}

- +
{/* Minitourn. link coming soon*/} {/**/}
-

Miniturniiride reeglid

+

+ {t("tournaments.mini.titleSingular")} {t("rules.title")} +

{/**/}
diff --git a/src/app/striim/page.tsx b/src/app/[locale]/striim/page.tsx similarity index 93% rename from src/app/striim/page.tsx rename to src/app/[locale]/striim/page.tsx index 1d1d7bf..c2e68ab 100644 --- a/src/app/striim/page.tsx +++ b/src/app/[locale]/striim/page.tsx @@ -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 (
@@ -30,7 +38,7 @@ export default function Home() {

- Ajakava + {t("navigation.schedule")}

arrow_right_alt @@ -41,8 +49,7 @@ export default function Home() { event_note

- TipiLAN on pungil põnevatest turniiridest, mini-võistlustest ja - paljust muust. + {t("home.sections.schedule.description")}

@@ -68,7 +75,7 @@ export default function Home() {

- Turniirid + {t("navigation.tournaments")}

arrow_right_alt @@ -80,8 +87,7 @@ export default function Home() { trophy

- TipiLANil toimuvad suurejoonelised CS2 ja LoL turniirid, mille - auhinnafond on 10 000€. + {t("home.sections.tournaments.description")}

@@ -93,7 +99,7 @@ export default function Home() {

- Messiala + {t("navigation.expo")}

arrow_right_alt @@ -104,8 +110,7 @@ export default function Home() { weekend

- TipiLANi messialal paiknevad ettevõtted, lisategevused ja toimuvad - loengud. + {t("home.sections.expo.description")}

@@ -117,7 +122,7 @@ export default function Home() { >

- Bro­neeri oma koht juba täna! + {t("home.sections.reserveSpot")}

arrow_right_alt @@ -133,7 +138,7 @@ export default function Home() { >

- TipiLANi tõmbab käima... + {t("home.sections.poweredBy")}

diff --git a/src/app/[locale]/turniirid/page.tsx b/src/app/[locale]/turniirid/page.tsx new file mode 100644 index 0000000..045cc24 --- /dev/null +++ b/src/app/[locale]/turniirid/page.tsx @@ -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 ( +
+

+ {t("tournaments.title")} +

+ +
+ {/* CS2 turniir */} +
+
+
+

+ {t("tournaments.cs2.title")} +

+

+ {t("tournaments.cs2.timing")} +

+

+ {t("tournaments.cs2.description1")} +

+
+

+ {t("tournaments.cs2.description2")} +

+
+ +
+ + + + + + +
+
+
+
+ {/* Outside div needs to remain so that overflow won't occur*/} + CS2 tournament +
+
+
+
+ + {/* LoL turniir */} +
+
+
+
+ {/* Outside div needs to remain so that overflow won't occur*/} + LoL tournament +
+
+
+

+ {t("tournaments.lol.title")} +

+

+ {t("tournaments.lol.timing")} +

+

+ {t("tournaments.lol.description1")} +

+
+

+ {t("tournaments.lol.description2")} +

+
+
+ + + + + + +
+
+
+
+ + {/* Mini-turniirid */} +
+
+
+

+ {t("tournaments.mini.title")} +

+

+ {t("tournaments.mini.timing")} +

+

+ {t("tournaments.mini.description1")} +

+
+

+ {t("tournaments.mini.description2")} +

+
+
+ + + + + + +
+
+
+
+ {/* Outside div needs to remain so that overflow won't occur*/} + mini tournaments +
+
+
+
+
+
+ ); +} diff --git a/src/app/ajakava/page.tsx b/src/app/ajakava/page.tsx deleted file mode 100644 index 8bd1850..0000000 --- a/src/app/ajakava/page.tsx +++ /dev/null @@ -1,77 +0,0 @@ -"use client"; - -import {useState} from "react"; -import {vipnagorgialla} from "@/components/Vipnagorgialla"; -import {scheduleData} from "@/data/timetable"; -import SectionDivider from "@/components/SectionDivider"; - -const tabs = Object.keys(scheduleData); - -export default function Timetable() { - const [activeTab, setActiveTab] = useState(tabs[0]); - const schedule = scheduleData[activeTab]; - - return ( -
-
-

- Ajakava -

- - {/* Tab menu */} -
- {tabs.map((tab) => ( - - ))} -
- - {/* Schedule entries */} -
- {schedule.map((item, idx) => ( -
-
- {item.time} -
-
-
- {item.title} -
- {item.description && ( -
- {item.description} -
- )} - {item.location && ( -
- {item.location} -
- )} -
-
- ))} -
-
- - -
- ); -} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 3b9c9c3..70e4eb3 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -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 ( - - - TipiLAN - - - + - - - {children} -