91 lines
3.5 KiB
TypeScript
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>
|
|
);
|
|
}
|