Add Saleh Kargar factory website with FA/AR i18n.

Clone of salehkargar.ir as a Next.js site with localized pages, SEO, and product gallery.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-14 13:35:07 +03:30
co-authored by Cursor
parent ab5425663f
commit eccbb5d79d
111 changed files with 4456 additions and 114 deletions
+89
View File
@@ -0,0 +1,89 @@
"use client";
import Image from "next/image";
import { useTranslations } from "next-intl";
import { useState } from "react";
import { Link } from "@/i18n/navigation";
const tabIds = ["work", "goal", "services"] as const;
export default function AboutTabs() {
const t = useTranslations("AboutTabs");
const [active, setActive] = useState(0);
const activeId = tabIds[active];
return (
<section className="section about-tabs">
<div className="container">
<div className="about-layout">
<div className="about-media">
<Link href="/" className="zoom-box">
<Image
src="/images/featurebox/2.jpg"
alt={t("sampleWork")}
width={400}
height={500}
/>
<span className="zoom-label">{t("sampleWork")}</span>
</Link>
<div className="about-side-photo">
<Image
src="/images/featurebox/1.jpg"
alt={t("factoryView")}
width={400}
height={500}
/>
</div>
</div>
<div className="about-panel">
<div className="tabs-nav" role="tablist">
{tabIds.map((id, index) => (
<button
key={id}
type="button"
role="tab"
aria-selected={active === index}
className={active === index ? "is-active" : ""}
onClick={() => setActive(index)}
>
{t(`tabs.${id}.label`)}
</button>
))}
</div>
<div className="tabs-panel" role="tabpanel">
<h2>{t(`tabs.${activeId}.title`)}</h2>
<h3 className="tabs-subtitle">
{t(`tabs.${activeId}.subtitle`)
.split("\n")
.map((line) => (
<span key={line}>
{line}
<br />
</span>
))}
</h3>
<p className="tabs-body">
{t(`tabs.${activeId}.body`)
.split("\n")
.map((line) => (
<span key={line}>
{line}
<br />
</span>
))}
</p>
<Link
href={activeId === "goal" ? "/aboutlime" : "/aboutus"}
className="btn-theme"
>
{t(`tabs.${activeId}.cta`)}
</Link>
</div>
</div>
</div>
</div>
</section>
);
}
+68
View File
@@ -0,0 +1,68 @@
import { getTranslations } from "next-intl/server";
const phones = [
{ key: "accounting", href: "tel:+982538700105", text: "8700105-0253" },
{ key: "sales", href: "tel:+982538700061", text: "4-8700061-0253" },
{ key: "loading", href: "tel:+982538700074", text: "8700074-8700683-0253" },
] as const;
export default async function Footer() {
const t = await getTranslations("Footer");
return (
<footer className="site-footer">
<div className="footer-action">
<div className="container">
<h3>{t("brand")}</h3>
</div>
</div>
<div className="footer-widgets">
<div className="container footer-grid">
<aside>
<h4>{t("aboutTitle")}</h4>
<p>{t("aboutText")}</p>
</aside>
<aside>
<h4>{t("addressTitle")}</h4>
<address className="footer-address">{t("address")}</address>
<ul>
<li>
<span className="bullet" aria-hidden />
{t("noteFactory")}
</li>
<li>
<span className="bullet" aria-hidden />
{t("noteSlogan")}
</li>
</ul>
</aside>
<aside>
<h4>{t("contactTitle")}</h4>
<ul>
{phones.map((item) => (
<li key={item.key}>
<span className="bullet" aria-hidden />
<span>
{t(item.key)} :{" "}
<a href={item.href} className="footer-phone">
{item.text}
</a>
</span>
</li>
))}
</ul>
</aside>
</div>
</div>
<div className="footer-copy">
<div className="container">
<p>{t("copyright")}</p>
</div>
</div>
</footer>
);
}
+216
View File
@@ -0,0 +1,216 @@
"use client";
import Image from "next/image";
import { useLocale, useTranslations } from "next-intl";
import { useEffect, useRef, useState } from "react";
import { Link, usePathname, useRouter } from "@/i18n/navigation";
import { routing, type AppLocale } from "@/i18n/routing";
const navLinks = [
{ key: "home", href: "/" },
{ key: "products", href: "/products" },
{ key: "aboutChalk", href: "/aboutsuperchalk" },
{ key: "aboutLime", href: "/aboutlime" },
{ key: "gallery", href: "/gallery" },
{ key: "aboutUs", href: "/aboutus" },
] as const;
export default function Header() {
const t = useTranslations("Nav");
const tLang = useTranslations("Language");
const locale = useLocale();
const pathname = usePathname();
const router = useRouter();
const langRef = useRef<HTMLLIElement>(null);
const [scrolled, setScrolled] = useState(false);
const [open, setOpen] = useState(false);
const [langOpen, setLangOpen] = useState(false);
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 10);
onScroll();
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
useEffect(() => {
setOpen(false);
setLangOpen(false);
}, [pathname, locale]);
useEffect(() => {
document.body.classList.toggle("nav-open", open);
return () => document.body.classList.remove("nav-open");
}, [open]);
useEffect(() => {
if (!langOpen) return;
const onPointerDown = (event: MouseEvent) => {
if (!langRef.current?.contains(event.target as Node)) {
setLangOpen(false);
}
};
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") setLangOpen(false);
};
document.addEventListener("mousedown", onPointerDown);
document.addEventListener("keydown", onKeyDown);
return () => {
document.removeEventListener("mousedown", onPointerDown);
document.removeEventListener("keydown", onKeyDown);
};
}, [langOpen]);
const isActive = (href: string) => {
if (href === "/") return pathname === "/";
return pathname === href || pathname.startsWith(`${href}/`);
};
const switchLocale = (nextLocale: AppLocale) => {
router.replace(pathname, { locale: nextLocale });
setLangOpen(false);
};
return (
<header className={`site-header${scrolled ? " is-sticky" : ""}`}>
<div className="header-bar">
<div className="header-brand-row">
<div className="header-main">
<Link href="/" className="header-wordmark">
<Image
src="/images/header/logo.jpg"
alt={t("logoAlt")}
width={260}
height={44}
priority
/>
</Link>
<div className="header-divider" />
<div className="header-nav-row">
<button
type="button"
className={`menu-toggle${open ? " is-open" : ""}`}
aria-label={open ? t("closeMenu") : t("openMenu")}
aria-expanded={open}
aria-controls="main-navigation"
onClick={() => setOpen((v) => !v)}
>
<span />
<span />
<span />
</button>
<nav
id="main-navigation"
className={`main-nav${open ? " is-open" : ""}`}
aria-label={t("mainMenu")}
>
<ul>
<li className="nav-lang" ref={langRef}>
<button
type="button"
className={`nav-icon-btn lang-trigger${langOpen ? " is-open" : ""}`}
aria-label={tLang("label")}
aria-expanded={langOpen}
aria-haspopup="listbox"
onClick={() => setLangOpen((v) => !v)}
>
<GlobeIcon />
<span className="lang-current">
{locale === "ar" ? "AR" : "FA"}
</span>
</button>
{langOpen && (
<ul className="lang-menu" role="listbox" aria-label={tLang("label")}>
{routing.locales.map((item) => (
<li key={item} role="option" aria-selected={item === locale}>
<button
type="button"
className={`lang-option${item === locale ? " is-active" : ""}`}
onClick={() => switchLocale(item)}
>
<Image
src={`/images/languages/${item}.png`}
alt=""
width={20}
height={14}
/>
<span>{tLang(item)}</span>
</button>
</li>
))}
</ul>
)}
</li>
{navLinks.map((item) => (
<li key={item.href}>
<Link
href={item.href}
aria-current={isActive(item.href) ? "page" : undefined}
className={isActive(item.href) ? "is-active" : undefined}
onClick={() => setOpen(false)}
>
{t(item.key)}
</Link>
</li>
))}
<li>
<button type="button" className="nav-icon-btn" aria-label={t("login")}>
<UserIcon />
</button>
</li>
</ul>
</nav>
</div>
</div>
<Link href="/" className="header-crest" aria-label={t("logoAlt")}>
<Image
src="/images/header/logo1.png"
alt={t("logoAlt")}
width={92}
height={92}
priority
/>
</Link>
</div>
</div>
</header>
);
}
function GlobeIcon() {
return (
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" aria-hidden>
<circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="1.6" />
<path
d="M3 12h18M12 3c2.8 2.6 4.2 5.7 4.2 9s-1.4 6.4-4.2 9c-2.8-2.6-4.2-5.7-4.2-9S9.2 5.6 12 3z"
stroke="currentColor"
strokeWidth="1.6"
/>
</svg>
);
}
function UserIcon() {
return (
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" aria-hidden>
<circle cx="12" cy="8" r="3.2" stroke="currentColor" strokeWidth="1.6" />
<path
d="M5 19.5c1.8-3.2 4-4.7 7-4.7s5.2 1.5 7 4.7"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
/>
</svg>
);
}
+48
View File
@@ -0,0 +1,48 @@
import Image from "next/image";
import { getTranslations } from "next-intl/server";
export default async function Hero() {
const t = await getTranslations("Hero");
const services = [
{ title: t("transport"), image: "/images/slider/1.png" },
{ title: t("demolition"), image: "/images/slider/2.png" },
{ title: t("limeProduction"), image: "/images/slider/3.png" },
];
return (
<section className="hero">
<div className="hero-bg">
<Image
src="/images/slider/slider.jpg"
alt={t("imageAlt")}
fill
priority
className="hero-bg-img"
sizes="100vw"
/>
</div>
<div className="hero-overlay">
<div className="hero-center animate-fade-up">
<h1>{t("title")}</h1>
<hr className="hero-hr" />
<div className="hero-en">{t("subtitleEn")}</div>
</div>
<ul className="hero-services">
{services.map((item, i) => (
<li
key={item.title}
className="animate-fade-up"
style={{ animationDelay: `${0.2 + i * 0.12}s` }}
>
<Image src={item.image} alt={item.title} width={64} height={60} />
<span>{item.title}</span>
</li>
))}
</ul>
</div>
</section>
);
}
+105
View File
@@ -0,0 +1,105 @@
import { siteConfig } from "@/data/seo";
type JsonLdProps = {
data: Record<string, unknown> | Record<string, unknown>[];
};
export default function JsonLd({ data }: JsonLdProps) {
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
/>
);
}
export function organizationJsonLd() {
return {
"@context": "https://schema.org",
"@type": "Organization",
name: siteConfig.name,
alternateName: siteConfig.nameEn,
url: siteConfig.url,
logo: `${siteConfig.url}/images/header/logo1.png`,
image: `${siteConfig.url}${siteConfig.ogImage}`,
description: siteConfig.description,
slogan: siteConfig.slogan,
telephone: siteConfig.phone,
address: {
"@type": "PostalAddress",
addressLocality: siteConfig.address.locality,
streetAddress: siteConfig.address.street,
addressCountry: siteConfig.address.country,
},
};
}
export function localBusinessJsonLd() {
return {
"@context": "https://schema.org",
"@type": "LocalBusiness",
"@id": `${siteConfig.url}/#business`,
name: siteConfig.name,
alternateName: siteConfig.nameEn,
url: siteConfig.url,
image: `${siteConfig.url}${siteConfig.ogImage}`,
description: siteConfig.description,
telephone: siteConfig.phone[0],
address: {
"@type": "PostalAddress",
addressLocality: siteConfig.address.locality,
streetAddress: siteConfig.address.street,
addressCountry: siteConfig.address.country,
},
areaServed: {
"@type": "Country",
name: "Iran",
},
};
}
export function productJsonLd(product: {
title: string;
description: string;
image: string;
price: number;
slug: string;
}) {
return {
"@context": "https://schema.org",
"@type": "Product",
name: product.title,
description: product.description,
image: `${siteConfig.url}${product.image}`,
brand: {
"@type": "Brand",
name: siteConfig.shortName,
},
offers: {
"@type": "Offer",
url: `${siteConfig.url}/products/${product.slug}`,
priceCurrency: "IRR",
price: product.price,
availability: "https://schema.org/InStock",
seller: {
"@type": "Organization",
name: siteConfig.name,
},
},
};
}
export function breadcrumbJsonLd(
items: { name: string; path: string }[],
) {
return {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: items.map((item, index) => ({
"@type": "ListItem",
position: index + 1,
name: item.name,
item: `${siteConfig.url}${item.path}`,
})),
};
}
+52
View File
@@ -0,0 +1,52 @@
import Image from "next/image";
import { getTranslations } from "next-intl/server";
export default async function Location() {
const t = await getTranslations("Location");
return (
<section className="section location">
<div className="container">
<div className="section-head section-head-center">
<h2>{t("title")}</h2>
<Image
src="/images/infoitems/line.png"
alt=""
width={151}
height={5}
className="section-line"
/>
</div>
<div className="location-grid">
<div className="location-copy">
<Image
src="/images/info1/2.png"
alt=""
width={80}
height={80}
className="location-icon"
/>
<h3>{t("heading")}</h3>
<p>
{t("subtitle")}
<br />
<br />
{t("coverage")}
</p>
</div>
<div className="location-map">
<Image
src="/images/info1/1.png"
alt={t("mapAlt")}
width={700}
height={420}
className="location-map-img"
/>
</div>
</div>
</div>
</section>
);
}
+13
View File
@@ -0,0 +1,13 @@
type Props = {
title: string;
};
export default function PageHero({ title }: Props) {
return (
<section className="page-hero">
<div className="container">
<h1>{title}</h1>
</div>
</section>
);
}
+21
View File
@@ -0,0 +1,21 @@
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import PageHero from "@/components/PageHero";
type Props = {
title: string;
children: React.ReactNode;
};
export default function PageShell({ title, children }: Props) {
return (
<>
<Header />
<main>
<PageHero title={title} />
{children}
</main>
<Footer />
</>
);
}
+75
View File
@@ -0,0 +1,75 @@
import Image from "next/image";
import { getTranslations } from "next-intl/server";
const benefitKeys = [
"quality",
"lessCement",
"whiteness",
"easyWork",
"finish",
] as const;
const useKeys = ["whitework", "grout", "mosaic", "slurry"] as const;
export default async function PowderFeatures() {
const t = await getTranslations("Powder");
return (
<section className="section powder-features">
<div className="container">
<div className="section-head section-head-center">
<h2>{t("title")}</h2>
<Image
src="/images/infoitems/line.png"
alt=""
width={151}
height={5}
className="section-line"
/>
</div>
<div className="powder-grid">
<div className="powder-col">
<div className="powder-photo shadow-box">
<Image
src="/images/info/1.jpg"
alt={t("benefitsImageAlt")}
width={700}
height={480}
/>
</div>
<div className="powder-text">
<h3>{t("benefitsTitle")}</h3>
<p className="dots">. . . . . . . . . . . . . . . . . . . . . . . .</p>
<ul>
{benefitKeys.map((key) => (
<li key={key}>{t(`benefits.${key}`)}</li>
))}
</ul>
</div>
</div>
<div className="powder-col powder-col-reverse">
<div className="powder-text">
<h3>{t("usesTitle")}</h3>
<p className="dots">. . . . . . . . . . . . . . . . . . . . . . . . .</p>
<ul>
{useKeys.map((key) => (
<li key={key}>{t(`uses.${key}`)}</li>
))}
</ul>
</div>
<div className="powder-photo shadow-box">
<Image
src="/images/info/2.jpg"
alt={t("usesImageAlt")}
width={700}
height={480}
/>
</div>
</div>
</div>
</div>
</section>
);
}
+44
View File
@@ -0,0 +1,44 @@
import Image from "next/image";
import { getTranslations, getLocale } from "next-intl/server";
import type { Product } from "@/data/products";
import { formatPrice } from "@/data/products";
import { Link } from "@/i18n/navigation";
type Props = {
product: Product;
};
export default async function ProductDetail({ product }: Props) {
const t = await getTranslations("Products");
const locale = await getLocale();
const title = t(`items.${product.slug}.title`);
const description = t(`items.${product.slug}.description`);
return (
<section className="section product-detail">
<div className="container product-detail-grid">
<div className="product-detail-media shadow-box">
<Image
src={product.image}
alt={title}
width={800}
height={800}
priority
/>
</div>
<div className="product-detail-info">
<h2>{title}</h2>
<hr />
<p className="product-detail-desc">{description}</p>
<p className="product-detail-price">
{formatPrice(product.price, locale, t("currency"))}
</p>
<Link href="/products" className="btn-theme">
{t("backToProducts")}
</Link>
</div>
</div>
</section>
);
}
+40
View File
@@ -0,0 +1,40 @@
import Image from "next/image";
import { getTranslations, getLocale } from "next-intl/server";
import type { Product } from "@/data/products";
import { formatPrice } from "@/data/products";
import { Link } from "@/i18n/navigation";
type Props = {
products: Product[];
};
export default async function ProductGrid({ products }: Props) {
const t = await getTranslations("Products");
const locale = await getLocale();
return (
<div className="product-grid">
{products.map((product) => {
const title = t(`items.${product.slug}.title`);
return (
<article key={product.id} className="product-tile">
<Link href={`/products/${product.slug}`} className="product-tile-link">
<div className="product-tile-img">
<Image
src={product.image}
alt={title}
width={500}
height={500}
/>
</div>
<h2>{title}</h2>
<p className="product-tile-price">
{formatPrice(product.price, locale, t("currency"))}
</p>
</Link>
</article>
);
})}
</div>
);
}
+57
View File
@@ -0,0 +1,57 @@
import Image from "next/image";
import { getTranslations } from "next-intl/server";
const productionKeys = [
"clay",
"mineralPowder",
"joshaghan",
"superChalk",
"buildingLime",
"limestone",
"superPowder",
"sand",
] as const;
export default async function Productions() {
const t = await getTranslations("Productions");
return (
<section className="section productions">
<div className="container">
<div className="productions-grid">
<div className="productions-list">
<div className="section-head">
<h2>{t("title")}</h2>
<Image
src="/images/infoitems/line.png"
alt=""
width={151}
height={5}
className="section-line"
/>
</div>
<div className="production-items">
{productionKeys.map((key) => (
<article key={key} className="production-item">
<h3>{t(`items.${key}.title`)}</h3>
<p>{t(`items.${key}.description`)}</p>
</article>
))}
</div>
</div>
<div className="productions-visual">
<Image
src="/images/infoitems/1.png"
alt={t("imageAlt")}
width={640}
height={720}
className="productions-img"
/>
</div>
</div>
</div>
</section>
);
}
+88
View File
@@ -0,0 +1,88 @@
"use client";
import Image from "next/image";
import { useTranslations } from "next-intl";
import { useRef } from "react";
import { Link } from "@/i18n/navigation";
const carouselItems = [
{ key: "aryanClay", image: "/images/infoitems1/1.jpg" },
{ key: "ore", image: "/images/infoitems1/2.jpg" },
{ key: "superPowder", image: "/images/files/Bd4HF0Mm3G.jpg" },
{ key: "mineralPowder", image: "/images/infoitems1/4.jpg" },
{ key: "ore", image: "/images/infoitems1/2.jpg", id: "ore-2" },
{ key: "joshaghan", image: "/images/infoitems1/3.jpg" },
{ key: "superChalk", image: "/images/files/eADGpxlT4k.jpg" },
] as const;
export default function ProductsCarousel() {
const t = useTranslations("Carousel");
const trackRef = useRef<HTMLUListElement>(null);
const scrollBy = (dir: 1 | -1) => {
const el = trackRef.current;
if (!el) return;
const amount = el.clientWidth * 0.7;
el.scrollBy({ left: dir * -amount, behavior: "smooth" });
};
return (
<section className="section products-carousel">
<div className="container">
<div className="section-head section-head-center">
<h2>{t("title")}</h2>
<Image
src="/images/infoitems/line.png"
alt=""
width={151}
height={5}
className="section-line"
/>
</div>
<div className="carousel-wrap">
<button
type="button"
className="carousel-btn prev"
aria-label={t("prev")}
onClick={() => scrollBy(-1)}
>
‹
</button>
<ul className="carousel-track" ref={trackRef}>
{carouselItems.map((item, index) => {
const title = t(`items.${item.key}`);
return (
<li key={`${"id" in item ? item.id : item.key}-${index}`}>
<Link href="/products" className="product-card-link">
<article className="product-card">
<div className="product-card-img">
<Image
src={item.image}
alt={title}
width={700}
height={400}
/>
</div>
<p>{title}</p>
</article>
</Link>
</li>
);
})}
</ul>
<button
type="button"
className="carousel-btn next"
aria-label={t("next")}
onClick={() => scrollBy(1)}
>
›
</button>
</div>
</div>
</section>
);
}