Files
ksan.dev/components/ui/LanguageSwitcher.tsx
T
2026-08-01 16:07:57 +02:00

91 lines
3.5 KiB
TypeScript

"use client";
import { useState, useRef, useEffect } from "react";
import { usePathname } from "next/navigation";
import Link from "next/link";
import { motion, AnimatePresence } from "framer-motion";
import { Globe } from "lucide-react";
import { locales, localeNames, localeFlags, type Locale } from "@/lib/dictionaries";
interface LanguageSwitcherProps {
locale: Locale;
}
export default function LanguageSwitcher({ locale }: LanguageSwitcherProps) {
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
const pathname = usePathname();
useEffect(() => {
function handleClick(e: MouseEvent) {
if (ref.current && !ref.current.contains(e.target as Node)) {
setOpen(false);
}
}
if (open) document.addEventListener("mousedown", handleClick);
return () => document.removeEventListener("mousedown", handleClick);
}, [open]);
// Build the equivalent path for the other locale
function getLocalePath(targetLocale: Locale) {
// Replace /en/... or /bs/... with the target locale
const segments = pathname.split("/");
segments[1] = targetLocale;
return segments.join("/") || `/${targetLocale}`;
}
return (
<div ref={ref} className="relative">
<button
type="button"
onClick={() => setOpen((v) => !v)}
className="flex h-9 items-center gap-1.5 rounded-lg border border-border/60 px-2.5 font-mono text-xs text-text-secondary transition-all hover:border-purple-400/40 hover:bg-purple-500/10 hover:text-text-primary"
aria-label="Change language"
>
<Globe className="h-3.5 w-3.5" strokeWidth={1.5} />
<span className="uppercase">{locale}</span>
</button>
<AnimatePresence>
{open && (
<motion.div
initial={{ opacity: 0, y: 6, scale: 0.97 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 6, scale: 0.97 }}
transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }}
className="absolute right-0 top-11 z-50 min-w-[140px] overflow-hidden rounded-xl border border-border/60 bg-surface/95 shadow-xl shadow-purple-500/10 backdrop-blur-xl"
>
{locales.map((l) => {
const isActive = l === locale;
return (
<Link
key={l}
href={getLocalePath(l)}
onClick={() => {
setOpen(false);
// Persist user's explicit language choice
document.cookie = `NEXT_LOCALE=${l}; path=/; max-age=31536000; SameSite=Lax`;
// Dismiss language banner since user chose explicitly
document.cookie = "lang-banner-dismissed=1; path=/; max-age=2592000; SameSite=Lax";
}}
className={`flex items-center gap-2.5 px-3 py-2.5 text-sm transition-colors ${
isActive
? "bg-purple-500/10 text-purple-300"
: "text-text-secondary hover:bg-white/5 hover:text-text-primary"
}`}
>
<span className="text-base">{localeFlags[l]}</span>
<span>{localeNames[l]}</span>
{isActive && (
<span className="ml-auto h-1.5 w-1.5 rounded-full bg-purple-400" />
)}
</Link>
);
})}
</motion.div>
)}
</AnimatePresence>
</div>
);
}