Add product/single product api

This commit is contained in:
haniyeroozmand
2026-03-30 22:35:43 +03:30
parent c24d0ef511
commit 65c28948a3
10 changed files with 338 additions and 285 deletions

View File

@@ -1,91 +1,40 @@
import { products } from "@/lib/data";
import ProductCard from "@/components/productcard";
import { getProductsByCategory } from "@/public/src/services/products/api";
interface PageProps {
params: Promise<{ categoryName: string }>;
}
export default async function CategoryPage({ params }:any) {
export default async function CategoryPage({ params }: PageProps) {
try {
// ۱. دریافت پارامترها
const resolvedParams = await params;
const rawCategoryName = resolvedParams.categoryName;
// ۲. دیکد کردن آدرس
const decodedUrl = decodeURIComponent(rawCategoryName);
const { categoryName } = await params;
// ۳. آماده‌سازی رشته برای مقایسه
const categoryNameToMatch = decodedUrl.replace(/-/g, " ").trim();
console.log("🔎 CategoryPage slug:", categoryName);
// استخراج تمام دسته‌بندی‌های موجود در دیتابیس (برای بررسی چشمی)
const allCategoriesInDb = Array.from(new Set(products.map(p => p.category)));
const products = await getProductsByCategory(categoryName);
// ۴. فیلتر کردن (با حذف فاصله‌های اضافی از دو طرف برای اطمینان)
const filteredProducts = products.filter(
(p) => p.category.trim() === categoryNameToMatch
);
return (
<div className="p-8">
// ۵. اگر پیدا نشد، صفحه دیباگ را در مرورگر نمایش بده
if (!filteredProducts || filteredProducts.length === 0) {
return (
<main className="container mx-auto px-4 py-10" dir="rtl">
<div className="bg-red-50 border border-red-200 text-red-800 p-6 rounded-lg">
<h1 className="text-2xl font-bold mb-4">محصولی پیدا نشد (حالت دیباگ)</h1>
<div className="mb-6 space-y-2">
<p>پارامتر دریافت شده از URL: <strong className="font-mono bg-white px-2 py-1 rounded">{rawCategoryName}</strong></p>
<p>رشتهی آماده شده برای جستجو: <strong className="font-mono bg-white px-2 py-1 rounded">{categoryNameToMatch}</strong> (طول: {categoryNameToMatch.length} کاراکتر)</p>
</div>
<h1 className="text-xl font-bold mb-6">
دستهبندی: {categoryName}
</h1>
<h2 className="text-xl font-bold mb-2">لیست دستهبندیهای موجود در دیتابیس شما:</h2>
<ul className="list-disc list-inside space-y-1 bg-white p-4 rounded-md">
{allCategoriesInDb.map((cat, index) => (
<li key={index} className="font-mono">
"{cat}" (طول: {cat.length} کاراکتر)
</li>
))}
</ul>
<p className="mt-4 text-sm text-gray-600">
لطفا طول کاراکترها و املای کلمات را در لیست بالا مقایسه کنید. (آیا نیمفاصله یا فاصله اضافه وجود دارد؟)
<br />
<strong>نکته مهم:</strong> برای دیدن لاگهای بیشتر، ترمینال (کنسول) محیط توسعه (مثلاً VSCode) را چک کنید، نه کنسول مرورگر را.
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
{products.items.map((product:any) => (
<div key={product.id} className="border p-4 rounded-xl">
<div className="h-32 bg-gray-100 rounded mb-3" />
<h3 className="text-sm font-medium">
{product.title}
</h3>
<p className="text-xs text-gray-500">
{product.brand}
</p>
</div>
</main>
);
}
))}
const categoryTitle = filteredProducts[0].category;
// رندر حالت موفق
return (
<main className="container mx-auto px-4 py-10" dir="rtl">
<div className="mb-8 border-b pb-4">
<h1 className="text-2xl font-bold text-gray-800">
دستهبندی: {categoryTitle}
</h1>
<p className="text-gray-500 mt-2">
تعداد محصولات یافت شده: {filteredProducts.length} مورد
</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
{filteredProducts.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
</main>
);
} catch (error) {
// هندلینگ خطاهای غیرمنتظره
console.error("Critical Error in Category Page:", error);
return (
<div className="p-10 text-center text-red-500 font-bold">
خطای پردازشی رخ داد. لطفا ترمینال VSCode را بررسی کنید.
</div>
);
}
</div>
);
}

View File

@@ -15,16 +15,29 @@ import { products } from "@/lib/data";
import { articles } from "@/lib/data";
import Link from "next/link";
import { useCategories } from "@/components/context/categoryprovider";
import { getProducts } from "@/public/src/services/products/api";
export default function Home() {
export default async function Home() {
const [activeTab, setActiveTab] = useState(0);
const brands = ["NTN", "KOYO", "NACHI", "TIMKEN", "FAG", "SKF"];
const latestArticles = articles.slice(-4);
const { rootCategories } = useCategories();
const data = await getProducts(1, 20);
const products = data.items.map((p: any) => ({
id: p.id,
title: p.title,
brand: p.brand,
slug: p.slug,
price: p.calculated_price,
stock: p.stock,
image: p.mainImageUrl || "/placeholder.png",
attributes: p.attributes
}));
const features = [
{
icon: Headphones,
@@ -281,13 +294,13 @@ export default function Home() {
</div>
</div>
{/* products → فقط ۴ تای آخر */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
{products.slice(-4).map((product, i) => (
<ProductCard key={i} product={product} />
{products.slice(-4).map((product: any) => (
<ProductCard key={product.id} product={product} />
))}
</div>
</div>
</section>

View File

@@ -1,28 +1,57 @@
import ScrollToTop from '@/components/scrolltop';
import { products } from '@/lib/data';
import { notFound } from 'next/navigation';
import Image from "next/image";
import ProductCartAction from '@/components/cartaction';
import { getProductBySlug } from "@/public/src/services/products/api";
interface PageProps {
params: Promise<{
id: string;
slug: string;
}>;
params: Promise<{ slug: string }>;
}
export default async function SingleProductPage({ params }: PageProps) {
const resolvedParams = await params;
const product = products.find((p) => p.id.toString() === resolvedParams.id);
// --------------------------------------------------------
// ۱. منطق دریافت داده‌ها (دقیقا مشابه درخواست شما)
// --------------------------------------------------------
const resolvedParams = await params;
const slug = resolvedParams.slug;
console.log("👉 Extracted slug from URL:", slug);
if (!slug) {
return notFound();
}
const product = await getProductBySlug(slug);
if (!product) {
notFound();
console.log(`🔴 Product not found or API failed for slug: ${slug}`);
return notFound();
}
// --------------------------------------------------------
// ۲. آماده‌سازی و مپ کردن داده‌های API برای UI
// --------------------------------------------------------
const formattedPrice = product.price
? Number(product.price.toString().replace(/,/g, '')).toLocaleString('fa-IR')
// فرمت کردن قیمت
const formattedPrice = product.display_price
? product.display_price.toLocaleString('fa-IR')
: null;
// وضعیت موجودی
const hasStock = product.stock && product.stock > 0;
// تصویر محصول (جلوگیری از خطای null در next/image)
const imageUrl = product.mainImageUrl || '/placeholder-image.png'; // می‌توانید آدرس یک عکس پیش‌فرض را اینجا بگذارید
// دریافت ویژگی‌ها (Attributes) از آرایه دیتابیس با تابع کمکی
const getAttribute = (attrName: string) => {
const attr = product.attributes?.find((a: any) => a.name === attrName);
return attr ? attr.valueText : "-";
};
// --------------------------------------------------------
// ۳. خروجی UI (بدون تغییر در ساختار و استایل‌ها)
// --------------------------------------------------------
return (
<div className="bg-[#f8f9fc] min-h-screen py-8" dir="rtl">
<div className="mx-auto px-4 lg:px-8 container max-w-7xl">
@@ -33,28 +62,20 @@ export default async function SingleProductPage({ params }: PageProps) {
<span>خانه</span> &gt; <span>محصولات</span> &gt; <span className="text-gray-800 font-semibold">{product.title}</span>
</div>
{/*
کانتینر اصلی گرید (Grid)
موبایل: 1 ستون
دسکتاپ: 12 ستون
*/}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 lg:gap-8">
{/* ========================================= */}
{/* بخش 1: معرفی محصول (بالای ستون راست) */}
{/* دسکتاپ: 8 ستون عرض دارد. موبایل: اولین آیتم نمایش داده می‌شود */}
{/* ========================================= */}
{/* بخش 1: معرفی محصول */}
<div className="lg:col-span-8 order-1 lg:order-none">
<div className="bg-white rounded-3xl shadow-sm p-6 md:p-8 flex flex-col md:flex-row gap-8 items-center border border-gray-100">
{/* باکس تصویر */}
<div className="w-full md:w-[52%] h-72 rounded-2xl bg-[linear-gradient(135deg,#f8f9fc_0%,#e2e8f0_100%)] flex items-center justify-center p-6 relative">
{product.badge && (
{product.featured && (
<span className="absolute top-4 right-4 bg-orange-500 text-white text-xs font-bold px-2 py-1 rounded-md">
{product.badge}
پیشنهاد ویژه
</span>
)}
<Image
src={product.image}
src={imageUrl}
width={250}
height={250}
alt={product.title}
@@ -71,44 +92,37 @@ export default async function SingleProductPage({ params }: PageProps) {
</h1>
<p className='text-gray-500 text-[13px] mb-8'>
طراحی شده برای عملکرد در دورهای بالا با آببندی کامل پلاستیکی (Rubber Seal). مناسب برای الکتروموتور و گیربکس.
{product.meta?.shortDescription || 'توضیحات کوتاهی برای این محصول ثبت نشده است.'}
</p>
<div className="grid grid-cols-2 gap-4">
<div className="bg-gray-50 p-4 rounded-2xl flex flex-col items-center justify-center text-center border border-gray-100">
<span className="text-gray-500 text-sm mb-2">تحمل بار ($C$)</span>
<span className="font-bold text-gray-800 text-lg" dir="ltr">{product.loadCapacity}</span>
</div>
<div className="bg-gray-50 p-4 rounded-2xl flex flex-col items-center justify-center text-center border border-gray-100">
<span className="text-gray-500 text-sm mb-2">سرعت نهایی</span>
<span className="font-bold text-gray-800 text-lg" dir="ltr">{product.maxSpeed}</span>
</div>
<div className="bg-gray-50 p-4 rounded-2xl flex flex-col items-center justify-center text-center border border-gray-100">
<span className="text-gray-500 text-sm mb-2">نوع آببند</span>
<span className="font-bold text-gray-800 text-lg" dir="ltr">{product.sealType}</span>
</div>
<div className="bg-gray-50 p-4 rounded-2xl flex flex-col items-center justify-center text-center border border-gray-100">
<span className="text-gray-500 text-sm mb-2">محدوده دما</span>
<span className="font-bold text-gray-800 text-lg" dir="ltr">{product.tempRange}</span>
</div>
</div>
{/* رندر داینامیک ۳ ویژگی اول از API */}
{product.attributes?.slice(0, 3).map((attr: any) => (
<div key={attr.id} className="bg-gray-50 p-4 rounded-2xl flex flex-col items-center justify-center text-center border border-gray-100">
<span className="text-gray-500 text-sm mb-2">{attr.name}</span>
<span className="font-bold text-gray-800 text-sm" dir="ltr">{attr.valueText || '-'}</span>
</div>
))}
{/* آیتم چهارم: دسته‌بندی (برای حفظ ظاهر گرید ۴ تایی) */}
<div className="bg-gray-50 p-4 rounded-2xl flex flex-col items-center justify-center text-center border border-gray-100">
<span className="text-gray-500 text-sm mb-2">دستهبندی</span>
<span className="font-bold text-gray-800 text-sm" dir="ltr">{product.primaryCategory?.name || '-'}</span>
</div>
</div>
</div>
</div>
</div>
{/* ========================================= */}
{/* بخش 2: سایدبار و دکمه خرید (ستون چپ) */}
{/* دسکتاپ: 4 ستون عرض دارد و 2 ردیف طول دارد (row-span-2) تا کنار هر دو بخش راست بماند */}
{/* موبایل: با order-2 بین بخش معرفی و مشخصات قرار می‌گیرد */}
{/* ========================================= */}
{/* بخش 2: سایدبار و دکمه خرید */}
<div className="lg:col-span-4 col-span-2 lg:row-span-2 order-2 lg:order-none relative h-full">
{/* کانتینر استیکی */}
<div className="flex flex-col gap-4 lg:sticky lg:top-[20px] pb-8">
<div className="bg-white rounded-[2rem] shadow-[0_4px_20px_-10px_rgba(0,0,0,0.05)] p-5 border border-gray-100 text-center">
{product.stock ? (
{hasStock ? (
<div className="bg-[#eaf8ee] text-[#28a745] py-3 rounded-2xl text-sm font-bold mb-4">
موجود در انبار شیراز
موجود در انبار ({product.stock} عدد)
</div>
) : (
<div className="bg-red-50 text-red-500 py-3 rounded-2xl text-sm font-bold mb-4">
@@ -121,7 +135,7 @@ export default async function SingleProductPage({ params }: PageProps) {
{formattedPrice ? (
<>
<span className="text-3xl font-extrabold text-[#1a2332] tracking-tight">{formattedPrice}</span>
<span className="text-gray-500 text-sm mt-2">تومان</span>
<span className="text-gray-500 text-sm mt-2"> {product.currency_label}</span>
</>
) : (
<span className="text-2xl font-extrabold text-[#1a2332]">استعلام قیمت</span>
@@ -143,6 +157,7 @@ export default async function SingleProductPage({ params }: PageProps) {
</div>
</div>
{/* دکمه‌های دانلود دیتاشیت و مشاوره */}
<div className="bg-white rounded-3xl p-5 shadow-[0_4px_20px_-10px_rgba(0,0,0,0.05)] border border-gray-100 hidden lg:flex justify-between items-center cursor-pointer hover:bg-gray-50 transition-colors">
<div className="flex flex-col gap-1 text-right">
<span className="font-extrabold text-[#1a2332] text-sm">دانلود دیتاشیت</span>
@@ -171,11 +186,7 @@ export default async function SingleProductPage({ params }: PageProps) {
</div>
</div>
{/* ========================================= */}
{/* بخش 3: مشخصات ابعادی (پایین ستون راست) */}
{/* دسکتاپ: 8 ستون عرض دارد و به طور خودکار زیر بخش معرفی قرار می‌گیرد */}
{/* موبایل: با order-3 در انتهای صفحه قرار می‌گیرد */}
{/* ========================================= */}
{/* بخش 3: مشخصات ابعادی */}
<div className="lg:col-span-8 order-3 lg:order-none">
<div className="bg-white rounded-3xl shadow-sm p-6 md:p-8 border border-gray-100">
<div className="flex justify-between items-center mb-8 border-b border-gray-100 pb-4">
@@ -191,10 +202,10 @@ export default async function SingleProductPage({ params }: PageProps) {
<div className="absolute h-1/2 w-[1px] bg-orange-500 top-1/2 right-1/2"></div>
<div className="absolute top-1/2 -mt-5 left-[60%] text-orange-400 text-xs font-mono px-1 z-10" dir="ltr">
{"$D=" + (product.l ? product.l.replace('mm', '').trim() : '') + "$"}
{"$D=" + getAttribute('قطر خارجی').replace('mm', '').trim() + "$"}
</div>
<div className="absolute top-[60%] right-1/2 text-orange-400 text-xs font-mono px-1 z-10" dir="ltr">
{"$d=" + (product.d ? product.d.replace('mm', '').trim() : '') + "$"}
{"$d=" + getAttribute('قطر داخلی').replace('mm', '').trim() + "$"}
</div>
</div>
</div>
@@ -204,27 +215,27 @@ export default async function SingleProductPage({ params }: PageProps) {
<tbody>
<tr className="border-b border-gray-50">
<td className="py-3 text-gray-500">قطر داخلی ($d$)</td>
<td className="py-3 font-bold text-gray-800 text-left" dir="ltr">{product.d}</td>
<td className="py-3 font-bold text-gray-800 text-left" dir="ltr">{getAttribute('قطر داخلی')}</td>
</tr>
<tr className="border-b border-gray-50">
<td className="py-3 text-gray-500">قطر خارجی ($D$)</td>
<td className="py-3 font-bold text-gray-800 text-left" dir="ltr">{product.l}</td>
<td className="py-3 font-bold text-gray-800 text-left" dir="ltr">{getAttribute('قطر خارجی')}</td>
</tr>
<tr className="border-b border-gray-50">
<td className="py-3 text-gray-500">پهنا ($B$)</td>
<td className="py-3 font-bold text-gray-800 text-left" dir="ltr">{product.width}</td>
<td className="py-3 font-bold text-gray-800 text-left" dir="ltr">{getAttribute('پهنا')}</td>
</tr>
<tr className="border-b border-gray-50">
<td className="py-3 text-gray-500">وزن خالص</td>
<td className="py-3 font-bold text-gray-800 text-left" dir="ltr">{product.weight}</td>
<td className="py-3 font-bold text-gray-800 text-left" dir="ltr">{getAttribute('وزن')}</td>
</tr>
<tr className="border-b border-gray-50">
<td className="py-3 text-gray-500">جنس قفسه</td>
<td className="py-3 font-bold text-gray-800 text-left" dir="ltr">{product.cageMaterial}</td>
<td className="py-3 font-bold text-gray-800 text-left" dir="ltr">{getAttribute('جنس قفسه')}</td>
</tr>
<tr>
<td className="py-3 text-gray-500">کد بینالمللی</td>
<td className="py-3 font-bold text-gray-800 text-left" dir="ltr">{product.intCode}</td>
<td className="py-3 font-bold text-gray-800 text-left" dir="ltr">{product.technicalCode}</td>
</tr>
</tbody>
</table>

View File

@@ -1,15 +1,29 @@
// app/products/page.tsx
import ProductCard from '@/components/productcard'; // مسیر را با توجه به ساختار خود اصلاح کنید
import { products } from '@/lib/data'; // ایمپورت دیتای مرکزی
import ProductCard from '@/components/productcard';
import { getProducts } from '@/public/src/services/products/api';
export default async function ProductsPage() {
const data = await getProducts(1, 20);
const products = data.items.map((p: any) => ({
id: p.id,
title: p.title,
brand: p.brand,
price: p.calculated_price,
stock: p.stock,
slug: p.slug,
image: p.mainImageUrl || "/placeholder.png",
attributes: p.attributes
}));
export default function ProductsPage() {
return (
<div className="container mx-auto p-4">
<h1 className="text-2xl font-bold mb-6">همه محصولات</h1>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
{products.map((product) => (
<ProductCard key={product.title} product={product} />
{products.map((product: any) => (
<ProductCard key={product.id} product={product} />
))}
</div>
</div>