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
+72
View File
@@ -0,0 +1,72 @@
import type { Metadata } from "next";
import Image from "next/image";
import { getTranslations, setRequestLocale } from "next-intl/server";
import PageShell from "@/components/PageShell";
import { buildMetadata } from "@/lib/seo";
const sectionMeta = [
{ key: "history", image: "/images/about-lime/1.jpg" },
{ key: "traditional", image: "/images/about-lime/2.jpg" },
{ key: "difference", image: "/images/about-lime/3.jpg" },
{ key: "uses", image: "/images/products/building-lime.jpg" },
] as const;
type Props = {
params: Promise<{ locale: string }>;
};
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "Meta" });
return buildMetadata({
title: t("aboutLimeTitle"),
description: t("aboutLimeDescription"),
path: `/${locale}/aboutlime`,
locale,
});
}
export default async function AboutLimePage({ params }: Props) {
const { locale } = await params;
setRequestLocale(locale);
const t = await getTranslations("AboutLimePage");
return (
<PageShell title={t("title")}>
<section className="section content-page">
<div className="container">
{sectionMeta.map((section, index) => (
<article
key={section.key}
className={`content-block${index % 2 === 1 ? " is-reverse" : ""}`}
>
<div className="content-block-media shadow-box">
<Image
src={section.image}
alt={t(`sections.${section.key}.title`)}
width={700}
height={520}
/>
</div>
<div className="content-block-text">
<h2>{t(`sections.${section.key}.title`)}</h2>
<Image
src="/images/infoitems/line.png"
alt=""
width={151}
height={5}
className="section-line"
/>
{t(`sections.${section.key}.body`)
.split("\n")
.map((para) =>
para.trim() ? <p key={para.slice(0, 24)}>{para}</p> : null,
)}
</div>
</article>
))}
</div>
</section>
</PageShell>
);
}
+74
View File
@@ -0,0 +1,74 @@
import type { Metadata } from "next";
import Image from "next/image";
import { getTranslations, setRequestLocale } from "next-intl/server";
import PageShell from "@/components/PageShell";
import { buildMetadata } from "@/lib/seo";
const sectionMeta = [
{ key: "history", image: "/images/about-chalk/1.jpg" },
{ key: "method", image: "/images/about-chalk/2.jpg" },
{ key: "strength", image: "/images/about-chalk/3.jpg" },
{ key: "pros", image: "/images/about-chalk/4.jpg" },
{ key: "cons", image: "/images/about-chalk/1.jpg" },
{ key: "super", image: "/images/products/super-chalk.jpg" },
] as const;
type Props = {
params: Promise<{ locale: string }>;
};
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "Meta" });
return buildMetadata({
title: t("aboutChalkTitle"),
description: t("aboutChalkDescription"),
path: `/${locale}/aboutsuperchalk`,
locale,
});
}
export default async function AboutChalkPage({ params }: Props) {
const { locale } = await params;
setRequestLocale(locale);
const t = await getTranslations("AboutChalkPage");
return (
<PageShell title={t("title")}>
<section className="section content-page">
<div className="container">
{sectionMeta.map((section, index) => (
<article
key={section.key}
className={`content-block${index % 2 === 1 ? " is-reverse" : ""}`}
>
<div className="content-block-media shadow-box">
<Image
src={section.image}
alt={t(`sections.${section.key}.title`)}
width={700}
height={520}
/>
</div>
<div className="content-block-text">
<h2>{t(`sections.${section.key}.title`)}</h2>
<Image
src="/images/infoitems/line.png"
alt=""
width={151}
height={5}
className="section-line"
/>
{t(`sections.${section.key}.body`)
.split("\n")
.map((para) =>
para.trim() ? <p key={para.slice(0, 24)}>{para}</p> : null,
)}
</div>
</article>
))}
</div>
</section>
</PageShell>
);
}
+65
View File
@@ -0,0 +1,65 @@
import type { Metadata } from "next";
import Image from "next/image";
import { getTranslations, setRequestLocale } from "next-intl/server";
import PageShell from "@/components/PageShell";
import { buildMetadata } from "@/lib/seo";
type Props = {
params: Promise<{ locale: string }>;
};
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "Meta" });
return buildMetadata({
title: t("aboutUsTitle"),
description: t("aboutUsDescription"),
path: `/${locale}/aboutus`,
locale,
});
}
export default async function AboutUsPage({ params }: Props) {
const { locale } = await params;
setRequestLocale(locale);
const t = await getTranslations("AboutUsPage");
return (
<PageShell title={t("title")}>
<section className="section about-us-page">
<div className="container about-us-grid">
<div className="about-us-media shadow-box">
<Image
src="/images/about-us/1.jpg"
alt={t("imageAlt")}
width={900}
height={600}
priority
sizes="(max-width: 1024px) 100vw, 55vw"
/>
</div>
<div className="about-us-copy">
<Image
src="/images/about-us/2.png"
alt=""
width={90}
height={90}
className="about-us-icon"
/>
<h2>{t("title")}</h2>
<Image
src="/images/infoitems/line.png"
alt=""
width={151}
height={5}
className="section-line"
/>
<p>{t("p1")}</p>
<p>{t("p2")}</p>
<p>{t("p3")}</p>
</div>
</div>
</section>
</PageShell>
);
}
+49
View File
@@ -0,0 +1,49 @@
import type { Metadata } from "next";
import Image from "next/image";
import { getTranslations, setRequestLocale } from "next-intl/server";
import PageShell from "@/components/PageShell";
import { galleryImages } from "@/data/pages";
import { buildMetadata } from "@/lib/seo";
type Props = {
params: Promise<{ locale: string }>;
};
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "Meta" });
return buildMetadata({
title: t("galleryTitle"),
description: t("galleryDescription"),
path: `/${locale}/gallery`,
locale,
});
}
export default async function GalleryPage({ params }: Props) {
const { locale } = await params;
setRequestLocale(locale);
const t = await getTranslations("GalleryPage");
return (
<PageShell title={t("title")}>
<section className="section">
<div className="container">
<div className="gallery-grid">
{galleryImages.map((item) => (
<figure key={item.id} className="gallery-item">
<Image
src={item.src}
alt={t("itemAlt", { n: item.id })}
width={700}
height={500}
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw"
/>
</figure>
))}
</div>
</div>
</section>
</PageShell>
);
}
+112
View File
@@ -0,0 +1,112 @@
import type { Metadata, Viewport } from "next";
import { hasLocale, NextIntlClientProvider } from "next-intl";
import { getMessages, getTranslations, setRequestLocale } from "next-intl/server";
import { Vazirmatn } from "next/font/google";
import { notFound } from "next/navigation";
import JsonLd, {
localBusinessJsonLd,
organizationJsonLd,
} from "@/components/JsonLd";
import { routing } from "@/i18n/routing";
import { siteConfig } from "@/data/seo";
import "../globals.css";
const vazirmatn = Vazirmatn({
subsets: ["arabic"],
weight: ["300", "400", "500", "700", "800"],
variable: "--font-vazirmatn",
display: "swap",
});
export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
themeColor: "#000000",
};
type Props = {
children: React.ReactNode;
params: Promise<{ locale: string }>;
};
export function generateStaticParams() {
return routing.locales.map((locale) => ({ locale }));
}
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>;
}): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "Meta" });
return {
metadataBase: new URL(siteConfig.url),
title: {
default: t("siteName"),
template: `%s | ${t("siteName")}`,
},
description: t("homeDescription"),
applicationName: siteConfig.shortName,
icons: {
icon: "/favicon.png",
apple: "/favicon.png",
},
openGraph: {
type: "website",
locale: locale === "ar" ? "ar_SA" : "fa_IR",
url: `${siteConfig.url}/${locale}`,
siteName: t("siteName"),
title: t("siteName"),
description: t("homeDescription"),
images: [
{
url: siteConfig.ogImage,
width: 1920,
height: 680,
alt: t("siteName"),
},
],
},
twitter: {
card: "summary_large_image",
title: t("siteName"),
description: t("homeDescription"),
images: [siteConfig.ogImage],
},
alternates: {
canonical: `/${locale}`,
languages: {
"fa-IR": "/fa",
"ar": "/ar",
"x-default": "/fa",
},
},
robots: {
index: true,
follow: true,
},
};
}
export default async function LocaleLayout({ children, params }: Props) {
const { locale } = await params;
if (!hasLocale(routing.locales, locale)) {
notFound();
}
setRequestLocale(locale);
const messages = await getMessages();
return (
<html lang={locale} dir="rtl" className={`${vazirmatn.variable} h-full`}>
<body className={`${vazirmatn.className} min-h-full antialiased`}>
<NextIntlClientProvider messages={messages}>
<JsonLd data={[organizationJsonLd(), localBusinessJsonLd()]} />
{children}
</NextIntlClientProvider>
</body>
</html>
);
}
+47
View File
@@ -0,0 +1,47 @@
import type { Metadata } from "next";
import { getTranslations, setRequestLocale } from "next-intl/server";
import AboutTabs from "@/components/AboutTabs";
import Footer from "@/components/Footer";
import Header from "@/components/Header";
import Hero from "@/components/Hero";
import Location from "@/components/Location";
import PowderFeatures from "@/components/PowderFeatures";
import Productions from "@/components/Productions";
import ProductsCarousel from "@/components/ProductsCarousel";
import { buildMetadata } from "@/lib/seo";
type Props = {
params: Promise<{ locale: string }>;
};
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "Meta" });
return buildMetadata({
title: t("homeTitle"),
description: t("homeDescription"),
path: `/${locale}`,
locale,
});
}
export default async function HomePage({ params }: Props) {
const { locale } = await params;
setRequestLocale(locale);
return (
<>
<Header />
<main>
<Hero />
<Productions />
<PowderFeatures />
<AboutTabs />
<ProductsCarousel />
<Location />
</main>
<Footer />
</>
);
}
+65
View File
@@ -0,0 +1,65 @@
import type { Metadata } from "next";
import { getTranslations, setRequestLocale } from "next-intl/server";
import { notFound } from "next/navigation";
import JsonLd, { breadcrumbJsonLd, productJsonLd } from "@/components/JsonLd";
import PageShell from "@/components/PageShell";
import ProductDetail from "@/components/ProductDetail";
import { getProductBySlug, getProducts } from "@/data/products";
import { buildMetadata } from "@/lib/seo";
type Props = {
params: Promise<{ locale: string; slug: string }>;
};
export async function generateStaticParams() {
return getProducts().map((product) => ({ slug: product.slug }));
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { locale, slug } = await params;
const product = getProductBySlug(slug);
const t = await getTranslations({ locale, namespace: "Products" });
if (!product) return { title: t("notFound") };
return buildMetadata({
title: t(`items.${product.slug}.title`),
description: t(`items.${product.slug}.description`),
path: `/${locale}/products/${product.slug}`,
image: product.image,
locale,
});
}
export default async function ProductDetailPage({ params }: Props) {
const { locale, slug } = await params;
setRequestLocale(locale);
const product = getProductBySlug(slug);
const t = await getTranslations("Products");
const tCommon = await getTranslations("Common");
if (!product) notFound();
const title = t(`items.${product.slug}.title`);
return (
<PageShell title={t("detailsTitle")}>
<JsonLd
data={[
productJsonLd({
...product,
title,
description: t(`items.${product.slug}.description`),
}),
breadcrumbJsonLd([
{ name: tCommon("home"), path: `/${locale}` },
{ name: tCommon("products"), path: `/${locale}/products` },
{
name: title,
path: `/${locale}/products/${product.slug}`,
},
]),
]}
/>
<ProductDetail product={product} />
</PageShell>
);
}
+38
View File
@@ -0,0 +1,38 @@
import type { Metadata } from "next";
import { getTranslations, setRequestLocale } from "next-intl/server";
import PageShell from "@/components/PageShell";
import ProductGrid from "@/components/ProductGrid";
import { getProducts } from "@/data/products";
import { buildMetadata } from "@/lib/seo";
type Props = {
params: Promise<{ locale: string }>;
};
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "Meta" });
return buildMetadata({
title: t("productsTitle"),
description: t("productsDescription"),
path: `/${locale}/products`,
locale,
});
}
export default async function ProductsPage({ params }: Props) {
const { locale } = await params;
setRequestLocale(locale);
const t = await getTranslations("Products");
const products = getProducts();
return (
<PageShell title={t("pageTitle")}>
<section className="section">
<div className="container">
<ProductGrid products={products} />
</div>
</section>
</PageShell>
);
}
+1348 -15
View File
File diff suppressed because it is too large Load Diff
+5 -25
View File
@@ -1,29 +1,9 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import type { ReactNode } from "react";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
type Props = {
children: ReactNode;
};
export default function RootLayout({ children }: LayoutProps<"/">) {
return (
<html
lang="en"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
>
<body className="min-h-full flex flex-col">{children}</body>
</html>
);
export default function RootLayout({ children }: Props) {
return children;
}
-69
View File
@@ -1,69 +0,0 @@
import Image from "next/image";
export default function Home() {
return (
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
<Image
className="dark:invert h-5 w-[100px]"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the{" "}
<code className="rounded bg-black/[.06] px-1.5 py-0.5 font-mono text-[0.9em] dark:bg-white/[.08]">
page.tsx
</code>{" "}
file.
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
</p>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert h-[14px] w-4"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={14}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
</div>
</main>
</div>
);
}
+13
View File
@@ -0,0 +1,13 @@
import type { MetadataRoute } from "next";
import { siteConfig } from "@/data/seo";
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: "*",
allow: "/",
},
sitemap: `${siteConfig.url}/sitemap.xml`,
host: siteConfig.url,
};
}
+43
View File
@@ -0,0 +1,43 @@
import type { MetadataRoute } from "next";
import { siteConfig } from "@/data/seo";
import { getProducts } from "@/data/products";
import { routing } from "@/i18n/routing";
export default function sitemap(): MetadataRoute.Sitemap {
const now = new Date();
const staticPaths = [
"",
"/products",
"/aboutsuperchalk",
"/aboutlime",
"/gallery",
"/aboutus",
];
const productPaths = getProducts().map(
(product) => `/products/${product.slug}`,
);
const entries: MetadataRoute.Sitemap = [];
for (const locale of routing.locales) {
for (const path of [...staticPaths, ...productPaths]) {
entries.push({
url: `${siteConfig.url}/${locale}${path}`,
lastModified: now,
changeFrequency: path.startsWith("/products") ? "weekly" : "monthly",
priority: path === "" ? 1 : path === "/products" ? 0.9 : 0.8,
alternates: {
languages: Object.fromEntries(
routing.locales.map((item) => [
item,
`${siteConfig.url}/${item}${path}`,
]),
),
},
});
}
}
return entries;
}