Add the FarrokhWin Next.js site for Meshkee hosting.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-31 09:18:26 +03:30
co-authored by Cursor
commit 4dec4eb92c
116 changed files with 13456 additions and 0 deletions
+173
View File
@@ -0,0 +1,173 @@
import type { Metadata } from "next";
import Link from "next/link";
import { notFound, redirect } from "next/navigation";
import MediaCarousel from "@/components/MediaCarousel";
import ProductGallery from "@/components/ProductGallery";
import ProductTabs from "@/components/ProductTabs";
import { categoryHref, categoryTitle } from "@/lib/categories";
import { fetchComments } from "@/lib/comments";
import { plainText, sanitizeHtml } from "@/lib/format";
import {
fetchProductById,
fetchProducts,
fetchProductTechnicalInfo,
productGallery,
productHref,
productImage,
productTitle,
} from "@/lib/products";
import { pageMetadata } from "@/lib/seo";
import { slugifyForUrl } from "@/lib/slug";
import type { SlotMediaItem } from "@/lib/static-images";
import { rowsFromTechnicalInfo } from "@/lib/technical";
export const revalidate = 60;
type Params = { id: string; slug: string };
export async function generateMetadata({
params,
}: {
params: Promise<Params>;
}): Promise<Metadata> {
const { id } = await params;
const data = await fetchProductById(id);
if (!data?.product) {
return {
title: "محصول یافت نشد",
description: "این محصول در کاتالوگ پنجره فرخ موجود نیست.",
robots: { index: false },
};
}
const title = productTitle(data.product);
const description =
plainText(data.product.summary || data.product.descriptionHtml).slice(0, 160) ||
`${title} | محصولات پنجره فرخ`;
return pageMetadata({
title,
description,
path: productHref(data.product),
image: productGallery(data.product)[0],
});
}
export default async function ProductDetailPage({ params }: { params: Promise<Params> }) {
const { id, slug } = await params;
const data = await fetchProductById(id);
if (!data?.product) notFound();
const product = data.product;
const title = productTitle(product);
const canonicalSlug = slugifyForUrl(title);
if (decodeURIComponent(slug) !== canonicalSlug) {
redirect(productHref(product));
}
const [technicalInfo, comments] = await Promise.all([
fetchProductTechnicalInfo(product.id),
fetchComments("product", product.id),
]);
const descriptionHtml = sanitizeHtml(product.descriptionHtml);
const descriptionText = product.summary?.trim() || "";
let related = (data.relatedProducts ?? []).filter((item) => item.id !== product.id);
if (related.length < 8) {
const others = await fetchProducts({ pageSize: 12 });
const extra = others.items.filter(
(item) => item.id !== product.id && !related.some((row) => row.id === item.id),
);
related = [...related, ...extra];
}
related = related.slice(0, 12);
const relatedItems: SlotMediaItem[] = related.map((item) => ({
id: item.id,
image: productImage(item),
href: productHref(item),
title: productTitle(item),
kicker: item.title && item.nameFa ? item.title : item.categoryNameFa || undefined,
}));
return (
<section className="detail-page">
<div className="container">
<nav className="detail-crumbs" aria-label="مسیر صفحه">
<Link href="/">خانه</Link>
<span>/</span>
<Link href="/products">محصولات</Link>
{product.categoryId && (product.categoryNameFa || product.categoryName) ? (
<>
<span>/</span>
<Link
href={categoryHref({
id: product.categoryId,
nameFa: product.categoryNameFa,
name: product.categoryName,
})}
>
{categoryTitle({ nameFa: product.categoryNameFa, name: product.categoryName })}
</Link>
</>
) : product.categoryNameFa || product.categoryName ? (
<>
<span>/</span>
<span>{product.categoryNameFa || product.categoryName}</span>
</>
) : null}
<span>/</span>
<b>{title}</b>
</nav>
<div className="product-detail-grid">
<ProductGallery images={productGallery(product)} alt={title} />
<div className="product-detail-copy">
<p className="en">Product Details</p>
{product.categoryId && (product.categoryNameFa || product.categoryName) ? (
<p className="product-category">
<Link
href={categoryHref({
id: product.categoryId,
nameFa: product.categoryNameFa,
name: product.categoryName,
})}
>
{categoryTitle({ nameFa: product.categoryNameFa, name: product.categoryName })}
</Link>
</p>
) : product.categoryNameFa || product.categoryName ? (
<p className="product-category">{product.categoryNameFa || product.categoryName}</p>
) : null}
<h1>{title}</h1>
{product.title && product.nameFa ? <p className="en">{product.title}</p> : null}
{descriptionText && !descriptionHtml ? <p>{descriptionText}</p> : null}
<Link className="text-link" href="/contact">
تماس با ما
</Link>
</div>
</div>
<ProductTabs
descriptionHtml={descriptionHtml || undefined}
descriptionText={descriptionHtml ? undefined : descriptionText}
technicalRows={rowsFromTechnicalInfo(technicalInfo)}
comments={comments}
entityId={product.id}
/>
</div>
{relatedItems.length > 0 ? (
<section className="related-section products-section">
<div className="container">
<h2>محصولات مشابه</h2>
<p className="en">Similar Products</p>
<MediaCarousel
items={relatedItems}
variant="product"
ariaLabel="محصولات مشابه"
/>
</div>
</section>
) : null}
</section>
);
}
@@ -0,0 +1,133 @@
import type { Metadata } from "next";
import Link from "next/link";
import { notFound, redirect } from "next/navigation";
import CatalogCard from "@/components/CatalogCard";
import PageHero from "@/components/PageHero";
import {
categoryHref,
categoryTitle,
fetchCategories,
fetchCategoryById,
} from "@/lib/categories";
import { fetchProducts } from "@/lib/products";
import { pageMetadata } from "@/lib/seo";
import { slugifyForUrl } from "@/lib/slug";
export const revalidate = 60;
type Params = { id: string; slug: string };
export async function generateMetadata({
params,
searchParams,
}: {
params: Promise<Params>;
searchParams: Promise<{ page?: string }>;
}): Promise<Metadata> {
const { id } = await params;
const { page: pageParam } = await searchParams;
const page = Math.max(1, Number(pageParam) || 1);
const category = await fetchCategoryById(id);
if (!category) {
return {
title: "دسته‌بندی یافت نشد",
description: "این دسته‌بندی در کاتالوگ پنجره فرخ موجود نیست.",
robots: { index: false },
};
}
const title = categoryTitle(category);
const description =
category.description?.trim() || `محصولات دسته ${title} در پنجره فرخ.`;
return pageMetadata({
title: page > 1 ? `${title} — صفحه ${page}` : title,
description,
path: page > 1 ? `${categoryHref(category)}?page=${page}` : categoryHref(category),
});
}
export default async function ProductCategoryPage({
params,
searchParams,
}: {
params: Promise<Params>;
searchParams: Promise<{ page?: string }>;
}) {
const { id, slug } = await params;
const { page: pageParam } = await searchParams;
const page = Math.max(1, Number(pageParam) || 1);
const category = await fetchCategoryById(id);
if (!category) notFound();
const title = categoryTitle(category);
const canonicalSlug = slugifyForUrl(title);
if (decodeURIComponent(slug) !== canonicalSlug) {
redirect(categoryHref(category));
}
const [result, allCategories] = await Promise.all([
fetchProducts({ page, pageSize: 12, categoryId: category.id }),
fetchCategories("product"),
]);
const children = allCategories.filter((item) => item.parentId === category.id);
const pageCount = Math.max(1, Math.ceil((result.total || result.items.length) / (result.pageSize || 12)));
const listHref = (number: number) =>
number === 1 ? categoryHref(category) : `${categoryHref(category)}?page=${number}`;
return (
<>
<PageHero
title={title}
en={category.name && category.nameFa ? category.name : "PRODUCTS"}
crumbs={[
{ href: "/", label: "خانه" },
{ href: "/products", label: "محصولات" },
]}
/>
<section className="section">
<div className="container">
{category.description ? <p className="detail-lead">{category.description}</p> : null}
{children.length > 0 ? (
<div className="category-pills">
{children.map((item) => (
<Link key={item.id} href={categoryHref(item)}>
{categoryTitle(item)}
</Link>
))}
</div>
) : null}
{result.items.length === 0 ? (
<p className="comment-empty">محصولی در این دسته ثبت نشده است.</p>
) : (
<div className="catalog-grid">
{result.items.map((product) => (
<CatalogCard key={product.id} product={product} />
))}
</div>
)}
{pageCount > 1 ? (
<nav className="pagination" aria-label="صفحه‌بندی دسته">
{Array.from({ length: pageCount }, (_, index) => {
const number = index + 1;
if (pageCount > 12 && Math.abs(number - page) > 3 && number !== 1 && number !== pageCount) {
return number === 2 || number === pageCount - 1 ? (
<span key={number}>…</span>
) : null;
}
return (
<Link key={number} href={listHref(number)} className={number === page ? "active" : undefined}>
{number}
</Link>
);
})}
</nav>
) : null}
</div>
</section>
</>
);
}
+98
View File
@@ -0,0 +1,98 @@
import type { Metadata } from "next";
import Link from "next/link";
import CatalogCard from "@/components/CatalogCard";
import PageHero from "@/components/PageHero";
import { categoryHref, categoryTitle, fetchCategories } from "@/lib/categories";
import { fetchProducts } from "@/lib/products";
import { pageMetadata } from "@/lib/seo";
export const revalidate = 60;
export async function generateMetadata({
searchParams,
}: {
searchParams: Promise<{ page?: string }>;
}): Promise<Metadata> {
const { page: pageParam } = await searchParams;
const page = Math.max(1, Number(pageParam) || 1);
return pageMetadata({
title: page > 1 ? `محصولات — صفحه ${page}` : "محصولات",
description: "کاتالوگ پنجره UPVC، شیشه دوجداره و محصولات پنجره فرخ.",
path: page > 1 ? `/products?page=${page}` : "/products",
});
}
export default async function ProductsPage({
searchParams,
}: {
searchParams: Promise<{ page?: string }>;
}) {
const { page: pageParam } = await searchParams;
const page = Math.max(1, Number(pageParam) || 1);
const [result, categories] = await Promise.all([
fetchProducts({ page, pageSize: 12 }),
fetchCategories("product"),
]);
const pageCount = Math.max(1, Math.ceil((result.total || result.items.length) / (result.pageSize || 12)));
const topCategories = categories.filter((item) => !item.parentId);
return (
<>
<PageHero
title="محصولات"
en="PRODUCTS"
crumbs={[
{ href: "/", label: "خانه" },
{ href: "/products", label: "محصولات" },
]}
/>
<section className="section">
<div className="container">
{topCategories.length > 0 ? (
<div className="category-pills">
{topCategories.map((item) => (
<Link key={item.id} href={categoryHref(item)}>
{categoryTitle(item)}
</Link>
))}
</div>
) : null}
{result.items.length === 0 ? (
<p className="comment-empty">محصولی برای نمایش وجود ندارد.</p>
) : (
<div className="catalog-grid">
{result.items.map((product) => (
<CatalogCard key={product.id} product={product} />
))}
</div>
)}
{pageCount > 1 ? (
<nav className="pagination" aria-label="صفحه‌بندی محصولات">
{Array.from({ length: pageCount }, (_, index) => {
const number = index + 1;
if (pageCount > 12 && Math.abs(number - page) > 3 && number !== 1 && number !== pageCount) {
return number === 2 || number === pageCount - 1 ? (
<span key={number}>…</span>
) : null;
}
return (
<Link
key={number}
href={number === 1 ? "/products" : `/products?page=${number}`}
className={number === page ? "active" : undefined}
>
{number}
</Link>
);
})}
</nav>
) : null}
</div>
</section>
</>
);
}