Add the Kaman Cable Next.js storefront with Meshkee catalog, blog, auth, and static-image slots.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-31 17:01:01 +03:30
co-authored by Cursor
commit 51fb9f6a26
84 changed files with 12999 additions and 0 deletions
+259
View File
@@ -0,0 +1,259 @@
"use client";
import Link from "next/link";
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";
function SearchIcon() {
return (
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" aria-hidden>
<circle cx="11" cy="11" r="6.25" stroke="currentColor" strokeWidth="1.75" />
<path d="M16 16.5 20 20.5" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" />
</svg>
);
}
function ChevronIcon() {
return (
<svg className="nav-caret" width="10" height="10" viewBox="0 0 12 12" fill="none" aria-hidden>
<path d="M2.5 4.5 6 8l3.5-3.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
</svg>
);
}
export default function Header() {
const pathname = usePathname();
const router = useRouter();
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);
const [hash, setHash] = useState("");
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);
}
function onKey(event: KeyboardEvent) {
if (event.key === "Escape") setSearchOpen(false);
}
document.addEventListener("mousedown", onPointer);
document.addEventListener("keydown", onKey);
return () => {
document.removeEventListener("mousedown", onPointer);
document.removeEventListener("keydown", onKey);
};
}, []);
useEffect(() => {
if (searchOpen) inputRef.current?.focus();
}, [searchOpen]);
useEffect(() => {
function syncHash() {
setHash(window.location.hash);
}
syncHash();
window.addEventListener("hashchange", syncHash);
return () => window.removeEventListener("hashchange", syncHash);
}, [pathname]);
const results = useMemo(() => {
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" : "وبلاگ",
},
];
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]);
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);
}
setSearchOpen(false);
setQuery("");
}
return (
<header className="site-header">
<div className="container header-inner">
<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} />
<span className="brand-text">
<strong>{locale === "en" ? site.nameEnShort : site.shortName}</strong>
<span className="en">{site.nameEnShort}</span>
</span>
</Link>
<button
type="button"
className="menu-toggle"
aria-expanded={open}
aria-label={locale === "en" ? "Menu" : "منو"}
onClick={() => setOpen((value) => !value)}
>
{locale === "en" ? "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={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>
{navLinks.slice(1).map((link) => (
<Link
key={link.href}
href={link.href}
className={pathname === link.href ? "active" : undefined}
onClick={() => setOpen(false)}
>
{locale === "en" ? link.labelEn : link.label}
</Link>
))}
</nav>
</div>
<div className="header-tools">
<div className={searchOpen ? "header-search open" : "header-search"} ref={searchRef}>
<form onSubmit={onSearch}>
<label className="sr-only" htmlFor="site-search">
{locale === "en" ? "Search" : "جستجو"}
</label>
<input
id="site-search"
ref={inputRef}
value={query}
tabIndex={searchOpen ? 0 : -1}
onChange={(event) => {
setQuery(event.target.value);
setSearchOpen(true);
}}
onFocus={() => setSearchOpen(true)}
placeholder={locale === "en" ? "Search" : "جستجو"}
/>
<button
type={searchOpen && query.trim() ? "submit" : "button"}
className="header-tool"
aria-expanded={searchOpen}
aria-label={locale === "en" ? "Search" : "جستجو"}
onClick={() => {
if (!searchOpen) setSearchOpen(true);
}}
>
<SearchIcon />
</button>
</form>
{searchOpen && results.length > 0 ? (
<ul className="header-search-results">
{results.map((item) => (
<li key={`${item.href}-${item.title}`}>
<Link
href={item.href}
onClick={() => {
setSearchOpen(false);
setQuery("");
}}
>
{item.title}
</Link>
</li>
))}
</ul>
) : null}
</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>
</select>
</label>
<AccountMenu />
</div>
</div>
</header>
);
}