From 65c28948a30843bdb259a51050f26c513aa2fcbc Mon Sep 17 00:00:00 2001 From: haniyeroozmand <147385975+haniyeroozmand@users.noreply.github.com> Date: Mon, 30 Mar 2026 22:35:43 +0330 Subject: [PATCH] Add product/single product api --- app/category/[categoryName]/page.tsx | 107 ++++---------- app/page.tsx | 23 ++- .../[id] => product}/[slug]/page.tsx | 135 ++++++++++-------- app/products/page.tsx | 26 +++- components/context/cartcontext.tsx | 23 +-- components/productcard.tsx | 79 +++++----- next-env.d.ts | 2 +- public/src/services/auth/api.tsx | 1 - public/src/services/products/api.tsx | 130 +++++++++++------ public/src/types/product.tsx | 97 +++++++------ 10 files changed, 338 insertions(+), 285 deletions(-) rename app/{products/[id] => product}/[slug]/page.tsx (70%) diff --git a/app/category/[categoryName]/page.tsx b/app/category/[categoryName]/page.tsx index 1b47aea..6a988a4 100644 --- a/app/category/[categoryName]/page.tsx +++ b/app/category/[categoryName]/page.tsx @@ -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 ( +
- // ۵. اگر پیدا نشد، صفحه دیباگ را در مرورگر نمایش بده - if (!filteredProducts || filteredProducts.length === 0) { - return ( -
-
-

محصولی پیدا نشد (حالت دیباگ)

- -
-

پارامتر دریافت شده از URL: {rawCategoryName}

-

رشته‌ی آماده شده برای جستجو: {categoryNameToMatch} (طول: {categoryNameToMatch.length} کاراکتر)

-
+

+ دسته‌بندی: {categoryName} +

-

لیست دسته‌بندی‌های موجود در دیتابیس شما:

-
    - {allCategoriesInDb.map((cat, index) => ( -
  • - "{cat}" (طول: {cat.length} کاراکتر) -
  • - ))} -
- -

- لطفا طول کاراکترها و املای کلمات را در لیست بالا مقایسه کنید. (آیا نیم‌فاصله یا فاصله اضافه وجود دارد؟) -
- نکته مهم: برای دیدن لاگ‌های بیشتر، ترمینال (کنسول) محیط توسعه (مثلاً VSCode) را چک کنید، نه کنسول مرورگر را. +

+ + {products.items.map((product:any) => ( +
+ +
+ +

+ {product.title} +

+ +

+ {product.brand}

+
-
- ); - } + ))} - const categoryTitle = filteredProducts[0].category; - - // رندر حالت موفق - return ( -
-
-

- دسته‌بندی: {categoryTitle} -

-

- تعداد محصولات یافت شده: {filteredProducts.length} مورد -

-
- -
- {filteredProducts.map((product) => ( - - ))} -
-
- ); - - } catch (error) { - // هندلینگ خطاهای غیرمنتظره - console.error("Critical Error in Category Page:", error); - return ( -
- خطای پردازشی رخ داد. لطفا ترمینال VSCode را بررسی کنید.
- ); - } + +
+ ); } diff --git a/app/page.tsx b/app/page.tsx index 709922e..5f5fe37 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -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() { - {/* products → فقط ۴ تای آخر */}
- {products.slice(-4).map((product, i) => ( - + {products.slice(-4).map((product: any) => ( + ))}
+ diff --git a/app/products/[id]/[slug]/page.tsx b/app/product/[slug]/page.tsx similarity index 70% rename from app/products/[id]/[slug]/page.tsx rename to app/product/[slug]/page.tsx index 5600471..d400efd 100644 --- a/app/products/[id]/[slug]/page.tsx +++ b/app/product/[slug]/page.tsx @@ -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 (
@@ -33,28 +62,20 @@ export default async function SingleProductPage({ params }: PageProps) { خانه > محصولات > {product.title}
- {/* - کانتینر اصلی گرید (Grid) - موبایل: 1 ستون - دسکتاپ: 12 ستون - */}
- {/* ========================================= */} - {/* بخش 1: معرفی محصول (بالای ستون راست) */} - {/* دسکتاپ: 8 ستون عرض دارد. موبایل: اولین آیتم نمایش داده می‌شود */} - {/* ========================================= */} + {/* بخش 1: معرفی محصول */}
{/* باکس تصویر */}
- {product.badge && ( + {product.featured && ( - {product.badge} + پیشنهاد ویژه )} {product.title}

- طراحی شده برای عملکرد در دورهای بالا با آب‌بندی کامل پلاستیکی (Rubber Seal). مناسب برای الکتروموتور و گیربکس. + {product.meta?.shortDescription || 'توضیحات کوتاهی برای این محصول ثبت نشده است.'}

-
- تحمل بار ($C$) - {product.loadCapacity} -
-
- سرعت نهایی - {product.maxSpeed} -
-
- نوع آب‌بند - {product.sealType} -
-
- محدوده دما - {product.tempRange} -
-
+ {/* رندر داینامیک ۳ ویژگی اول از API */} + {product.attributes?.slice(0, 3).map((attr: any) => ( +
+ {attr.name} + {attr.valueText || '-'} +
+ ))} + + {/* آیتم چهارم: دسته‌بندی (برای حفظ ظاهر گرید ۴ تایی) */} +
+ دسته‌بندی + {product.primaryCategory?.name || '-'} +
+
+
- {/* ========================================= */} - {/* بخش 2: سایدبار و دکمه خرید (ستون چپ) */} - {/* دسکتاپ: 4 ستون عرض دارد و 2 ردیف طول دارد (row-span-2) تا کنار هر دو بخش راست بماند */} - {/* موبایل: با order-2 بین بخش معرفی و مشخصات قرار می‌گیرد */} - {/* ========================================= */} + {/* بخش 2: سایدبار و دکمه خرید */}
- {/* کانتینر استیکی */}
- {product.stock ? ( + {hasStock ? (
- موجود در انبار شیراز + موجود در انبار ({product.stock} عدد)
) : (
@@ -121,7 +135,7 @@ export default async function SingleProductPage({ params }: PageProps) { {formattedPrice ? ( <> {formattedPrice} - تومان + {product.currency_label} ) : ( استعلام قیمت @@ -143,6 +157,7 @@ export default async function SingleProductPage({ params }: PageProps) {
+ {/* دکمه‌های دانلود دیتاشیت و مشاوره */}
دانلود دیتاشیت @@ -171,11 +186,7 @@ export default async function SingleProductPage({ params }: PageProps) {
- {/* ========================================= */} - {/* بخش 3: مشخصات ابعادی (پایین ستون راست) */} - {/* دسکتاپ: 8 ستون عرض دارد و به طور خودکار زیر بخش معرفی قرار می‌گیرد */} - {/* موبایل: با order-3 در انتهای صفحه قرار می‌گیرد */} - {/* ========================================= */} + {/* بخش 3: مشخصات ابعادی */}
@@ -191,10 +202,10 @@ export default async function SingleProductPage({ params }: PageProps) {
- {"$D=" + (product.l ? product.l.replace('mm', '').trim() : '') + "$"} + {"$D=" + getAttribute('قطر خارجی').replace('mm', '').trim() + "$"}
- {"$d=" + (product.d ? product.d.replace('mm', '').trim() : '') + "$"} + {"$d=" + getAttribute('قطر داخلی').replace('mm', '').trim() + "$"}
@@ -204,27 +215,27 @@ export default async function SingleProductPage({ params }: PageProps) { قطر داخلی ($d$) - {product.d} + {getAttribute('قطر داخلی')} قطر خارجی ($D$) - {product.l} + {getAttribute('قطر خارجی')} پهنا ($B$) - {product.width} + {getAttribute('پهنا')} وزن خالص - {product.weight} + {getAttribute('وزن')} جنس قفسه - {product.cageMaterial} + {getAttribute('جنس قفسه')} کد بین‌المللی - {product.intCode} + {product.technicalCode} diff --git a/app/products/page.tsx b/app/products/page.tsx index 7c0ca36..e109338 100644 --- a/app/products/page.tsx +++ b/app/products/page.tsx @@ -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 (

همه محصولات

- + +
- {products.map((product) => ( - + {products.map((product: any) => ( + ))}
diff --git a/components/context/cartcontext.tsx b/components/context/cartcontext.tsx index b069a22..9294bb6 100644 --- a/components/context/cartcontext.tsx +++ b/components/context/cartcontext.tsx @@ -1,18 +1,19 @@ 'use client'; import { createContext, useContext, useState, useEffect, ReactNode } from 'react'; +import { Product } from '@/public/src/types/product'; -export interface Product { - id: string; - title: string; - image: string; - l: string; - d: string; - brand: string; - price?: string | null; - badge?: string; - stock: boolean; -} +// export interface Product { +// id: string; +// title: string; +// image: string; +// l: string; +// d: string; +// brand: string; +// price?: string | null; +// badge?: string; +// stock: boolean; +// } export interface CartItem extends Product { quantity: number; diff --git a/components/productcard.tsx b/components/productcard.tsx index 29a33b8..4613286 100644 --- a/components/productcard.tsx +++ b/components/productcard.tsx @@ -1,31 +1,24 @@ -'use client'; +'use client'; import { Phone, Plus, ShoppingCart, Minus, Trash2 } from "lucide-react"; import Image from "next/image"; import Link from 'next/link'; -import { useCart, Product } from "./context/cartcontext"; +import { useCart } from "./context/cartcontext"; +import { Product } from "@/public/src/types/product"; + interface ProductCardProps { product: Product; } + export default function ProductCard({ product }: ProductCardProps) { const { addToCart, decreaseQuantity, cart } = useCart(); - - // پیدا کردن محصول در سبد خرید + const cartItem = cart.find(item => item.id === product.id); const quantity = cartItem ? cartItem.quantity : 0; + const slug = product.slug; - const generateSlug = (text: string) => { - if (!text) return ""; - return text - .trim() - .replace(/[\s\u200c]+/g, '-') - .replace(/[^\w\u0600-\u06FF0-9\-]/g, '') - .replace(/\-\-+/g, '-'); - }; - - const slug = generateSlug(product.title); const handleIncrease = (e: React.MouseEvent) => { e.preventDefault(); @@ -39,13 +32,14 @@ export default function ProductCard({ product }: ProductCardProps) { decreaseQuantity(product.id); }; - // پاکسازی قیمت از ویرگول و تبدیل به عدد برای نمایش صحیح - const formattedPrice = product.price - ? Number(product.price.toString().replace(/,/g, '')).toLocaleString('fa-IR') + const formattedPrice = product.price + ? Number(product.price.toString().replace(/,/g, '')).toLocaleString('fa-IR') : null; - + + + return ( - +
{product.title}{product.brand}

{product.title}

-
-
-

قطر داخلی (L):

-

{product.l}

-
+ {/* + ✅ این بخش را تغییر می‌دهیم تا نام attribute ها را بگیریم + اگر attribute اول وجود داشت، نامش را بگیر و نمایش بده + اگر attribute اول نبود، نمایش نده + */} + {product.attributes?.[0] && ( +
+

{product.attributes[0].name}:

{/* 👈 نام attribute اول */} +

{product.attributes[0].valueText || "-"}

{/* 👈 مقدار attribute اول */} +
+ )} -
-

قطر خارجی (D):

-

{product.d}

-
+ {/* + ✅ مشابه بالا برای attribute دوم + */} + {product.attributes?.[1] && ( +
+

{product.attributes[1].name}:

{/* 👈 نام attribute دوم */} +

{product.attributes[1].valueText || "-"}

{/* 👈 مقدار attribute دوم */} +
+ )}
- {/* بخش قیمت و دکمه خرید */} + {product.price ? (
{formattedPrice} تومان - {/* بررسی موجودی برای نمایش دکمه سبد خرید */} {product.stock ? ( quantity > 0 ? ( -
{ e.preventDefault(); e.stopPropagation(); }} > - - + {quantity} - -
) : ( -