Add product/single product api
This commit is contained in:
@@ -1,91 +1,40 @@
|
|||||||
import { products } from "@/lib/data";
|
import { getProductsByCategory } from "@/public/src/services/products/api";
|
||||||
import ProductCard from "@/components/productcard";
|
|
||||||
|
|
||||||
interface PageProps {
|
export default async function CategoryPage({ params }:any) {
|
||||||
params: Promise<{ categoryName: string }>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function CategoryPage({ params }: PageProps) {
|
const { categoryName } = await params;
|
||||||
try {
|
|
||||||
// ۱. دریافت پارامترها
|
|
||||||
const resolvedParams = await params;
|
|
||||||
const rawCategoryName = resolvedParams.categoryName;
|
|
||||||
|
|
||||||
|
console.log("🔎 CategoryPage slug:", categoryName);
|
||||||
|
|
||||||
// ۲. دیکد کردن آدرس
|
const products = await getProductsByCategory(categoryName);
|
||||||
const decodedUrl = decodeURIComponent(rawCategoryName);
|
|
||||||
|
|
||||||
// ۳. آمادهسازی رشته برای مقایسه
|
return (
|
||||||
const categoryNameToMatch = decodedUrl.replace(/-/g, " ").trim();
|
<div className="p-8">
|
||||||
|
|
||||||
// استخراج تمام دستهبندیهای موجود در دیتابیس (برای بررسی چشمی)
|
<h1 className="text-xl font-bold mb-6">
|
||||||
const allCategoriesInDb = Array.from(new Set(products.map(p => p.category)));
|
دستهبندی: {categoryName}
|
||||||
|
</h1>
|
||||||
|
|
||||||
// ۴. فیلتر کردن (با حذف فاصلههای اضافی از دو طرف برای اطمینان)
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
|
||||||
const filteredProducts = products.filter(
|
|
||||||
(p) => p.category.trim() === categoryNameToMatch
|
|
||||||
);
|
|
||||||
|
|
||||||
// ۵. اگر پیدا نشد، صفحه دیباگ را در مرورگر نمایش بده
|
{products.items.map((product:any) => (
|
||||||
if (!filteredProducts || filteredProducts.length === 0) {
|
<div key={product.id} className="border p-4 rounded-xl">
|
||||||
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">
|
<div className="h-32 bg-gray-100 rounded mb-3" />
|
||||||
<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>
|
|
||||||
|
|
||||||
<h2 className="text-xl font-bold mb-2">لیست دستهبندیهای موجود در دیتابیس شما:</h2>
|
<h3 className="text-sm font-medium">
|
||||||
<ul className="list-disc list-inside space-y-1 bg-white p-4 rounded-md">
|
{product.title}
|
||||||
{allCategoriesInDb.map((cat, index) => (
|
</h3>
|
||||||
<li key={index} className="font-mono">
|
|
||||||
"{cat}" (طول: {cat.length} کاراکتر)
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p className="mt-4 text-sm text-gray-600">
|
<p className="text-xs text-gray-500">
|
||||||
لطفا طول کاراکترها و املای کلمات را در لیست بالا مقایسه کنید. (آیا نیمفاصله یا فاصله اضافه وجود دارد؟)
|
{product.brand}
|
||||||
<br />
|
|
||||||
<strong>نکته مهم:</strong> برای دیدن لاگهای بیشتر، ترمینال (کنسول) محیط توسعه (مثلاً VSCode) را چک کنید، نه کنسول مرورگر را.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
</div>
|
</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>
|
||||||
);
|
|
||||||
}
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
21
app/page.tsx
21
app/page.tsx
@@ -15,15 +15,28 @@ import { products } from "@/lib/data";
|
|||||||
import { articles } from "@/lib/data";
|
import { articles } from "@/lib/data";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useCategories } from "@/components/context/categoryprovider";
|
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 [activeTab, setActiveTab] = useState(0);
|
||||||
const brands = ["NTN", "KOYO", "NACHI", "TIMKEN", "FAG", "SKF"];
|
const brands = ["NTN", "KOYO", "NACHI", "TIMKEN", "FAG", "SKF"];
|
||||||
const latestArticles = articles.slice(-4);
|
const latestArticles = articles.slice(-4);
|
||||||
const { rootCategories } = useCategories();
|
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 = [
|
const features = [
|
||||||
{
|
{
|
||||||
@@ -281,13 +294,13 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* products → فقط ۴ تای آخر */}
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
<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) => (
|
{products.slice(-4).map((product: any) => (
|
||||||
<ProductCard key={i} product={product} />
|
<ProductCard key={product.id} product={product} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,57 @@
|
|||||||
import ScrollToTop from '@/components/scrolltop';
|
import ScrollToTop from '@/components/scrolltop';
|
||||||
import { products } from '@/lib/data';
|
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import ProductCartAction from '@/components/cartaction';
|
import ProductCartAction from '@/components/cartaction';
|
||||||
|
import { getProductBySlug } from "@/public/src/services/products/api";
|
||||||
|
|
||||||
interface PageProps {
|
interface PageProps {
|
||||||
params: Promise<{
|
params: Promise<{ slug: string }>;
|
||||||
id: string;
|
|
||||||
slug: string;
|
|
||||||
}>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function SingleProductPage({ params }: PageProps) {
|
export default async function SingleProductPage({ params }: PageProps) {
|
||||||
|
// --------------------------------------------------------
|
||||||
|
// ۱. منطق دریافت دادهها (دقیقا مشابه درخواست شما)
|
||||||
|
// --------------------------------------------------------
|
||||||
const resolvedParams = await params;
|
const resolvedParams = await params;
|
||||||
const product = products.find((p) => p.id.toString() === resolvedParams.id);
|
const slug = resolvedParams.slug;
|
||||||
|
|
||||||
if (!product) {
|
console.log("👉 Extracted slug from URL:", slug);
|
||||||
notFound();
|
|
||||||
|
if (!slug) {
|
||||||
|
return notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
const formattedPrice = product.price
|
const product = await getProductBySlug(slug);
|
||||||
? Number(product.price.toString().replace(/,/g, '')).toLocaleString('fa-IR')
|
|
||||||
|
if (!product) {
|
||||||
|
console.log(`🔴 Product not found or API failed for slug: ${slug}`);
|
||||||
|
return notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------
|
||||||
|
// ۲. آمادهسازی و مپ کردن دادههای API برای UI
|
||||||
|
// --------------------------------------------------------
|
||||||
|
|
||||||
|
// فرمت کردن قیمت
|
||||||
|
const formattedPrice = product.display_price
|
||||||
|
? product.display_price.toLocaleString('fa-IR')
|
||||||
: null;
|
: 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 (
|
return (
|
||||||
<div className="bg-[#f8f9fc] min-h-screen py-8" dir="rtl">
|
<div className="bg-[#f8f9fc] min-h-screen py-8" dir="rtl">
|
||||||
<div className="mx-auto px-4 lg:px-8 container max-w-7xl">
|
<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> > <span>محصولات</span> > <span className="text-gray-800 font-semibold">{product.title}</span>
|
<span>خانه</span> > <span>محصولات</span> > <span className="text-gray-800 font-semibold">{product.title}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/*
|
|
||||||
کانتینر اصلی گرید (Grid)
|
|
||||||
موبایل: 1 ستون
|
|
||||||
دسکتاپ: 12 ستون
|
|
||||||
*/}
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 lg:gap-8">
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 lg:gap-8">
|
||||||
|
|
||||||
{/* ========================================= */}
|
{/* بخش 1: معرفی محصول */}
|
||||||
{/* بخش 1: معرفی محصول (بالای ستون راست) */}
|
|
||||||
{/* دسکتاپ: 8 ستون عرض دارد. موبایل: اولین آیتم نمایش داده میشود */}
|
|
||||||
{/* ========================================= */}
|
|
||||||
<div className="lg:col-span-8 order-1 lg:order-none">
|
<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="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">
|
<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">
|
<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>
|
</span>
|
||||||
)}
|
)}
|
||||||
<Image
|
<Image
|
||||||
src={product.image}
|
src={imageUrl}
|
||||||
width={250}
|
width={250}
|
||||||
height={250}
|
height={250}
|
||||||
alt={product.title}
|
alt={product.title}
|
||||||
@@ -71,44 +92,37 @@ export default async function SingleProductPage({ params }: PageProps) {
|
|||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p className='text-gray-500 text-[13px] mb-8'>
|
<p className='text-gray-500 text-[13px] mb-8'>
|
||||||
طراحی شده برای عملکرد در دورهای بالا با آببندی کامل پلاستیکی (Rubber Seal). مناسب برای الکتروموتور و گیربکس.
|
{product.meta?.shortDescription || 'توضیحات کوتاهی برای این محصول ثبت نشده است.'}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<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">
|
{/* رندر داینامیک ۳ ویژگی اول از API */}
|
||||||
<span className="text-gray-500 text-sm mb-2">تحمل بار ($C$)</span>
|
{product.attributes?.slice(0, 3).map((attr: any) => (
|
||||||
<span className="font-bold text-gray-800 text-lg" dir="ltr">{product.loadCapacity}</span>
|
<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">
|
||||||
</div>
|
<span className="text-gray-500 text-sm mb-2">{attr.name}</span>
|
||||||
<div className="bg-gray-50 p-4 rounded-2xl flex flex-col items-center justify-center text-center border border-gray-100">
|
<span className="font-bold text-gray-800 text-sm" dir="ltr">{attr.valueText || '-'}</span>
|
||||||
<span className="text-gray-500 text-sm mb-2">سرعت نهایی</span>
|
</div>
|
||||||
<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>
|
<div className="bg-gray-50 p-4 rounded-2xl flex flex-col items-center justify-center text-center border border-gray-100">
|
||||||
<span className="font-bold text-gray-800 text-lg" dir="ltr">{product.sealType}</span>
|
<span className="text-gray-500 text-sm mb-2">دستهبندی</span>
|
||||||
</div>
|
<span className="font-bold text-gray-800 text-sm" dir="ltr">{product.primaryCategory?.name || '-'}</span>
|
||||||
<div className="bg-gray-50 p-4 rounded-2xl flex flex-col items-center justify-center text-center border border-gray-100">
|
</div>
|
||||||
<span className="text-gray-500 text-sm mb-2">محدوده دما</span>
|
</div>
|
||||||
<span className="font-bold text-gray-800 text-lg" dir="ltr">{product.tempRange}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ========================================= */}
|
{/* بخش 2: سایدبار و دکمه خرید */}
|
||||||
{/* بخش 2: سایدبار و دکمه خرید (ستون چپ) */}
|
|
||||||
{/* دسکتاپ: 4 ستون عرض دارد و 2 ردیف طول دارد (row-span-2) تا کنار هر دو بخش راست بماند */}
|
|
||||||
{/* موبایل: با order-2 بین بخش معرفی و مشخصات قرار میگیرد */}
|
|
||||||
{/* ========================================= */}
|
|
||||||
<div className="lg:col-span-4 col-span-2 lg:row-span-2 order-2 lg:order-none relative h-full">
|
<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="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">
|
<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">
|
<div className="bg-[#eaf8ee] text-[#28a745] py-3 rounded-2xl text-sm font-bold mb-4">
|
||||||
موجود در انبار شیراز
|
موجود در انبار ({product.stock} عدد)
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="bg-red-50 text-red-500 py-3 rounded-2xl text-sm font-bold mb-4">
|
<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 ? (
|
{formattedPrice ? (
|
||||||
<>
|
<>
|
||||||
<span className="text-3xl font-extrabold text-[#1a2332] tracking-tight">{formattedPrice}</span>
|
<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>
|
<span className="text-2xl font-extrabold text-[#1a2332]">استعلام قیمت</span>
|
||||||
@@ -143,6 +157,7 @@ export default async function SingleProductPage({ params }: PageProps) {
|
|||||||
</div>
|
</div>
|
||||||
</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="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">
|
<div className="flex flex-col gap-1 text-right">
|
||||||
<span className="font-extrabold text-[#1a2332] text-sm">دانلود دیتاشیت</span>
|
<span className="font-extrabold text-[#1a2332] text-sm">دانلود دیتاشیت</span>
|
||||||
@@ -171,11 +186,7 @@ export default async function SingleProductPage({ params }: PageProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ========================================= */}
|
{/* بخش 3: مشخصات ابعادی */}
|
||||||
{/* بخش 3: مشخصات ابعادی (پایین ستون راست) */}
|
|
||||||
{/* دسکتاپ: 8 ستون عرض دارد و به طور خودکار زیر بخش معرفی قرار میگیرد */}
|
|
||||||
{/* موبایل: با order-3 در انتهای صفحه قرار میگیرد */}
|
|
||||||
{/* ========================================= */}
|
|
||||||
<div className="lg:col-span-8 order-3 lg:order-none">
|
<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="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">
|
<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 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">
|
<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>
|
||||||
<div className="absolute top-[60%] right-1/2 text-orange-400 text-xs font-mono px-1 z-10" dir="ltr">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -204,27 +215,27 @@ export default async function SingleProductPage({ params }: PageProps) {
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr className="border-b border-gray-50">
|
<tr className="border-b border-gray-50">
|
||||||
<td className="py-3 text-gray-500">قطر داخلی ($d$)</td>
|
<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>
|
||||||
<tr className="border-b border-gray-50">
|
<tr className="border-b border-gray-50">
|
||||||
<td className="py-3 text-gray-500">قطر خارجی ($D$)</td>
|
<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>
|
||||||
<tr className="border-b border-gray-50">
|
<tr className="border-b border-gray-50">
|
||||||
<td className="py-3 text-gray-500">پهنا ($B$)</td>
|
<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>
|
||||||
<tr className="border-b border-gray-50">
|
<tr className="border-b border-gray-50">
|
||||||
<td className="py-3 text-gray-500">وزن خالص</td>
|
<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>
|
||||||
<tr className="border-b border-gray-50">
|
<tr className="border-b border-gray-50">
|
||||||
<td className="py-3 text-gray-500">جنس قفسه</td>
|
<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>
|
||||||
<tr>
|
<tr>
|
||||||
<td className="py-3 text-gray-500">کد بینالمللی</td>
|
<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>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -1,15 +1,29 @@
|
|||||||
// app/products/page.tsx
|
// app/products/page.tsx
|
||||||
import ProductCard from '@/components/productcard'; // مسیر را با توجه به ساختار خود اصلاح کنید
|
import ProductCard from '@/components/productcard';
|
||||||
import { products } from '@/lib/data'; // ایمپورت دیتای مرکزی
|
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 (
|
return (
|
||||||
<div className="container mx-auto p-4">
|
<div className="container mx-auto p-4">
|
||||||
<h1 className="text-2xl font-bold mb-6">همه محصولات</h1>
|
<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">
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
||||||
{products.map((product) => (
|
{products.map((product: any) => (
|
||||||
<ProductCard key={product.title} product={product} />
|
<ProductCard key={product.id} product={product} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||||
|
import { Product } from '@/public/src/types/product';
|
||||||
|
|
||||||
export interface Product {
|
// export interface Product {
|
||||||
id: string;
|
// id: string;
|
||||||
title: string;
|
// title: string;
|
||||||
image: string;
|
// image: string;
|
||||||
l: string;
|
// l: string;
|
||||||
d: string;
|
// d: string;
|
||||||
brand: string;
|
// brand: string;
|
||||||
price?: string | null;
|
// price?: string | null;
|
||||||
badge?: string;
|
// badge?: string;
|
||||||
stock: boolean;
|
// stock: boolean;
|
||||||
}
|
// }
|
||||||
|
|
||||||
export interface CartItem extends Product {
|
export interface CartItem extends Product {
|
||||||
quantity: number;
|
quantity: number;
|
||||||
|
|||||||
@@ -3,29 +3,22 @@
|
|||||||
import { Phone, Plus, ShoppingCart, Minus, Trash2 } from "lucide-react";
|
import { Phone, Plus, ShoppingCart, Minus, Trash2 } from "lucide-react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from 'next/link';
|
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 {
|
interface ProductCardProps {
|
||||||
product: Product;
|
product: Product;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default function ProductCard({ product }: ProductCardProps) {
|
export default function ProductCard({ product }: ProductCardProps) {
|
||||||
const { addToCart, decreaseQuantity, cart } = useCart();
|
const { addToCart, decreaseQuantity, cart } = useCart();
|
||||||
|
|
||||||
// پیدا کردن محصول در سبد خرید
|
|
||||||
const cartItem = cart.find(item => item.id === product.id);
|
const cartItem = cart.find(item => item.id === product.id);
|
||||||
const quantity = cartItem ? cartItem.quantity : 0;
|
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) => {
|
const handleIncrease = (e: React.MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -39,13 +32,14 @@ export default function ProductCard({ product }: ProductCardProps) {
|
|||||||
decreaseQuantity(product.id);
|
decreaseQuantity(product.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
// پاکسازی قیمت از ویرگول و تبدیل به عدد برای نمایش صحیح
|
|
||||||
const formattedPrice = product.price
|
const formattedPrice = product.price
|
||||||
? Number(product.price.toString().replace(/,/g, '')).toLocaleString('fa-IR')
|
? Number(product.price.toString().replace(/,/g, '')).toLocaleString('fa-IR')
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link href={`/products/${product.id}/${slug}`} scroll={true} className="bg-white gap-1 flex flex-col justify-between border border-gray-200 rounded-xl shadow-sm p-4 transition-all hover:shadow-md">
|
<Link href={`/product/${slug}`} scroll={true} className="bg-white gap-1 flex flex-col justify-between border border-gray-200 rounded-xl shadow-sm p-4 transition-all hover:shadow-md">
|
||||||
|
|
||||||
<div className="relative flex justify-center mb-4">
|
<div className="relative flex justify-center mb-4">
|
||||||
<span
|
<span
|
||||||
@@ -55,7 +49,7 @@ export default function ProductCard({ product }: ProductCardProps) {
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<Image
|
<Image
|
||||||
src={product.image}
|
src={product.image || "/placeholder.png"}
|
||||||
width={150}
|
width={150}
|
||||||
height={150}
|
height={150}
|
||||||
alt={product.title}
|
alt={product.title}
|
||||||
@@ -64,27 +58,37 @@ export default function ProductCard({ product }: ProductCardProps) {
|
|||||||
|
|
||||||
<p className="text-[#808080] text-sm">{product.brand}</p>
|
<p className="text-[#808080] text-sm">{product.brand}</p>
|
||||||
<h3 className="text-sm font-semibold">{product.title}</h3>
|
<h3 className="text-sm font-semibold">{product.title}</h3>
|
||||||
|
|
||||||
<div className="text-xs text-gray-500 mt-2">
|
<div className="text-xs text-gray-500 mt-2">
|
||||||
<div className="flex border-b border-[#dfdfdf] mb-2 pb-2 justify-between">
|
{/*
|
||||||
<p>قطر داخلی (L):</p>
|
✅ این بخش را تغییر میدهیم تا نام attribute ها را بگیریم
|
||||||
<p dir="ltr" className="font-semibold text-black">{product.l}</p>
|
اگر attribute اول وجود داشت، نامش را بگیر و نمایش بده
|
||||||
</div>
|
اگر attribute اول نبود، نمایش نده
|
||||||
|
*/}
|
||||||
|
{product.attributes?.[0] && (
|
||||||
|
<div className="flex border-b border-[#dfdfdf] mb-2 pb-2 justify-between">
|
||||||
|
<p>{product.attributes[0].name}:</p> {/* 👈 نام attribute اول */}
|
||||||
|
<p dir="ltr" className="font-semibold text-black">{product.attributes[0].valueText || "-"}</p> {/* 👈 مقدار attribute اول */}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex justify-between">
|
{/*
|
||||||
<p>قطر خارجی (D):</p>
|
✅ مشابه بالا برای attribute دوم
|
||||||
<p dir="ltr" className="font-semibold text-black">{product.d}</p>
|
*/}
|
||||||
</div>
|
{product.attributes?.[1] && (
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<p>{product.attributes[1].name}:</p> {/* 👈 نام attribute دوم */}
|
||||||
|
<p dir="ltr" className="font-semibold text-black">{product.attributes[1].valueText || "-"}</p> {/* 👈 مقدار attribute دوم */}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* بخش قیمت و دکمه خرید */}
|
|
||||||
{product.price ? (
|
{product.price ? (
|
||||||
<div className="flex justify-between mt-3 items-center min-h-[32px]">
|
<div className="flex justify-between mt-3 items-center min-h-[32px]">
|
||||||
<span className="text-[0.85em] font-bold text-gray-800">
|
<span className="text-[0.85em] font-bold text-gray-800">
|
||||||
{formattedPrice} تومان
|
{formattedPrice} تومان
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{/* بررسی موجودی برای نمایش دکمه سبد خرید */}
|
|
||||||
{product.stock ? (
|
{product.stock ? (
|
||||||
quantity > 0 ? (
|
quantity > 0 ? (
|
||||||
<div
|
<div
|
||||||
@@ -122,7 +126,6 @@ export default function ProductCard({ product }: ProductCardProps) {
|
|||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
// جایگزین دکمه خرید وقتی محصول ناموجود است
|
|
||||||
<span className="text-[10px] text-red-500 font-medium bg-red-50 px-2 py-1 rounded-md">
|
<span className="text-[10px] text-red-500 font-medium bg-red-50 px-2 py-1 rounded-md">
|
||||||
عدم موجودی
|
عدم موجودی
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
import "./.next/types/routes.d.ts";
|
import "./.next/dev/types/routes.d.ts";
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ export async function registerUser(data: RegisterPayload) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export async function loginUser(data: LoginPayload) {
|
export async function loginUser(data: LoginPayload) {
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
|
|||||||
@@ -1,49 +1,95 @@
|
|||||||
import { ProductsResponse } from "../../types/product";
|
|
||||||
|
|
||||||
const BASE_URL = "https://parsshop-back.mugit.ir/api";
|
import { API_BASE_URL } from "../config";
|
||||||
|
|
||||||
export const productService = {
|
export async function getProductsByCategory(
|
||||||
/**
|
slug: string,
|
||||||
* دریافت لیست تمام محصولات
|
page = 1,
|
||||||
* @param page شماره صفحه (پیشفرض: 1)
|
limit = 20
|
||||||
* @param limit تعداد در هر صفحه (پیشفرض: 20)
|
) {
|
||||||
* @returns ProductsResponse
|
try {
|
||||||
*/
|
const url = `${API_BASE_URL}/products/categories/${slug}?page=${page}&limit=${limit}`;
|
||||||
async getProducts(page: number = 1, limit: number = 20): Promise<ProductsResponse> {
|
|
||||||
try {
|
|
||||||
// استفاده از URLSearchParams برای مدیریت تمیزتر کوئری پارامترها (در صورت نیاز)
|
|
||||||
const url = new URL(`${BASE_URL}/products`);
|
|
||||||
// اگر API شما از کوئری پشتیبانی میکند، میتوانید به این شکل ارسال کنید:
|
|
||||||
// url.searchParams.append('page', page.toString());
|
|
||||||
// url.searchParams.append('limit', limit.toString());
|
|
||||||
|
|
||||||
const response = await fetch(url.toString(), {
|
console.log("📡 Fetching:", url);
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
// هدرهای ضروری بر اساس مستندات شما (معمولاً کلاینت فقط Accept را میفرستد)
|
|
||||||
"Accept": "application/json",
|
|
||||||
"Content-Type": "application/json; charset=utf-8",
|
|
||||||
},
|
|
||||||
// در Next.js برای کشینگ میتوانید از گزینههای زیر استفاده کنید:
|
|
||||||
// cache: 'no-store', // اگر دادهها مدام تغییر میکنند (مثل موجودی)
|
|
||||||
next: { revalidate: 60 } // کش کردن دادهها برای 60 ثانیه (مناسب برای محصولات)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
const res = await fetch(url, {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
method: "GET",
|
||||||
}
|
headers: {
|
||||||
|
"Accept": "application/json",
|
||||||
|
},
|
||||||
|
cache: "no-store",
|
||||||
|
});
|
||||||
|
|
||||||
const data: ProductsResponse = await response.json();
|
if (!res.ok) {
|
||||||
return data;
|
const errorText = await res.text();
|
||||||
} catch (error) {
|
console.error("❌ API error:", errorText);
|
||||||
console.error("Error fetching products:", error);
|
throw new Error("API Error " + res.status);
|
||||||
// مدیریت خطا (بسته به سیاست پروژه شما میتواند throw شود یا مقدار پیشفرض برگردد)
|
|
||||||
throw new Error("مشکلی در دریافت اطلاعات محصولات به وجود آمد.");
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
const json = await res.json();
|
||||||
* دریافت محصول بر اساس Slug یا ID (برای استفاده در صفحات سینگل پروداکت در آینده)
|
|
||||||
*/
|
return json.data;
|
||||||
// async getProductBySlug(slug: string) { ... }
|
} catch (err) {
|
||||||
};
|
console.error("🔥 getProductsByCategory Error:", err);
|
||||||
|
return { items: [], meta: { total: 0, page: 1, limit: 20 } };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function getProducts(page = 1, limit = 20) {
|
||||||
|
try {
|
||||||
|
const url = `${API_BASE_URL}/products?page=${page}&limit=${limit}`;
|
||||||
|
|
||||||
|
console.log("📡 Fetching products:", url);
|
||||||
|
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
cache: "no-store",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const errorText = await res.text();
|
||||||
|
console.error("❌ API Error:", errorText);
|
||||||
|
throw new Error(`API Error ${res.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
return json.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("🔥 getProducts error:", error);
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: [],
|
||||||
|
meta: {
|
||||||
|
total: 0,
|
||||||
|
page: 1,
|
||||||
|
limit: 20,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function getProductBySlug(slug: string) {
|
||||||
|
try {
|
||||||
|
const url = `${API_BASE_URL}/product/${slug}`;
|
||||||
|
|
||||||
|
const res = await fetch(url, { cache: "no-store" });
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const errorText = await res.text();
|
||||||
|
console.error("❌ API Error:", errorText);
|
||||||
|
throw new Error(`API Error ${res.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = await res.json();
|
||||||
|
return json.data;
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error("🔥 getProductBySlug error:", err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,32 +1,32 @@
|
|||||||
// ساختار هر محصول (Product) بر اساس بادی ریسپانس
|
// // ساختار هر محصول (Product) بر اساس بادی ریسپانس
|
||||||
export interface Product {
|
// export interface Product {
|
||||||
id: string;
|
// id: string;
|
||||||
sku: string;
|
// sku: string;
|
||||||
title: string;
|
// title: string;
|
||||||
slug: string;
|
// slug: string;
|
||||||
technicalCode: string;
|
// technicalCode: string;
|
||||||
brand: string;
|
// brand: string;
|
||||||
brandEntity: any | null; // اگر ساختار مشخصی دارد، بجای any از تایپ مناسب استفاده کنید
|
// brandEntity: any | null; // اگر ساختار مشخصی دارد، بجای any از تایپ مناسب استفاده کنید
|
||||||
basePriceUSD: number;
|
// basePriceUSD: number;
|
||||||
salePriceUSD: number;
|
// salePriceUSD: number;
|
||||||
stock: number;
|
// stock: number;
|
||||||
featured: boolean;
|
// featured: boolean;
|
||||||
type: string;
|
// type: string;
|
||||||
status: string;
|
// status: string;
|
||||||
mainImageUrl: string | null;
|
// mainImageUrl: string | null;
|
||||||
threeDModelUrl: string | null;
|
// threeDModelUrl: string | null;
|
||||||
imageGalleryUrls: string[];
|
// imageGalleryUrls: string[];
|
||||||
tags: string[];
|
// tags: string[];
|
||||||
averageRating: number;
|
// averageRating: number;
|
||||||
reviewsCount: number;
|
// reviewsCount: number;
|
||||||
primaryCategory: any | null;
|
// primaryCategory: any | null;
|
||||||
categories: any[];
|
// categories: any[];
|
||||||
meta: any | null;
|
// meta: any | null;
|
||||||
createdAt: string;
|
// createdAt: string;
|
||||||
updatedAt: string;
|
// updatedAt: string;
|
||||||
brandInfo: any | null;
|
// brandInfo: any | null;
|
||||||
attributes: any[];
|
// attributes: any[];
|
||||||
}
|
// }
|
||||||
|
|
||||||
// ساختار متادیتا (Paginagtion)
|
// ساختار متادیتا (Paginagtion)
|
||||||
export interface PaginationMeta {
|
export interface PaginationMeta {
|
||||||
@@ -35,15 +35,32 @@ export interface Product {
|
|||||||
limit: number;
|
limit: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ساختار نهایی ریسپانس (Response Body)
|
|
||||||
export interface ProductsResponse {
|
|
||||||
success: boolean;
|
export interface ProductAttribute {
|
||||||
statusCode: number;
|
id: string;
|
||||||
path: string;
|
name: string;
|
||||||
timestamp: string;
|
slug: string;
|
||||||
data: {
|
valueText: string | null;
|
||||||
items: Product[];
|
}
|
||||||
meta: PaginationMeta;
|
|
||||||
};
|
export interface Product {
|
||||||
}
|
id: string;
|
||||||
|
title: string;
|
||||||
|
brand: string;
|
||||||
|
price?: number;
|
||||||
|
stock: number;
|
||||||
|
image: string;
|
||||||
|
slug: string;
|
||||||
|
attributes?: ProductAttribute[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductsResponse {
|
||||||
|
items: Product[];
|
||||||
|
meta: {
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user