Add FA, EN, and AR static copy, with English LTR and catalog hidden outside Persian.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
51fb9f6a26
commit
6543c994c3
+87
-97
@@ -5,9 +5,9 @@ import { usePathname, useRouter } from "next/navigation";
|
||||
import { FormEvent, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import AccountMenu from "@/components/AccountMenu";
|
||||
import { blogPosts, navLinks, products, productsNav, site } from "@/data/site";
|
||||
|
||||
type Locale = "fa" | "en";
|
||||
import { products, site } from "@/data/site";
|
||||
import { localeOptions, showsCatalog } from "@/i18n/config";
|
||||
import { useLocale, useMessages, useSetLocale } from "@/i18n/locale-context";
|
||||
|
||||
function SearchIcon() {
|
||||
return (
|
||||
@@ -29,8 +29,11 @@ function ChevronIcon() {
|
||||
export default function Header() {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const locale = useLocale();
|
||||
const t = useMessages();
|
||||
const setLocale = useSetLocale();
|
||||
const catalog = showsCatalog(locale);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [locale, setLocale] = useState<Locale>("fa");
|
||||
const [query, setQuery] = useState("");
|
||||
const [searchOpen, setSearchOpen] = useState(false);
|
||||
const [productsOpen, setProductsOpen] = useState(false);
|
||||
@@ -38,17 +41,6 @@ export default function Header() {
|
||||
const searchRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const saved = window.localStorage.getItem("kaman-locale");
|
||||
if (saved === "en" || saved === "fa") setLocale(saved);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.lang = locale;
|
||||
document.documentElement.dir = locale === "fa" ? "rtl" : "ltr";
|
||||
window.localStorage.setItem("kaman-locale", locale);
|
||||
}, [locale]);
|
||||
|
||||
useEffect(() => {
|
||||
function onPointer(event: MouseEvent) {
|
||||
if (!searchRef.current?.contains(event.target as Node)) setSearchOpen(false);
|
||||
@@ -81,39 +73,30 @@ export default function Header() {
|
||||
const q = query.trim().toLowerCase();
|
||||
if (q.length < 2) return [];
|
||||
const pages = [
|
||||
...navLinks.map((link) => ({
|
||||
href: link.href,
|
||||
title: locale === "en" ? link.labelEn : link.label,
|
||||
})),
|
||||
{
|
||||
href: "/products",
|
||||
title: locale === "en" ? productsNav.labelEn : productsNav.label,
|
||||
},
|
||||
{
|
||||
href: "/blog",
|
||||
title: locale === "en" ? "Blog" : "وبلاگ",
|
||||
},
|
||||
{ href: "/", title: t.nav.home },
|
||||
...(catalog
|
||||
? [
|
||||
{ href: "/products", title: t.nav.products },
|
||||
{ href: "/blog", title: t.nav.blog },
|
||||
]
|
||||
: []),
|
||||
{ href: "/about", title: t.nav.about },
|
||||
{ href: "/contact", title: t.nav.contact },
|
||||
];
|
||||
const catalog = products.map((product) => ({
|
||||
href: `/products`,
|
||||
title: locale === "en" ? product.titleEn : product.title,
|
||||
}));
|
||||
const articles = blogPosts.map((post) => ({
|
||||
href: `/blog`,
|
||||
title: locale === "en" ? post.titleEn : post.title,
|
||||
}));
|
||||
return [...pages, ...catalog, ...articles].filter((item) => item.title.toLowerCase().includes(q)).slice(0, 8);
|
||||
}, [locale, query]);
|
||||
const catalogItems = catalog
|
||||
? t.categories.map((product) => ({
|
||||
href: `/#product-${product.id}`,
|
||||
title: product.title,
|
||||
}))
|
||||
: [];
|
||||
return [...pages, ...catalogItems].filter((item) => item.title.toLowerCase().includes(q)).slice(0, 8);
|
||||
}, [catalog, query, t]);
|
||||
|
||||
function onSearch(event: FormEvent) {
|
||||
event.preventDefault();
|
||||
const first = results[0];
|
||||
if (!first) return;
|
||||
if (/^https?:\/\//i.test(first.href)) {
|
||||
window.open(first.href, "_blank", "noopener,noreferrer");
|
||||
} else {
|
||||
router.push(first.href);
|
||||
}
|
||||
router.push(first.href);
|
||||
setSearchOpen(false);
|
||||
setQuery("");
|
||||
}
|
||||
@@ -124,10 +107,10 @@ export default function Header() {
|
||||
<div className="header-start">
|
||||
<Link href="/" className="brand" onClick={() => setOpen(false)}>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src={site.logo} alt={site.shortName} width={32} height={32} />
|
||||
<img src={site.logo} alt={t.brand.shortName} width={32} height={32} />
|
||||
<span className="brand-text">
|
||||
<strong>{locale === "en" ? site.nameEnShort : site.shortName}</strong>
|
||||
<span className="en">{site.nameEnShort}</span>
|
||||
<strong>{t.brand.shortName}</strong>
|
||||
{locale !== "en" ? <span className="en">{t.brand.nameEnShort}</span> : null}
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
@@ -135,58 +118,66 @@ export default function Header() {
|
||||
type="button"
|
||||
className="menu-toggle"
|
||||
aria-expanded={open}
|
||||
aria-label={locale === "en" ? "Menu" : "منو"}
|
||||
aria-label={t.nav.menu}
|
||||
onClick={() => setOpen((value) => !value)}
|
||||
>
|
||||
{locale === "en" ? "Menu" : "منو"}
|
||||
{t.nav.menu}
|
||||
</button>
|
||||
|
||||
<nav className={open ? "nav open" : "nav"}>
|
||||
{navLinks.slice(0, 1).map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className={pathname === "/" && !hash.startsWith("#product") && hash !== "#products" ? "active" : undefined}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
{locale === "en" ? link.labelEn : link.label}
|
||||
</Link>
|
||||
))}
|
||||
|
||||
<div
|
||||
className={productsOpen ? "nav-dropdown open" : "nav-dropdown"}
|
||||
onMouseEnter={() => setProductsOpen(true)}
|
||||
onMouseLeave={() => setProductsOpen(false)}
|
||||
<Link
|
||||
href="/"
|
||||
className={pathname === "/" && !hash.startsWith("#product") && hash !== "#products" ? "active" : undefined}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
<Link
|
||||
href={productsNav.href}
|
||||
className={pathname.startsWith("/products") || hash === "#products" || hash.startsWith("#product-") ? "active" : undefined}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
{locale === "en" ? productsNav.labelEn : productsNav.label}
|
||||
<ChevronIcon />
|
||||
</Link>
|
||||
<ul className="nav-sub">
|
||||
{products.map((product) => (
|
||||
<li key={product.id}>
|
||||
<Link href={`/#product-${product.id}`} onClick={() => setOpen(false)}>
|
||||
{locale === "en" ? product.titleEn : product.title}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
{t.nav.home}
|
||||
</Link>
|
||||
|
||||
{navLinks.slice(1).map((link) => (
|
||||
{catalog ? (
|
||||
<div
|
||||
className={productsOpen ? "nav-dropdown open" : "nav-dropdown"}
|
||||
onMouseEnter={() => setProductsOpen(true)}
|
||||
onMouseLeave={() => setProductsOpen(false)}
|
||||
>
|
||||
<Link
|
||||
href="/products"
|
||||
className={pathname.startsWith("/products") || hash === "#products" || hash.startsWith("#product-") ? "active" : undefined}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
{t.nav.products}
|
||||
<ChevronIcon />
|
||||
</Link>
|
||||
<ul className="nav-sub">
|
||||
{products.map((product) => {
|
||||
const copy = t.categories.find((item) => item.id === product.id);
|
||||
return (
|
||||
<li key={product.id}>
|
||||
<Link href={`/#product-${product.id}`} onClick={() => setOpen(false)}>
|
||||
{copy?.title || product.title}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{catalog ? (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className={pathname === link.href ? "active" : undefined}
|
||||
href="/blog"
|
||||
className={pathname.startsWith("/blog") ? "active" : undefined}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
{locale === "en" ? link.labelEn : link.label}
|
||||
{t.nav.blog}
|
||||
</Link>
|
||||
))}
|
||||
) : null}
|
||||
|
||||
<Link href="/about" className={pathname === "/about" ? "active" : undefined} onClick={() => setOpen(false)}>
|
||||
{t.nav.about}
|
||||
</Link>
|
||||
<Link href="/contact" className={pathname === "/contact" ? "active" : undefined} onClick={() => setOpen(false)}>
|
||||
{t.nav.contact}
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -194,7 +185,7 @@ export default function Header() {
|
||||
<div className={searchOpen ? "header-search open" : "header-search"} ref={searchRef}>
|
||||
<form onSubmit={onSearch}>
|
||||
<label className="sr-only" htmlFor="site-search">
|
||||
{locale === "en" ? "Search" : "جستجو"}
|
||||
{t.nav.search}
|
||||
</label>
|
||||
<input
|
||||
id="site-search"
|
||||
@@ -206,13 +197,13 @@ export default function Header() {
|
||||
setSearchOpen(true);
|
||||
}}
|
||||
onFocus={() => setSearchOpen(true)}
|
||||
placeholder={locale === "en" ? "Search" : "جستجو"}
|
||||
placeholder={t.nav.search}
|
||||
/>
|
||||
<button
|
||||
type={searchOpen && query.trim() ? "submit" : "button"}
|
||||
className="header-tool"
|
||||
aria-expanded={searchOpen}
|
||||
aria-label={locale === "en" ? "Search" : "جستجو"}
|
||||
aria-label={t.nav.search}
|
||||
onClick={() => {
|
||||
if (!searchOpen) setSearchOpen(true);
|
||||
}}
|
||||
@@ -240,14 +231,13 @@ export default function Header() {
|
||||
</div>
|
||||
|
||||
<label className="header-lang">
|
||||
<span className="sr-only">{locale === "en" ? "Language" : "زبان"}</span>
|
||||
<select
|
||||
value={locale}
|
||||
onChange={(event) => setLocale(event.target.value as Locale)}
|
||||
aria-label={locale === "en" ? "Language" : "انتخاب زبان"}
|
||||
>
|
||||
<option value="fa">FA</option>
|
||||
<option value="en">EN</option>
|
||||
<span className="sr-only">{t.nav.language}</span>
|
||||
<select value={locale} onChange={(event) => setLocale(event.target.value)} aria-label={t.nav.languageSelect}>
|
||||
{localeOptions.map((option) => (
|
||||
<option key={option.id} value={option.id}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user