Add English translation to page with next-intl

This commit is contained in:
2025-08-20 22:00:33 +03:00
parent 02b2cce115
commit c22afa28d9
32 changed files with 1487 additions and 754 deletions

View File

@@ -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"
/>
const Footer = () => {
const t = useTranslations();
return (
<div className="flex flex-col justify-center sm:justify-between px-6 py-8 md:px-12 md:py-16 gap-4 md:gap-8">
<div className="flex md:items-center gap-8 md:gap-0 justify-between flex-col md:flex-row">
<div className="flex flex-col items-start md:items-center">
<Image
src="/tipilan-white.svg"
width={250}
height={36}
alt="TipiLAN Logo"
className="h-9 dark:hidden"
/>
<Image
src="/tipilan-dark.svg"
width={250}
height={36}
alt="TipiLAN Logo"
className="h-9 not-dark:hidden"
/>
</div>
{/* Social media */}
<div className="flex flex-row">
<a
href="https://discord.gg/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>
{/* 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"
<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`}
>
<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 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>
<div className="flex flex-row gap-2">
<span className="material-symbols-outlined !font-bold text-[#007CAB] dark:text-[#00A3E0]">
phone
</span>
<a href="tel:+37256931193" className="underline">
+372 5693 1193
</a>
</div>
</div>
<h3 className="text-xl font-bold pt-4">MTÜ For Tsükkel</h3>
{t("footer.contact")}
</h2>
<div className="flex flex-row justify-between gap-4 items-center">
<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>
<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>
<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>
</div>
</div>
</div>
</div>
);
);
};
export default Footer;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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