110 lines
3.5 KiB
TypeScript
110 lines
3.5 KiB
TypeScript
import { useState, useRef, useEffect } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
const languages = [
|
|
{ code: "en", label: "EN", native: "English" },
|
|
{ code: "vi", label: "VI", native: "Tiếng Việt" },
|
|
{ code: "ja", label: "JA", native: "日本語" },
|
|
{ code: "ko", label: "KO", native: "한국어" },
|
|
{ code: "zh", label: "ZH", native: "中文" },
|
|
];
|
|
|
|
const LanguageSwitcher = () => {
|
|
const { i18n, t } = useTranslation();
|
|
const [open, setOpen] = useState(false);
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
|
|
const current = languages.find((l) => l.code === i18n.language) ?? languages[0];
|
|
|
|
useEffect(() => {
|
|
const handler = (e: MouseEvent) => {
|
|
if (ref.current && !ref.current.contains(e.target as Node)) {
|
|
setOpen(false);
|
|
}
|
|
};
|
|
document.addEventListener("mousedown", handler);
|
|
return () => document.removeEventListener("mousedown", handler);
|
|
}, []);
|
|
|
|
const select = (code: string) => {
|
|
i18n.changeLanguage(code);
|
|
setOpen(false);
|
|
};
|
|
|
|
return (
|
|
<div ref={ref} className="relative">
|
|
{/* Trigger */}
|
|
<button
|
|
onClick={() => setOpen((v) => !v)}
|
|
className="
|
|
btn btn-ghost px-2
|
|
flex items-center gap-1
|
|
text-sm font-semibold tracking-wider
|
|
text-white/80+
|
|
select-none
|
|
tooltip tooltip-bottom
|
|
"
|
|
data-tip={t("header.language")}
|
|
>
|
|
<span className="uppercase">{current.label}</span>
|
|
{/* Chevron */}
|
|
<svg
|
|
className={`w-3 h-3 text-white/50 transition-transform duration-200 ${open ? "rotate-180" : ""}`}
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
strokeWidth={2.5}
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
|
</svg>
|
|
</button>
|
|
|
|
{/* Dropdown */}
|
|
{open && (
|
|
<ul
|
|
className="
|
|
absolute right-0 mt-2 w-32
|
|
bg-black/60 backdrop-blur-md
|
|
border border-white/10 rounded-xl
|
|
shadow-2xl shadow-black/50
|
|
overflow-hidden z-999
|
|
py-1
|
|
animate-in fade-in slide-in-from-top-2 duration-150
|
|
"
|
|
>
|
|
{languages.map((lang) => {
|
|
const isActive = lang.code === i18n.language;
|
|
return (
|
|
<li key={lang.code}>
|
|
<button
|
|
onClick={() => select(lang.code)}
|
|
className={`
|
|
w-full flex items-center gap-3 px-4 py-2.5 text-sm
|
|
transition-colors duration-150
|
|
${isActive
|
|
? "bg-emerald-500/20 text-emerald-400"
|
|
: "text-white/70 hover:bg-white/8 hover:text-white"
|
|
}
|
|
`}
|
|
>
|
|
<span className="flex-1 text-left">{lang.native}</span>
|
|
{isActive && (
|
|
<svg className="w-3.5 h-3.5 text-emerald-400 shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
|
<path
|
|
fillRule="evenodd"
|
|
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
|
clipRule="evenodd"
|
|
/>
|
|
</svg>
|
|
)}
|
|
</button>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default LanguageSwitcher; |