first commit

This commit is contained in:
haniyeroozmand
2026-03-21 18:58:07 +03:30
parent 738b02d376
commit 8b733c817c
40 changed files with 3968 additions and 0 deletions

62
app/blog/[slug]/page.tsx Normal file
View File

@@ -0,0 +1,62 @@
import { articles } from '@/lib/data';// مسیر دیتای خود را تنظیم کنید
import { notFound } from 'next/navigation';
import Image from "next/image";
// همان تابع استانداردسازی باید اینجا هم باشد تا بتوانیم مقایسه درستی انجام دهیم
const generateSlug = (text: string) => {
if (!text) return "";
return text
.trim()
.replace(/[\s\u200c]+/g, '-')
.replace(/[^\w\u0600-\u06FF0-9\-]/g, '')
.replace(/\-\-+/g, '-')
.replace(/^-+|-+$/g, '');
};
interface PageProps {
params: Promise<{
slug: string;
}>;
}
export default async function SingleArticlePage({ params }: PageProps) {
const resolvedParams = await params;
// ۱. مرورگر کاراکترهای فارسی در آدرس را به طور پیش‌فرض کدگذاری می‌کند، پس ابتدا آن را دیکد (Decode) می‌کنیم
const decodedUrlSlug = decodeURIComponent(resolvedParams.slug);
// ۲. جستجو در دیتابیس: عنوان هر مقاله را به همان فرمت تبدیل کرده و با مقدار موجود در آدرس مقایسه می‌کنیم
const article = articles.find((a) => generateSlug(a.title) === decodedUrlSlug);
// ۳. اگر مقاله‌ای با این عنوان پیدا نشد، ارور ۴۰۴ بده
if (!article) {
notFound();
}
return (
<article className="container mx-auto p-8 max-w-3xl">
<header className="mb-8">
<h1 className="text-3xl font-bold mb-4">{article.title}</h1>
<span className="text-gray-500 text-sm">{article.date}</span>
</header>
<div className="relative w-full h-[400px] mb-8">
<Image
src={article.image}
alt={article.title}
fill
className="object-cover rounded-xl"
/>
</div>
<div className="bg-gray-50 p-4 border-r-4 border-[#ffb900] mb-8 font-medium">
{article.excerpt}
</div>
{/* بخش نمایش محتوای کامل که درخواست کرده بودید */}
<div className="prose max-w-none leading-loose">
{article.content}
</div>
</article>
);
}

144
app/blog/page.tsx Normal file
View File

@@ -0,0 +1,144 @@
'use client';
import { useState, useMemo } from 'react';
import ArticleCard from "@/components/articlecard";
import { articles } from "@/lib/data";
import { Search, BookOpen, FileText } from "lucide-react";
// رابط کاربری مقالات (فیلد category اضافه شد)
interface Article {
title: string;
image: string;
excerpt: string;
date: string;
category?: string; // این فیلد را در صورت امکان به دیتای خود در lib/data.ts اضافه کنید
}
export default function BlogListingPage() {
// ۱. استیت‌های مربوط به فیلتر و جستجو
const [activeCategory, setActiveCategory] = useState<string>('همه');
const [searchQuery, setSearchQuery] = useState<string>('');
// ۲. استخراج خودکار دسته‌بندی‌های یکتا از دیتای مقالات
const categories = useMemo(() => {
// اگر مقاله‌ای دسته‌بندی نداشت، آن را در 'سایر' قرار می‌دهیم
const allCategories = articles.map(article => article.category || 'سایر');
const uniqueCategories = Array.from(new Set(allCategories));
return ['همه', ...uniqueCategories];
}, []);
// ۳. فیلتر کردن مقالات بر اساس دسته‌بندی و متن جستجو
const filteredArticles = useMemo(() => {
return articles.filter((article) => {
const articleCategory = article.category || 'سایر';
const matchesCategory = activeCategory === 'همه' || articleCategory === activeCategory;
const matchesSearch =
article.title.includes(searchQuery) ||
article.excerpt.includes(searchQuery);
return matchesCategory && matchesSearch;
});
}, [activeCategory, searchQuery]);
return (
<main className="min-h-screen bg-gray-50/30 pb-20">
{/*
بخش هدر (Hero Section)
استفاده از گرادیانت ملایم و استایل‌های مدرن
*/}
<div className="bg-gradient-to-b from-gray-50/80 to-transparent pt-12 pb-8 mb-8 border-b border-gray-100">
<div className="container mx-auto px-4 max-w-7xl">
<div className="flex flex-col md:flex-row items-center justify-between gap-6">
<div className="text-center md:text-right">
<h1 className="text-3xl md:text-4xl font-black text-gray-800 mb-4 flex items-center justify-center md:justify-start gap-3">
<BookOpen className="text-[#ffb900]" size={32} />
وبلاگ و مقالات آموزشی
</h1>
<p className="text-gray-500 text-sm md:text-base font-medium max-w-2xl">
جدیدترین اخبار، آموزشها، ترفندها و مقالات تخصصی در حوزه صنعت و قطعات را اینجا بخوانید.
</p>
</div>
{/* باکس آمار مقالات */}
<div className="hidden lg:flex items-center gap-4 bg-white p-4 rounded-2xl shadow-sm border border-gray-100">
<div className="bg-[#ffb900] p-3 rounded-xl ">
<FileText size={24} />
</div>
<div>
<p className="text-xs text-gray-400 font-bold mb-1">تعداد کل مقالات</p>
<p className="text-xl font-black text-gray-800">{articles.length} مقاله</p>
</div>
</div>
</div>
</div>
</div>
<div className="container mx-auto px-4 max-w-7xl">
{/* بخش فیلترها و جستجو */}
<div className="flex flex-col lg:flex-row justify-between items-start lg:items-center gap-6 mb-10 bg-white p-4 rounded-3xl shadow-sm border border-gray-100">
{/* فیلتر دسته‌بندی (قابلیت اسکرول افقی در موبایل با مخفی کردن نوار اسکرول) */}
<div className="flex w-full lg:w-auto overflow-x-auto gap-2 pb-2 lg:pb-0 [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]">
{categories.map((category) => (
<button
key={category}
onClick={() => setActiveCategory(category)}
className={`
whitespace-nowrap px-5 py-2.5 rounded-full text-sm font-bold transition-all
${activeCategory === category
? 'bg-[#ffb900] text-white '
: 'bg-gray-50 text-gray-600 hover:bg-gray-100 border border-gray-100'}
`}
>
{category}
</button>
))}
</div>
{/* باکس جستجو */}
<div className="relative w-full lg:w-80 shrink-0">
<Search className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400" size={18} />
<input
type="text"
placeholder="جستجو در مقالات..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-gray-50 border border-gray-100 rounded-full py-3 pr-11 pl-4 text-sm font-medium text-gray-700 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 focus:bg-white transition-all"
/>
</div>
</div>
{/* گرید مقالات */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 md:gap-8">
{filteredArticles.map((article) => (
<div key={article.title} className="transform hover:-translate-y-1 transition-transform duration-300">
<ArticleCard article={article} />
</div>
))}
</div>
{/* حالت خالی (وقتی مقاله‌ای با فیلتر یا جستجو پیدا نشد) */}
{filteredArticles.length === 0 && (
<div className="flex flex-col justify-center items-center py-20 bg-white rounded-3xl border border-gray-100 border-dashed mt-8 text-center px-4">
<div className="bg-gray-50 p-6 rounded-full mb-4">
<Search size={40} className="text-gray-300" />
</div>
<h3 className="text-xl font-bold text-gray-700 mb-2">مقالهای پیدا نشد!</h3>
<p className="text-gray-500 text-sm">
با کلمه جستجو شده یا دستهبندی انتخاب شده، مقالهای وجود ندارد. لطفا فیلترها را تغییر دهید.
</p>
<button
onClick={() => { setActiveCategory('همه'); setSearchQuery(''); }}
className="mt-6 px-6 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 font-semibold rounded-xl transition-colors text-sm"
>
پاک کردن فیلترها
</button>
</div>
)}
</div>
</main>
);
}

217
app/cart/page.tsx Normal file
View File

@@ -0,0 +1,217 @@
'use client';
import { useCart } from "@/components/context/cartcontext";
import Image from "next/image";
import Link from "next/link";
import {
Trash2,
ShoppingBag,
ChevronLeft,
Plus,
Minus,
ShieldCheck,
Truck,
CreditCard
} from "lucide-react";
export default function CartPage() {
const { cart, clearCart, addToCart, decreaseQuantity } = useCart();
// تبدیل رشته قیمت به عدد
const parsePrice = (priceStr?: string | null) => {
if (!priceStr) return 0;
return Number(priceStr.toString().replace(/,/g, ''));
};
// محاسبه قیمت کل و تعداد کل
const totalPrice = cart.reduce((total, item) => total + (parsePrice(item.price) * item.quantity), 0);
const totalItems = cart.reduce((total, item) => total + item.quantity, 0);
// دیزاین حالت سبد خرید خالی
if (cart.length === 0) {
return (
<div className="min-h-screen bg-gray-50/50 flex flex-col items-center justify-center p-6">
<div className="bg-white p-12 rounded-[3rem] shadow-sm border border-gray-100 flex flex-col items-center max-w-md w-full text-center">
<div className="bg-blue-50/50 w-32 h-32 rounded-full flex items-center justify-center mb-8 relative">
<ShoppingBag size={56} className="text-blue-500 relative z-10" />
<div className="absolute inset-0 bg-blue-100 rounded-full animate-ping opacity-20"></div>
</div>
<h2 className="text-2xl font-black text-gray-800 mb-3">سبد خرید شما خالی است!</h2>
<p className="text-gray-500 mb-10 leading-relaxed text-sm">
هنوز هیچ محصولی به سبد خرید خود اضافه نکردهاید. برای مشاهده محصولات به صفحه اصلی برگردید.
</p>
<Link href="/" className="w-full bg-[#ffb900] hover:bg-[#e5a600] text-black font-bold text-lg px-8 py-4 rounded-2xl transition-all shadow-[0_4px_20px_rgba(255,185,0,0.3)] hover:shadow-[0_6px_25px_rgba(255,185,0,0.4)] flex items-center justify-center gap-2">
بازگشت به فروشگاه
<ChevronLeft size={20} />
</Link>
</div>
</div>
);
}
return (
<main className="bg-gray-50/30 min-h-screen pb-20">
<div className="container mx-auto px-4 py-8 max-w-6xl">
{/* بخش هدر و Breadcrumb */}
<div className="mb-10">
<div className="flex items-center justify-between mb-8">
<h1 className="text-2xl md:text-3xl font-black text-gray-800 flex items-center gap-3">
سبد خرید
<span className="bg-[#ffb900]/20 text-[#d99d00] text-sm font-bold py-1 px-3 rounded-xl flex items-center">
{totalItems} کالا
</span>
</h1>
</div>
{/* Breadcrumb (مراحل پرداخت) */}
<div className="flex items-center justify-center w-full max-w-2xl mx-auto">
<div className="flex flex-col items-center w-1/3 relative z-10">
<div className="w-10 h-10 bg-[#ffb900] text-black rounded-full flex items-center justify-center font-bold shadow-md mb-2 ring-4 ring-white">
<ShoppingBag size={18} />
</div>
<span className="text-sm font-bold text-gray-800">سبد خرید</span>
</div>
<div className="h-[2px] w-full bg-gray-200 absolute top-[28%] -z-0">
<div className="h-full bg-[#ffb900] w-1/6 transition-all"></div>
</div>
<div className="flex flex-col items-center w-1/3 relative z-10">
<div className="w-10 h-10 bg-white border-2 border-gray-200 text-gray-400 rounded-full flex items-center justify-center font-bold mb-2">
<Truck size={18} />
</div>
<span className="text-sm font-medium text-gray-400">اطلاعات ارسال</span>
</div>
<div className="flex flex-col items-center w-1/3 relative z-10">
<div className="w-10 h-10 bg-white border-2 border-gray-200 text-gray-400 rounded-full flex items-center justify-center font-bold mb-2">
<CreditCard size={18} />
</div>
<span className="text-sm font-medium text-gray-400">پرداخت</span>
</div>
</div>
</div>
<div className="flex flex-col lg:flex-row gap-6 lg:gap-8">
{/* بخش لیست محصولات */}
<div className="flex-1">
<div className="bg-white rounded-[2rem] p-4 md:p-8 shadow-sm border border-gray-100">
<div className="flex justify-between items-center mb-6 pb-6 border-b border-gray-100">
<h2 className="text-lg font-bold text-gray-800">محصولات انتخاب شده</h2>
<button onClick={clearCart} className="text-sm text-red-500 hover:text-red-700 transition flex items-center gap-1.5 bg-red-50 hover:bg-red-100 px-3 py-1.5 rounded-lg">
<Trash2 size={16} />
حذف همه
</button>
</div>
<div className="flex flex-col gap-6">
{cart.map((item) => {
const itemTotal = parsePrice(item.price) * item.quantity;
return (
<div key={item.id} className="group flex flex-col sm:flex-row gap-4 sm:gap-6 items-start sm:items-center border-b border-gray-50 pb-6 last:border-0 last:pb-0">
{/* تصویر محصول */}
<div className="bg-gray-50/80 p-3 rounded-2xl border border-gray-100 shrink-0 relative group-hover:bg-white transition-colors duration-300 w-full sm:w-auto flex justify-center">
<Image src={item.image} alt={item.title} width={100} height={100} className="object-contain w-24 h-24 drop-shadow-sm" />
</div>
{/* اطلاعات محصول */}
<div className="flex-1 w-full">
<p className="text-xs font-medium text-blue-500 mb-1.5">{item.brand}</p>
<h3 className="text-base font-bold text-gray-800 line-clamp-2 leading-tight mb-3 group-hover:text-blue-600 transition-colors">{item.title}</h3>
{/* ویژگی‌ها (Badges) */}
<div className="flex flex-wrap gap-2 text-xs font-medium text-gray-600">
<span className="bg-gray-100/80 px-2.5 py-1 rounded-md" dir="ltr">L : {item.l}</span>
<span className="bg-gray-100/80 px-2.5 py-1 rounded-md" dir="ltr">D : {item.d}</span>
</div>
</div>
{/* قیمت و کنترلر */}
<div className="flex sm:flex-col justify-between sm:justify-end items-center sm:items-end w-full sm:w-auto gap-4 shrink-0 mt-2 sm:mt-0">
<span className="font-black text-lg text-gray-900">
{item.price ? `${itemTotal.toLocaleString('fa-IR')} ت` : 'استعلام'}
</span>
{/* کنترلر تعداد طرح کپسولی */}
<div className="flex items-center gap-1 bg-gray-50 border border-gray-200 rounded-full p-1 shadow-sm">
<button
onClick={() => addToCart(item)}
className="w-8 h-8 flex items-center justify-center rounded-full bg-white text-gray-600 hover:text-green-600 hover:shadow-sm transition-all"
>
<Plus size={16} strokeWidth={2.5} />
</button>
<span className="text-sm font-bold text-gray-800 w-6 text-center">
{item.quantity}
</span>
<button
onClick={() => decreaseQuantity(item.id)}
className="w-8 h-8 flex items-center justify-center rounded-full bg-white text-gray-600 hover:text-red-500 hover:shadow-sm transition-all"
>
{item.quantity === 1 ? <Trash2 size={16} /> : <Minus size={16} strokeWidth={2.5} />}
</button>
</div>
</div>
</div>
);
})}
</div>
</div>
</div>
{/* بخش صورتحساب (Sidebar) */}
<div className="w-full lg:w-[340px] shrink-0">
<div className="bg-white rounded-[2rem] p-6 md:p-8 shadow-xl shadow-gray-200/20 border border-gray-100 sticky top-6">
<h2 className="text-xl font-bold text-gray-800 mb-6">خلاصه صورتحساب</h2>
<div className="space-y-4 mb-6">
<div className="flex justify-between items-center text-sm">
<span className="text-gray-500">تعداد کالاها</span>
<span className="font-bold text-gray-800">{totalItems} عدد</span>
</div>
<div className="flex justify-between items-center text-sm">
<span className="text-gray-500">هزینه ارسال</span>
<span className="text-blue-600 font-bold bg-blue-50 px-2 py-1 rounded-md text-xs">در مرحله بعد</span>
</div>
</div>
{/* خط‌چین جداکننده */}
<div className="w-full border-t-2 border-dashed border-gray-200 my-6"></div>
<div className="flex justify-between items-center mb-8">
<span className="text-sm font-bold text-gray-600">مبلغ قابل پرداخت</span>
<span className="font-black text-2xl text-[#ffb900] tracking-tight">
{totalPrice > 0 ? `${totalPrice.toLocaleString('fa-IR')}` : 'استعلام'}
{totalPrice > 0 && <span className="text-sm font-medium text-gray-500 mr-1">تومان</span>}
</span>
</div>
<button className="w-full bg-[#ffb900] hover:bg-[#e5a600] text-black py-4 rounded-xl font-bold text-lg transition-all shadow-[0_4px_15px_rgba(255,185,0,0.2)] hover:shadow-[0_6px_20px_rgba(255,185,0,0.3)] flex justify-center items-center gap-2 mb-6">
تایید و ادامه
<ChevronLeft size={20} />
</button>
{/* بج‌های اعتماد (Trust Badges) */}
<div className="flex items-center justify-center gap-4 text-xs font-medium text-gray-400 bg-gray-50 py-3 rounded-xl border border-gray-100">
<div className="flex items-center gap-1">
<ShieldCheck size={16} className="text-green-500" />
پرداخت امن
</div>
<div className="w-1 h-1 bg-gray-300 rounded-full"></div>
<div className="flex items-center gap-1">
<Truck size={16} className="text-blue-500" />
ارسال سریع
</div>
</div>
</div>
</div>
</div>
</div>
</main>
);
}

BIN
app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

21
app/globals.css Normal file
View File

@@ -0,0 +1,21 @@
@import "tailwindcss";
:root {
/* --background: #ffffff; */
/* --foreground: #171717; */
direction: rtl;
}
@media (prefers-color-scheme: dark) {
:root {
/* --background: #0a0a0a; */
--foreground: #ededed;
}
}
body {
/* background: var(--background); */
color: var(--foreground);
font-family: var(--font-site), Arial, Helvetica, sans-serif;
}

43
app/layout.tsx Normal file
View File

@@ -0,0 +1,43 @@
import "./globals.css";
import { Header } from "@/components/header";
import Footer from "@/components/footer";
import localFont from 'next/font/local';
import { CartProvider } from "@/components/context/cartcontext";
const Yekanbakh = localFont({
src: [
{
path: '../public/src/font/YekanBakhFaNum-Regular.woff',
weight: '400',
style: 'normal',
},
{
path: '../public/src/font/YekanBakhFaNum-SemiBold.woff',
weight: '500',
style: 'normal',
},
],
variable: '--font-site',
display: 'swap',
})
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="fa" dir="rtl" className={Yekanbakh.variable}>
<body>
<CartProvider>
<Header/>
{children}
</CartProvider>
<Footer/>
</body>
</html>
)
}

445
app/page.tsx Normal file
View File

@@ -0,0 +1,445 @@
"use client";
import { useState } from "react";
import { Headphones, FileText, Truck, ShieldCheck, Square, MoreVertical, Circle, Target, CookingPot, Plus, Minus, MessageCircleCheckIcon, } from "lucide-react";
import ProductCard from "@/components/productcard";
import ArticleCard from "@/components/articlecard";
import FAQItem from "@/components/faq";
import { products } from "@/lib/data";
import { articles } from "@/lib/data";
const features = [
{
icon: Headphones,
title: "مشاوره فنی رایگان",
desc: "انتخاب بهترین قطعه با توجه به نیاز شما",
},
{
icon: FileText,
title: "فاکتور رسمی",
desc: "صدور فاکتور برای شرکت‌ها",
},
{
icon: Truck,
title: "ارسال فوری",
desc: "ارسال در سریع‌ترین زمان ممکن",
},
{
icon: ShieldCheck,
title: "ضمانت اصالت کالا",
desc: "تضمین اورجینال بودن محصولات",
},
];
const categories = [
{
title: "فیلتر و رگلاتور",
icon: CookingPot,
},
{
title: "باتن",
icon: Square,
},
{
title: "سوئیچ",
icon: MoreVertical,
},
{
title: "شبکه ای",
icon: Circle,
},
{
title: "مدیرفشار",
icon: Target,
},
{
title: "شیر برقی",
icon: Minus,
},
];
const tabs = ["پرفروش‌ترین‌ها", "تخفیف‌دار", "جدیدترین‌ها"];
const faqs = [
{
question: "آیا تمامی قطعات دارای ضمانت اصالت هستند؟",
answer:
"بله، تمامی محصولات ارائه شده دارای ضمانت اصالت کالا بوده و از برندهای معتبر جهانی تأمین می‌شوند.",
},
{
question: "امکان صدور فاکتور رسمی برای شرکت‌ها وجود دارد؟",
answer:
"بله، برای تمامی سفارشات امکان صدور فاکتور رسمی وجود دارد.",
},
{
question: "سفارش‌ها چه زمانی ارسال می‌شوند؟",
answer:
"سفارش‌ها در سریع‌ترین زمان ممکن پردازش شده و از طریق روش‌های ارسال معتبر ارسال می‌شوند.",
},
];
export default function Home() {
const [activeTab, setActiveTab] = useState(0);
const brands = ["NTN", "KOYO", "NACHI", "TIMKEN", "FAG", "SKF"];
const latestArticles = articles.slice(-3);
const featuredProducts = products.slice(0, 10);
const [startIndex, setStartIndex] = useState(0);
const nextSlide = () => {
if (startIndex + 4 < featuredProducts.length) {
setStartIndex(startIndex + 1);
}
};
const prevSlide = () => {
if (startIndex > 0) {
setStartIndex(startIndex - 1);
}
};
return (
<main className=" bg-gray-50">
{/* hero section */}
<section className="bg-[#0b1d36] text-white py-16">
<div className="container mx-auto text-center">
{/* Badge */}
<div className="inline-block border mb-4 px-3 py-2 bg-[#443A27] text-[#ffb900] text-xs rounded-full">
تأمینکننده برتر قطعات
</div>
{/* Heading */}
<h1 className=" leading-snug md:text-5xl font-extrabold mb-4">
تخصصیترین مرجع
<br></br>
بلبرینگ و قطعات صنعتی
</h1>
{/* Subtitle */}
<p className=" text-[15px] text-[#9a9a9a] mb-10">
دسترسی به بیش از 10.000 قطعه با ضمانت اصالت کالا
</p>
</div>
</section>
{/* hero search */}
<div className="w-full px-4">
<div className=" max-w-[70.5rem] mx-auto px-4 bg-white -mt-20 text-gray-800 rounded-2xl shadow-lg p-6 flex flex-col md:flex-row gap-4 md:gap-2 items-center justify-between">
{/* Filters */}
<div className="grid grid-cols-1 md:grid-cols-6 w-full gap-4 justify-between">
<div className="flex flex-col md:col-span-2">
<label htmlFor="partNumber" className="text-xs mb-3 text-black font-bold">
شماره فنی (Part Number)
</label>
<input
id="partNumber"
type="text"
placeholder="Part Number"
className="px-4 py-3 bg-[#f9f9f9] rounded-xl border border-gray-300 text-sm focus:outline-none focus:ring-2 focus:ring-[#e6d3a3]"
/>
</div>
<div className="flex flex-col">
<label htmlFor="innerDiameter" className="text-xs mb-3 text-black font-bold">
قطر داخل
</label>
<input
id="innerDiameter"
type="text"
placeholder="mm"
className="px-4 bg-[#f9f9f9] py-3 rounded-xl border border-gray-300 text-sm focus:outline-none focus:ring-2 focus:ring-[#e6d3a3]"
/>
</div>
<div className="flex flex-col">
<label htmlFor="outerDiameter" className="text-xs mb-3 text-black font-bold">
قطر خارج
</label>
<input
id="outerDiameter"
type="text"
placeholder="mm"
className="px-4 bg-[#f9f9f9] py-3 rounded-xl border border-gray-300 text-sm focus:outline-none focus:ring-2 focus:ring-[#e6d3a3]"
/>
</div>
<div className="flex flex-col">
<label htmlFor="thickness" className="text-xs mb-3 text-black font-bold">
ضخامت
</label>
<input
id="thickness"
type="text"
placeholder="mm"
className="px-4 bg-[#f9f9f9] py-3 rounded-xl border border-gray-300 text-sm focus:outline-none focus:ring-2 focus:ring-[#e6d3a3]"
/>
</div>
{/* Button */}
<button
className="mt-6 rounded-xl border-2 md:w-auto md:h-auto w-[100px] h-[48px] border-[#e6d3a3] bg-[#ffb900] text-black font-semibold text-xs transition-all hover:bg-[#da9800]">
جستجوی قطعه
</button>
</div>
</div>
</div>
{/* feature cards */}
<div className="w-full py-6 mt-12">
<div className="max-w-6xl px-4 mx-auto grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{features.map((item, index) => {
const Icon = item.icon;
return (
<div
key={index}
className="flex items-center gap-4 px-4 py-7 border bg-white shadow-sm hover:shadow-md transition-shadow duration-300 border-[#e3e3e3] rounded-xl "
>
<div className="p-3 bg-gray-100 rounded-lg">
<Icon size={24} className="text-gray-700" />
</div>
<div>
<h3 className="font-semibold text-gray-800 text-sm">
{item.title}
</h3>
<p className="text-xs text-gray-500">{item.desc}</p>
</div>
</div>
);
})}
</div>
</div>
{/* categories card */}
<section className="w-full py-12 bg-gray-50">
<div className="max-w-6xl mx-auto px-4">
{/* title */}
<div className="w-full mb-10">
<div className="flex">
<h2 className="text-xl font-bold text-gray-800">
دسته بندی های بلبرینگ
</h2>
</div>
{/* line */}
<div className="relative mt-3 h-[2px] bg-gray-200 w-full">
<div className="absolute right-0 top-0 h-[2px] w-24 bg-yellow-500"></div>
</div>
</div>
{/* cards */}
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-6">
{categories.map((cat, index) => {
const Icon = cat.icon;
return (
<div
key={index}
className="bg-white border border-gray-200 rounded-xl shadow-sm hover:shadow-md transition-all duration-300 flex flex-col items-center justify-center py-8 cursor-pointer"
>
<Icon size={34} className="text-gray-400 mb-3" />
<p className="text-sm font-medium text-gray-700">
{cat.title}
</p>
</div>
);
})}
</div>
</div>
</section>
{/* products */}
<section className="py-12">
<div className="max-w-6xl mx-auto px-4">
<div className="items-center justify-between mb-8">
{/* title */}
<div className="flex flex-wrap md:flex-nowrap justify-between w-full mb-4">
<div className="flex">
<h3 className="text-xl font-bold text-gray-800">
پیشنهاد ویژه صنعتی
</h3>
</div>
{/* tabs */}
<div className="flex mt-6 mb-4 md:mt-0 md:mb-0 gap-8 text-sm">
{tabs.map((tab, index) => (
<button
key={index}
onClick={() => setActiveTab(index)}
className={`pb-2 cursor-pointer border-b-2 transition
${activeTab === index
? "border-yellow-500 text-black"
: "border-transparent text-gray-400"
}`}
>
{tab}
</button>
))}
</div>
</div>
{/* line */}
<div className="relative mt-3 h-[2px] bg-gray-200 w-full">
<div className="absolute right-0 top-0 h-[2px] w-24 bg-yellow-500"></div>
</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} />
))}
</div>
</div>
</section>
{/* blog */}
<section className="py-16 bg-white">
<div className="max-w-6xl mx-auto px-4">
{/* header */}
<div className="mb-8">
<div className="flex justify-between w-full mb-4">
<h3 className="text-xl font-bold text-gray-800">
مجله فنی و مهندسی
</h3>
<a className="text-[#ffb900] text-sm cursor-pointer">
مشاهده آرشیو
</a>
</div>
<div className="relative h-[2px] bg-gray-200 w-full">
<div className="absolute right-0 top-0 h-[2px] w-24 bg-yellow-500" />
</div>
</div>
{/* grid */}
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{latestArticles.map((article, i) => (
<ArticleCard key={i} article={article} />
))}
</div>
</div>
</section>
{/* search */}
<section className="py-16">
<div className="max-w-6xl mx-auto px-4">
<div className="bg-[#0b1e3b] rounded-2xl p-10 flex flex-col lg:flex-row items-center justify-between gap-8 relative overflow-hidden">
{/* right content */}
<div className="text-right text-white max-w-md">
<h2 className="text-2xl font-bold text-amber-400 mb-3">
قطعه خاصی مد نظرتان است؟
</h2>
<p className="text-sm text-gray-300 leading-7">
تیم فنی ما آماده است تا قطعات صنعتی مورد نیاز شما را در سریعترین زمان
ممکن تأمین کند. مشخصات قطعه را وارد کنید.
</p>
<div className="flex items-center gap-2 mt-3 text-sm text-gray-300">
<MessageCircleCheckIcon size={16} />
پاسخگویی سریع در واتساپ
</div>
</div>
{/* search box */}
<div className="bg-[#2a3548] p-4 flex-wrap rounded-xl flex items-center gap-3 w-full max-w-xl">
<input
type="text"
placeholder="شماره تماس"
className="flex-1 bg-gray-200 rounded-lg md:px-4 px-0 py-3 outline-none text-xs"
/>
<input
type="text"
placeholder=" کد فنی قطعه"
className="flex-1 bg-gray-200 rounded-lg px-0 md:px-4 py-3 outline-none text-xs"
/>
<button className="bg-amber-500 hover:bg-amber-600 text-black px-3 py-3 rounded-lg flex items-center gap-2 text-xs cursor-pointer">
ثبت درخواست
</button>
</div>
</div>
</div>
</section>
{/* faq */}
<section className="py-6">
<div className="max-w-3xl mx-auto px-4">
<h2 className="text-center text-xl font-bold mb-10">
سوالات پرتکرار مشتریان
</h2>
{/* faq list */}
<div className="space-y-4">
{faqs.map((faq, index) => (
<FAQItem
key={index}
question={faq.question}
answer={faq.answer}
/>
))}
</div>
</div>
</section>
{/* brands */}
<section className="w-full py-20 px-6">
<div className="max-w-6xl mx-auto text-center">
<h2 className="text-gray-500 text-sm font-semibold mb-10">
تامین کننده برندهای معتبر جهان
</h2>
<div className="flex flex-wrap justify-center items-center gap-8">
{brands.map((brand) => (
<div
key={brand}
className="w-40 h-16 flex items-center justify-center border border-gray-200 rounded-md bg-white"
>
<span className="text-gray-300 font-semibold tracking-wider text-lg">
{brand}
</span>
</div>
))}
</div>
</div>
</section>
</main>
);
}

View File

@@ -0,0 +1,79 @@
import ScrollToTop from '@/components/scrolltop';
import { products } from '@/lib/data';
import { notFound } from 'next/navigation';
import Image from "next/image";
// تعریف دقیق پارامترهایی که از URL می‌آیند
interface PageProps {
params: Promise<{
id: string;
slug: string;
}>;
}
export default async function SingleProductPage({ params }: PageProps) {
const resolvedParams = await params;
const product = products.find((p) => p.id.toString() === resolvedParams.id);
// ۳. اگر محصول با این ID در دیتابیس نبود
if (!product) {
notFound();
}
return (
<div className="container mx-auto p-8 max-w-4xl">
<div className="bg-white rounded-xl shadow-lg p-6 flex flex-col md:flex-row gap-8 border border-gray-100">
<ScrollToTop />
<div className="w-full md:w-1/3 flex justify-center bg-gray-50 rounded-lg p-4">
<Image
src={product.image}
width={300}
height={300}
alt={product.title}
className="object-contain"
/>
</div>
<div className="w-full md:w-2/3 flex flex-col justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-800 mb-2">{product.title}</h1>
<p className="text-gray-500 font-semibold mb-6">برند: {product.brand}</p>
<div className="space-y-3 bg-gray-50 p-4 rounded-lg">
<div className="flex justify-between border-b pb-2">
<span className="text-gray-600">قطر داخلی (L):</span>
<span className="font-bold" dir="ltr">{product.l}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">قطر خارجی (D):</span>
<span className="font-bold" dir="ltr">{product.d}</span>
</div>
</div>
</div>
<div className="mt-8 flex justify-between items-center border-t pt-4">
<span className={`px-4 py-2 rounded-full font-bold text-sm ${product.stock ? "bg-green-100 text-green-800" : "bg-red-100 text-red-800"}`}>
{product.stock ? "موجود در انبار" : "ناموجود"}
</span>
{product.price ? (
<span className="text-2xl font-bold text-blue-600">{product.price} تومان</span>
) : (
<span className="text-xl font-bold text-gray-700 flex items-center gap-2">
نیاز به استعلام
</span>
)}
</div>
</div>
</div>
</div>
);
}

17
app/products/page.tsx Normal file
View File

@@ -0,0 +1,17 @@
// app/products/page.tsx
import ProductCard from '@/components/productcard'; // مسیر را با توجه به ساختار خود اصلاح کنید
import { products } from '@/lib/data'; // ایمپورت دیتای مرکزی
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} />
))}
</div>
</div>
);
}

67
app/search/page.tsx Normal file
View File

@@ -0,0 +1,67 @@
'use client';
import { useSearchParams } from 'next/navigation';
import { Suspense } from 'react';
import { products } from '@/lib/data';
import { Search } from 'lucide-react';
import ProductCard from '@/components/productcard';
// کامپوننت داخلی برای هندل کردن جستجو
function SearchResults() {
const searchParams = useSearchParams();
const query = searchParams.get('q') || '';
// فیلتر کردن محصولات بر اساس جستجو
const results = products.filter(p =>
p.title.toLowerCase().includes(query.toLowerCase()) ||
p.brand?.toLowerCase().includes(query.toLowerCase()) ||
p.d?.toLowerCase().includes(query.toLowerCase()) ||
p.l?.toLowerCase().includes(query.toLowerCase())
);
return (
<>
<div className="flex items-center gap-3 mb-8 border-b border-gray-100 pb-6">
<div className="bg-blue-50 p-3 rounded-2xl text-blue-600">
<Search size={28} />
</div>
<div>
<h1 className="text-2xl font-bold text-gray-800">نتایج جستجو برای: "{query}"</h1>
<p className="text-sm text-gray-500 mt-1">{results.length} مورد پیدا شد</p>
</div>
</div>
{results.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{results.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
) : (
<div className="text-center py-20 bg-gray-50 rounded-3xl">
<p className="text-gray-500 text-lg">متاسفیم، کالایی با این مشخصات پیدا نشد.</p>
</div>
)}
</>
);
}
// کامپوننت اصلی صفحه که Suspense را پیاده‌سازی می‌کند
export default function SearchPage() {
return (
<div className="container mx-auto px-4 py-12 min-h-screen">
{/*
استفاده از Suspense برای جلوگیری از خطای Deopt در Next.js
هنگام استفاده از useSearchParams
*/}
<Suspense fallback={
<div className="flex justify-center items-center py-20 text-gray-500">
در حال جستجو...
</div>
}>
<SearchResults />
</Suspense>
</div>
);
}