category api - dashboard page

This commit is contained in:
haniyeroozmand
2026-03-27 22:48:14 +03:30
parent 299eb2d968
commit 6d121f059d
13 changed files with 1013 additions and 187 deletions

View File

@@ -3,9 +3,11 @@ import React, { useState, useEffect, useRef } from 'react';
import { usePathname, useRouter } from 'next/navigation';
import Link from 'next/link';
import Image from 'next/image';
import { ShoppingCart, Trash2 } from "lucide-react";
import { Category } from '@/public/src/types/categories';
import { ShoppingCart, Trash2, Search, X, ChevronDown } from "lucide-react";
import { useCart } from './context/cartcontext';
import { Search, X } from 'lucide-react';
import { products } from '@/lib/data';
import '@/public/src/css/header.css';
@@ -16,13 +18,16 @@ const topBarLinks = [
const mainNavLinks = [
{ label: "صفحه اصلی", href: "/" },
{ label: "خرید بلبرینگ", href: "/products" },
{ label: "محصولات", href: "/products" },
{ label: "مقالات", href: "/blog" },
{ label: "درباره ما", href: "/about" },
{ label: "تماس با ما", href: "/contact" },
];
export function Header() {
// 1. استخراج دسته‌بندی‌های منحصر به فرد از محصولات
const uniqueCategories = Array.from(new Set(products.map(p => p.category)));
export function Header({ categories }: { categories: Category[] }) {
const [menuOpen, setMenuOpen] = useState(false);
const pathname = usePathname();
const { cart, removeFromCart } = useCart();
@@ -31,8 +36,8 @@ export function Header() {
const [filteredProducts, setFilteredProducts] = useState<any[]>([]);
const searchRef = useRef<HTMLDivElement>(null);
const router = useRouter();
// محاسبه قیمت کل برای سبد خرید (اصلاح شده برای حذف ویرگول‌های احتمالی)
const safeCategories = categories || [];
const rootCategories = safeCategories.filter(cat => cat.parent === null);
const parsePrice = (priceStr?: string | null) => {
if (!priceStr) return 0;
return Number(priceStr.toString().replace(/,/g, ''));
@@ -40,13 +45,6 @@ export function Header() {
const totalPrice = cart.reduce((total, item) => total + (parsePrice(item.price) * item.quantity), 0);
const isLinkActive = (href: string): boolean => {
if (href === "/") {
return pathname === "/";
}
return pathname === href;
};
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (searchRef.current && !searchRef.current.contains(event.target as Node)) {
@@ -57,7 +55,6 @@ export function Header() {
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
// فیلتر کردن محصولات در لحظه تایپ
useEffect(() => {
if (searchTerm.trim().length > 1) {
const results = products.filter(p =>
@@ -65,7 +62,7 @@ export function Header() {
p.brand?.toLowerCase().includes(searchTerm.toLowerCase()) ||
p.d?.toLowerCase().includes(searchTerm.toLowerCase()) ||
p.l?.toLowerCase().includes(searchTerm.toLowerCase())
).slice(0, 6); // نمایش فقط ۶ نتیجه اول در دراپ‌داون
).slice(0, 6);
setFilteredProducts(results);
setShowResults(true);
} else {
@@ -82,10 +79,12 @@ export function Header() {
}
};
// ... (بقیه کدهای شما از اینجا تا بخش <nav> دست نخورده باقی می‌ماند)
// ... (بخش‌های top bar, main bar, search, cart, etc. بدون تغییر هستند)
return (
<header>
<div>
{/* ... کدهای مربوط به mobile slider, top bar, main bar ... */}
{/* overlay */}
{menuOpen && (
<div
@@ -244,48 +243,30 @@ export function Header() {
<div className="absolute top-full left-0 right-0 mt-2 bg-white border border-gray-200 rounded-2xl shadow-xl z-[100] overflow-hidden animate-in fade-in slide-in-from-top-2 duration-200">
<div className="p-2 flex flex-col">
{filteredProducts.map((product) => {
// تبدیل نام محصول به اسلاگ (جایگزینی فاصله‌ها با خط تیره)
const productSlug = product.title.replace(/\s+/g, '-');
return (
<Link
key={product.id}
// اصلاح URL برای تطابق با /products/[id]/[slug]
href={`/products/${product.id}/${encodeURIComponent(productSlug)}`}
onClick={() => setShowResults(false)}
className="flex items-center gap-3 p-2 hover:bg-gray-50 rounded-xl transition group"
>
<div className="bg-gray-100 p-1 rounded-lg shrink-0 w-10 h-10 flex items-center justify-center">
<Image
src={product.image}
alt={product.title}
width={32}
height={32}
className="object-contain"
/>
<Image src={product.image} alt={product.title} width={32} height={32} className="object-contain" />
</div>
<div className="flex-1 min-w-0 text-right">
<p className="text-[11px] font-bold text-gray-800 truncate group-hover:text-blue-600 transition-colors">
{product.title}
</p>
<p className="text-[11px] font-bold text-gray-800 truncate group-hover:text-blue-600 transition-colors">{product.title}</p>
<p className="text-[9px] text-gray-500">{product.brand}</p>
</div>
<div className="text-[10px] font-bold text-gray-700 bg-gray-50 px-2 py-1 rounded-md shrink-0">
{/* اعمال منطق حذف کاما و تبدیل به عدد در صورت رشته بودن قیمت */}
{product.price
? `${Number(product.price.toString().replace(/,/g, '')).toLocaleString('fa-IR')} ت`
: 'استعلام'}
{product.price ? `${Number(product.price.toString().replace(/,/g, '')).toLocaleString('fa-IR')} ت` : 'استعلام'}
</div>
</Link>
);
})}
</div>
{/* دکمه مشاهده همه نتایج */}
<button
onClick={handleSearch}
className="w-full bg-gray-50 py-2.5 text-[11px] text-blue-600 font-bold border-t border-gray-100 hover:bg-blue-50 transition-colors"
>
<button onClick={handleSearch} className="w-full bg-gray-50 py-2.5 text-[11px] text-blue-600 font-bold border-t border-gray-100 hover:bg-blue-50 transition-colors">
مشاهده تمام نتایج برای "{searchTerm}"
</button>
</div>
@@ -300,22 +281,16 @@ export function Header() {
<Link href="/cart" className="flex items-center justify-center p-2.5 bg-white border border-gray-300/60 rounded-xl text-gray-700 hover:bg-gray-50 transition relative z-10">
<ShoppingCart className="w-4 h-4 text-gray-500" strokeWidth={1.8} />
{/* بج (Badge) تعداد محصولات */}
{cart.length > 0 && (
<span className="absolute -top-1.5 -right-1.5 bg-[#f92a35] text-white text-[10px] font-bold w-4 h-4 flex items-center justify-center rounded-full shadow-sm">
{cart.reduce((total, item) => total + item.quantity, 0)}
</span>
)}
</Link>
{/* یک پل نامرئی برای جلوگیری از قطع شدن هاور */}
<div className="absolute top-full left-0 w-full h-3 bg-transparent hidden group-hover:block"></div>
{/* منوی کشویی سبد خرید */}
<div className="absolute top-[calc(100%+12px)] left-0 w-80 bg-white border border-gray-200 rounded-2xl shadow-xl hidden group-hover:flex flex-col overflow-hidden z-50">
{cart.length === 0 ? (
<div className="p-6 text-center text-sm text-gray-500 flex flex-col items-center gap-2">
<ShoppingCart className="w-8 h-8 text-gray-200" />
@@ -325,46 +300,25 @@ export function Header() {
<>
<div className="flex justify-between items-center p-4 border-b border-gray-100 bg-gray-50/50">
<span className="text-xs font-semibold text-gray-500">{cart.length} کالا</span>
<Link href="/cart" className="text-xs text-blue-500 hover:text-blue-700 font-medium transition">
مشاهده سبد خرید
</Link>
<Link href="/cart" className="text-xs text-blue-500 hover:text-blue-700 font-medium transition">مشاهده سبد خرید</Link>
</div>
<div className="max-h-64 overflow-y-auto p-2 flex flex-col gap-1 custom-scrollbar">
{cart.slice(0, 3).map((item) => {
// محاسبه قیمت بر اساس تعداد (قیمت واحد × تعداد)
const itemTotal = item.price
? (Number(item.price.toString().replace(/,/g, '')) * item.quantity).toLocaleString('fa-IR')
: null;
const itemTotal = item.price ? (Number(item.price.toString().replace(/,/g, '')) * item.quantity).toLocaleString('fa-IR') : null;
return (
<div key={item.id} className="flex items-center gap-3 p-2 hover:bg-gray-50 rounded-xl transition group/item relative">
<div className="bg-white border border-gray-100 p-1 rounded-lg shrink-0">
<Image src={item.image} alt={item.title} width={40} height={40} className="object-contain w-10 h-10" />
</div>
<div className="flex-1 min-w-0">
<h4 className="text-xs font-semibold text-gray-800 truncate">{item.title}</h4>
{/* بخش جدید: نمایش قیمت و تعداد در کنار هم */}
<div className="flex items-center gap-2 mt-1">
<span className="text-[10px] text-gray-500 font-medium">
{itemTotal ? `${itemTotal} تومان` : 'استعلام'}
</span>
{/* بج نمایش تعداد محصول */}
<span className="text-[9px] font-bold text-blue-600 bg-blue-50 px-1.5 py-0.5 rounded-md border border-blue-100">
{item.quantity} عدد
</span>
<span className="text-[10px] text-gray-500 font-medium">{itemTotal ? `${itemTotal} تومان` : 'استعلام'}</span>
<span className="text-[9px] font-bold text-blue-600 bg-blue-50 px-1.5 py-0.5 rounded-md border border-blue-100">{item.quantity} عدد</span>
</div>
</div>
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
removeFromCart(item.id);
}}
onClick={(e) => { e.preventDefault(); e.stopPropagation(); removeFromCart(item.id); }}
className="text-gray-300 hover:text-red-500 transition p-1.5 opacity-0 group-hover/item:opacity-100"
title="حذف"
>
@@ -373,49 +327,70 @@ export function Header() {
</div>
)
})}
{cart.length > 3 && (
<p className="text-center text-[10px] text-gray-400 py-2 border-t border-gray-50 mt-1">
و {cart.length - 3} کالای دیگر...
</p>
)}
{cart.length > 3 && (<p className="text-center text-[10px] text-gray-400 py-2 border-t border-gray-50 mt-1"> و {cart.length - 3} کالای دیگر...</p>)}
</div>
<div className="p-4 bg-white border-t border-gray-100">
<div className="flex justify-between items-center mb-3">
<span className="text-xs text-gray-500">مبلغ قابل پرداخت:</span>
<span className="text-sm font-bold text-gray-800">
{totalPrice > 0 ? `${totalPrice.toLocaleString('fa-IR')} تومان` : 'استعلام'}
</span>
<span className="text-sm font-bold text-gray-800">{totalPrice > 0 ? `${totalPrice.toLocaleString('fa-IR')} تومان` : 'استعلام'}</span>
</div>
<Link href="/cart" className="flex items-center justify-center w-full py-2 bg-[#ffb900] hover:bg-[#e6a600] text-black text-xs font-semibold rounded-xl transition-colors">
ثبت سفارش
</Link>
<Link href="/cart" className="flex items-center justify-center w-full py-2 bg-[#ffb900] hover:bg-[#e6a600] text-black text-xs font-semibold rounded-xl transition-colors">ثبت سفارش</Link>
</div>
</>
)}
</div>
</div>
<button className="flex items-center gap-2 px-3 py-2.5 bg-white border border-gray-300/60 rounded-xl text-xs text-gray-700 hover:bg-gray-50 transition">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="w-4 h-4 text-gray-500">
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.5 20.25a7.5 7.5 0 0115 0" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="w-4 h-4 text-gray-500"><path strokeLinecap="round" strokeLinejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.5 20.25a7.5 7.5 0 0115 0" /></svg>
<span>ورود / عضویت</span>
</button>
</div>
</div>
</div>
{/* nav */}
{/* nav -- این بخش تغییر کرده است */}
<div className='justify-center flex'>
<div className='flex max-w-6xl container mx-auto px-4 justify-between py-2 items-center'>
<nav className="flex space-x-6 rtl:space-x-4 text-gray-700 text-xs w-full lg:w-auto lg:order-none">
{mainNavLinks.map((link) => (
<a key={link.label} href={link.href} className="hover:text-blue-600 transition duration-200 font-medium hidden md:block">
<nav className="hidden md:flex items-center space-x-6 rtl:space-x-4 text-gray-700 text-xs w-full lg:w-auto lg:order-none">
{/* لینک صفحه اصلی */}
<Link href="/" className="hover:text-blue-600 transition duration-200 font-medium">صفحه اصلی</Link>
<div className="relative group">
<div className="flex items-center gap-1 cursor-pointer hover:text-blue-600 transition duration-200 font-medium">
<span>دسته بندی محصولات</span>
<ChevronDown size={14} className="transition-transform duration-200 group-hover:rotate-180" />
</div>
<div className="absolute top-full right-0 w-full h-4 bg-transparent hidden group-hover:block"></div>
<div className="absolute top-[calc(100%+16px)] right-0 w-56 bg-white border border-gray-200 rounded-xl shadow-lg p-2 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 flex flex-col gap-1 z-50">
{rootCategories.length > 0 ? (
rootCategories.map((category) => (
<Link
key={category.id}
href={`/category/${category.slug}`}
className="block w-full text-right px-3 py-2 text-xs font-medium text-gray-600 hover:bg-gray-100 hover:text-blue-600 rounded-md transition-colors"
>
{category.name}
</Link>
))
) : (
<div className="text-center text-xs text-gray-500 py-2">
در حال بارگذاری...
</div>
)}
</div>
</div>
{/* نمایش بقیه لینک‌ها */}
{mainNavLinks.filter(link => link.href !== "/").map((link) => (
<Link key={link.label} href={link.href} className="hover:text-blue-600 transition duration-200 font-medium">
{link.label}
</a>
</Link>
))}
</nav>

View File

@@ -3,7 +3,7 @@
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, Product } from "./context/cartcontext";
interface ProductCardProps {
product: Product;
@@ -77,50 +77,59 @@ export default function ProductCard({ product }: ProductCardProps) {
</div>
</div>
{/* بخش قیمت و دکمه خرید */}
{product.price ? (
<div className="flex justify-between mt-3 items-center min-h-[32px]">
<span className="text-[0.85em] font-bold">
<span className="text-[0.85em] font-bold text-gray-800">
{formattedPrice} تومان
</span>
{quantity > 0 ? (
<div
className="flex items-center gap-3 bg-white border border-gray-200 rounded-lg py-1 px-2 shadow-sm"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
{/* بررسی موجودی برای نمایش دکمه سبد خرید */}
{product.stock ? (
quantity > 0 ? (
<div
className="flex items-center gap-3 bg-white border border-gray-200 rounded-lg py-1 px-2 shadow-sm"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
<button
onClick={handleIncrease}
className="text-gray-500 hover:text-green-600 transition-colors"
>
<Plus size={16} />
</button>
<span className="text-xs font-bold text-gray-800 w-3 text-center">
{quantity}
</span>
<button
onClick={handleDecrease}
className="text-gray-500 hover:text-red-500 transition-colors"
>
{quantity === 1 ? <Trash2 size={16} /> : <Minus size={16} />}
</button>
</div>
) : (
<button
onClick={handleIncrease}
className="text-gray-500 hover:text-green-600 transition-colors"
className="rounded-lg border border-[#e6e6e6] bg-gray-50 flex items-center p-2 text-gray-600 hover:bg-gray-200 transition-colors"
title="افزودن به سبد خرید"
>
<Plus size={16} />
<ShoppingCart size={16} />
</button>
<span className="text-xs font-bold text-gray-800 w-3 text-center">
{quantity}
</span>
<button
onClick={handleDecrease}
className="text-gray-500 hover:text-red-500 transition-colors"
>
{quantity === 1 ? <Trash2 size={16} /> : <Minus size={16} />}
</button>
</div>
)
) : (
<button
onClick={handleIncrease}
className="rounded-lg border border-[#e6e6e6] bg-gray-50 flex items-center p-2 text-gray-600 hover:bg-gray-200 transition-colors"
title="افزودن به سبد خرید"
>
<ShoppingCart size={16} />
</button>
// جایگزین دکمه خرید وقتی محصول ناموجود است
<span className="text-[10px] text-red-500 font-medium bg-red-50 px-2 py-1 rounded-md">
عدم موجودی
</span>
)}
</div>
) : (
<span className="flex justify-between mt-3 text-[0.85em] font-bold items-center gap-1 min-h-[32px]">
<span className="flex justify-between mt-3 text-[0.85em] font-bold items-center gap-1 min-h-[32px] text-gray-700">
استعلام
<Phone className="mb-1" size={13} />
</span>