diff --git a/app/(core)/components/Header.jsx b/app/(core)/components/Header.jsx index 12456db..6f9d682 100644 --- a/app/(core)/components/Header.jsx +++ b/app/(core)/components/Header.jsx @@ -10,7 +10,7 @@ import { useSticky } from "../hooks/useSticky"; import { useTheme } from "../hooks/useTheme"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faHamburger } from "@fortawesome/free-solid-svg-icons"; -import GoogleTranslator from "./GoogleTranslator.jsx"; +import LanguageSwitcher from "./LanguageSwitcher.jsx"; import { usePathname } from "next/navigation.js"; export default function Header() { @@ -129,8 +129,8 @@ export default function Header() {
- +
diff --git a/app/(core)/components/LanguageSwitcher.jsx b/app/(core)/components/LanguageSwitcher.jsx new file mode 100644 index 0000000..807d55f --- /dev/null +++ b/app/(core)/components/LanguageSwitcher.jsx @@ -0,0 +1,131 @@ +"use client"; + +import React, { useCallback, useEffect, useRef, useState } from "react"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faLanguage, faChevronDown } from "@fortawesome/free-solid-svg-icons"; +import useTranslation from "../hooks/useTranslation.ts"; +import "../styles/translator.css"; + +const LANGUAGES = { + en: "English", + ru: "Русский", +}; + +const getStoredLang = () => { + if (typeof document === "undefined") return "en"; + const value = `; ${document.cookie}`; + const parts = value.split("; googtrans="); + if (parts.length === 2) { + const cookie = parts.pop()?.split(";").shift(); + if (cookie) { + const segments = cookie.split("/"); + if (segments.length >= 3 && LANGUAGES[segments[2]]) { + return segments[2]; + } + } + } + return "en"; +}; + +const setLangCookie = (languageCode) => { + const hostname = window.location.hostname; + const isLocal = hostname === "localhost" || hostname === "127.0.0.1"; + const domainAttr = isLocal ? "" : `; domain=.${hostname}`; + document.cookie = `googtrans=/en/${languageCode}; path=/${domainAttr}; SameSite=Lax`; +}; + +export default function LanguageSwitcher() { + const { t, meta } = useTranslation(); + const isCompleted = meta?.completed || false; + const [currentLanguage, setCurrentLanguage] = useState("en"); + const [open, setOpen] = useState(false); + const containerRef = useRef(null); + + useEffect(() => { + setCurrentLanguage(getStoredLang()); + }, []); + + useEffect(() => { + if (typeof document === "undefined") return; + document.documentElement.lang = currentLanguage === "ru" ? "ru" : "en"; + }, [currentLanguage]); + + const changeLanguage = useCallback((languageCode) => { + setCurrentLanguage(languageCode); + setLangCookie(languageCode); + setOpen(false); + + window.dispatchEvent( + new CustomEvent("gtrans:languagechange", { + detail: { lang: languageCode }, + }) + ); + }, []); + + useEffect(() => { + if (!open) return; + + const handleClickOutside = (event) => { + if ( + containerRef.current && + !containerRef.current.contains(event.target) + ) { + setOpen(false); + } + }; + + const handleEscape = (event) => { + if (event.key === "Escape") setOpen(false); + }; + + document.addEventListener("mousedown", handleClickOutside); + document.addEventListener("keydown", handleEscape); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + document.removeEventListener("keydown", handleEscape); + }; + }, [open]); + + return ( +
+ + + {open ? ( + + ) : null} +
+ ); +} diff --git a/app/(core)/styles/index.css b/app/(core)/styles/index.css index b102e9a..4b627d5 100644 --- a/app/(core)/styles/index.css +++ b/app/(core)/styles/index.css @@ -650,6 +650,13 @@ nav a:hover::after { } .controls { + display: flex; + gap: 0.45rem; + position: relative; + z-index: 2200; + } + + .github-header-badge { display: none; } diff --git a/app/(core)/styles/translator.css b/app/(core)/styles/translator.css index d8abd83..b36a4a6 100644 --- a/app/(core)/styles/translator.css +++ b/app/(core)/styles/translator.css @@ -117,3 +117,114 @@ body { transform: scale(1); } } + +/* Compact language switcher button + dropdown */ +.lang-switcher { + position: relative; + flex-shrink: 0; + z-index: 2200; +} + +.lang-switcher__trigger { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.35rem; + height: 2.75rem; + min-width: 2.75rem; + padding: 0 0.65rem; + border: 1px solid var(--border-color); + border-radius: 0.75rem; + background: var(--bg-color); + color: var(--ph-fg-muted, var(--text-color)); + cursor: pointer; + box-shadow: 0 10px 20px rgba(15, 23, 42, 0.12); + transition: + border-color 0.2s ease, + box-shadow 0.2s ease, + background 0.2s ease; +} + +.lang-switcher__trigger:hover, +.lang-switcher__trigger:focus-visible { + border-color: var(--accent-color); + box-shadow: 0 0 0 2px rgba(0, 230, 230, 0.25); + outline: none; +} + +.lang-switcher__icon { + font-size: 1rem; + color: var(--accent-color); + pointer-events: none; +} + +.lang-switcher__code { + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.04em; + line-height: 1; + pointer-events: none; +} + +.lang-switcher__chevron { + font-size: 0.55rem; + opacity: 0.7; + transition: transform 0.2s ease; + pointer-events: none; +} + +.lang-switcher__chevron.is-open { + transform: rotate(180deg); +} + +.lang-switcher__menu { + position: absolute; + top: calc(100% + 0.4rem); + right: 0; + z-index: 2300; + min-width: 8.5rem; + margin: 0; + padding: 0.35rem; + list-style: none; + border: 1px solid var(--border-color); + border-radius: 0.75rem; + background: var(--bg-color); + box-shadow: 0 12px 28px rgba(15, 23, 42, 0.25); +} + +.lang-switcher__option { + display: block; + width: 100%; + padding: 0.55rem 0.75rem; + border: none; + border-radius: 0.5rem; + background: transparent; + color: var(--ph-fg-muted, var(--text-color)); + font-size: 0.9rem; + text-align: left; + cursor: pointer; + transition: background 0.15s ease; +} + +.lang-switcher__option:hover, +.lang-switcher__option:focus-visible { + background: rgba(0, 230, 230, 0.1); + outline: none; +} + +.lang-switcher__option.is-active { + color: var(--accent-color); + font-weight: 600; + background: rgba(0, 230, 230, 0.08); +} + +@media (max-width: 840px) { + .lang-switcher__code { + display: none; + } + + .lang-switcher__trigger { + padding: 0 0.55rem; + min-width: 2.75rem; + } +}