Initial commit
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user