fix bugs
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 122 KiB |
183
src/app/about/page.tsx
Normal file
183
src/app/about/page.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { CalendarDays, ShoppingBag, Sparkles, ShieldCheck, Clock, HeartHandshake } from "lucide-react";
|
||||
|
||||
export const metadata = {
|
||||
title: "درباره سالونا | رزرو آرایشگاه و فروشگاه آرایشی",
|
||||
description:
|
||||
"سالونا، همراه شما در مسیر زیبایی. پلتفرم جامع رزرو آنلاین نوبت آرایشگاههای زنانه و فروشگاه تخصصی محصولات آرایشی و بهداشتی.",
|
||||
};
|
||||
|
||||
export default function AboutPage() {
|
||||
return (
|
||||
// تخصیص فونت وزیرمتن و راستچین کردن کل صفحه
|
||||
<main className="min-h-screen bg-background font-vazir" dir="rtl">
|
||||
{/* بخش هیرو (Hero Section) */}
|
||||
<section className="container mx-auto px-4 py-16 lg:py-24 animate-fade-in">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
|
||||
<div className="space-y-6">
|
||||
<div className="inline-block px-4 py-1.5 bg-salona-100 text-salona-700 rounded-full text-sm font-semibold mb-2">
|
||||
داستان سالونا
|
||||
</div>
|
||||
<h1 className="text-4xl lg:text-5xl font-extrabold text-foreground font-iransans leading-tight">
|
||||
تلاقی <span className="text-salona-500">هنر زیبایی</span> و <br />
|
||||
تکنولوژی مدرن
|
||||
</h1>
|
||||
<p className="text-gray-600 leading-relaxed text-lg text-justify">
|
||||
سالونا تنها یک وبسایت نیست؛ یک همراه همیشگی برای بانوانی است که به زمان و زیبایی خود اهمیت
|
||||
میدهند. ما با درک دغدغههای روزمره شما، پلتفرمی را خلق کردهایم که نهتنها فرآیند رزرو نوبت
|
||||
در بهترین آرایشگاههای زنانه را بیدردسر میکند، بلکه دسترسی به مرغوبترین محصولات آرایشی و
|
||||
بهداشتی را در یک مکان فراهم آورده است.
|
||||
</p>
|
||||
<div className="flex gap-4 pt-4">
|
||||
<Link
|
||||
href="/salons"
|
||||
className="bg-salona-500 hover:bg-salona-600 text-white px-6 py-3 rounded-xl font-medium transition-colors shadow-lg shadow-salona-500/30 flex items-center gap-2"
|
||||
>
|
||||
<CalendarDays className="w-5 h-5 ms-2" />
|
||||
رزرو نوبت
|
||||
</Link>
|
||||
<Link
|
||||
href="/shop"
|
||||
className="bg-salona-50 hover:bg-salona-100 text-salona-600 border border-salona-200 px-6 py-3 rounded-xl font-medium transition-colors flex items-center gap-2"
|
||||
>
|
||||
<ShoppingBag className="w-5 h-5 ms-2" />
|
||||
فروشگاه محصولات
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative h-[400px] lg:h-[500px] rounded-3xl overflow-hidden shadow-2xl">
|
||||
{/* در پروژه واقعی از عکس با کیفیت خودتان استفاده کنید */}
|
||||
<Image
|
||||
src="https://images.unsplash.com/photo-1560066984-138dadb4c035?q=80&w=1974&auto=format&fit=crop"
|
||||
alt="سالن زیبایی سالونا"
|
||||
fill
|
||||
className="object-cover"
|
||||
priority
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent"></div>
|
||||
<div className="absolute bottom-6 right-6 text-white">
|
||||
<p className="text-2xl font-bold font-iransans">سالونا</p>
|
||||
<p className="text-sm opacity-90">تجربه متفاوت زیبایی</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* بخش خدمات اصلی (Core Services) */}
|
||||
<section className="bg-salona-50 py-20">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="text-center max-w-2xl mx-auto mb-16">
|
||||
<h2 className="text-3xl font-bold text-gray-900 font-iransans mb-4">
|
||||
ما چه کاری انجام میدهیم؟
|
||||
</h2>
|
||||
<p className="text-gray-600">
|
||||
سالونا دو بال قدرتمند برای پرواز به سوی زیبایی دارد. ما خدمات حضوری و محصولات خانگی را به
|
||||
یکدیگر پیوند دادهایم.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-4xl mx-auto">
|
||||
{/* کارت رزرو */}
|
||||
<div className="bg-white p-8 rounded-3xl shadow-sm hover:shadow-md transition-shadow border border-salona-100">
|
||||
<div className="w-14 h-14 bg-salona-100 text-salona-500 rounded-2xl flex items-center justify-center mb-6">
|
||||
<CalendarDays className="w-7 h-7" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold mb-3 font-iransans">مدیریت نوبتدهی آرایشگاهها</h3>
|
||||
<p className="text-gray-600 leading-relaxed text-sm">
|
||||
دیگر نیازی به تماسهای مکرر و هماهنگیهای زمانبر نیست. با سالونا میتوانید تقویم کاری
|
||||
بهترین سالنهای زیبایی را مشاهده کنید، نمونه کارها را ببینید و در ساعت دلخواه خود،
|
||||
نوبتتان را قطعی کنید.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* کارت فروشگاه */}
|
||||
<div className="bg-white p-8 rounded-3xl shadow-sm hover:shadow-md transition-shadow border border-salona-100">
|
||||
<div className="w-14 h-14 bg-salona-100 text-salona-500 rounded-2xl flex items-center justify-center mb-6">
|
||||
<ShoppingBag className="w-7 h-7" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold mb-3 font-iransans">فروشگاه تخصصی محصولات</h3>
|
||||
<p className="text-gray-600 leading-relaxed text-sm">
|
||||
مراقبت از زیبایی پس از خروج از سالن متوقف نمیشود. ما مجموعهای از بهترین و معتبرترین
|
||||
برندهای آرایشی، مراقبت از پوست و مو را با ضمانت اصالت کالا برای شما گردآوری کردهایم.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* بخش ارزشهای کلیدی (Core Values) */}
|
||||
<section className="container mx-auto px-4 py-20">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl font-bold text-gray-900 font-iransans mb-4">چرا سالونا را انتخاب کنید؟</h2>
|
||||
<div className="w-24 h-1 bg-salona-300 mx-auto rounded-full"></div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
{[
|
||||
{
|
||||
icon: <Clock className="w-6 h-6" />,
|
||||
title: "صرفهجویی در زمان",
|
||||
desc: "رزرو سریع در کمتر از ۲ دقیقه و ارسال فوری محصولات به سراسر کشور.",
|
||||
},
|
||||
{
|
||||
icon: <ShieldCheck className="w-6 h-6" />,
|
||||
title: "تضمین اصالت",
|
||||
desc: "تمامی محصولات فروشگاه و خدمات سالنها مورد تایید و دارای ضمانت هستند.",
|
||||
},
|
||||
{
|
||||
icon: <Sparkles className="w-6 h-6" />,
|
||||
title: "تنوع بینظیر",
|
||||
desc: "دسترسی به صدها سالن زیبایی و هزاران محصول آرایشی در یک پلتفرم.",
|
||||
},
|
||||
{
|
||||
icon: <HeartHandshake className="w-6 h-6" />,
|
||||
title: "پشتیبانی دلسوزانه",
|
||||
desc: "تیم پشتیبانی سالونا در تمام مراحل رزرو و خرید در کنار شماست.",
|
||||
},
|
||||
].map((item, index) => (
|
||||
<div key={index} className="text-center group">
|
||||
<div className="w-16 h-16 mx-auto bg-gray-50 group-hover:bg-salona-500 group-hover:text-white text-salona-500 rounded-full flex items-center justify-center mb-4 transition-colors duration-300 shadow-sm">
|
||||
{item.icon}
|
||||
</div>
|
||||
<h4 className="text-lg font-bold mb-2 font-iransans text-gray-800">{item.title}</h4>
|
||||
<p className="text-gray-500 text-sm leading-relaxed">{item.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* بخش دعوت به اقدام (Call to Action) */}
|
||||
<section className="container mx-auto px-4 pb-20">
|
||||
<div className="bg-gradient-to-r from-salona-600 to-salona-400 rounded-3xl p-10 md:p-16 text-center text-white relative overflow-hidden shadow-xl shadow-salona-500/20">
|
||||
{/* افکتهای پسزمینه تزئینی */}
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-white opacity-5 rounded-full blur-3xl transform translate-x-1/2 -translate-y-1/2"></div>
|
||||
<div className="absolute bottom-0 left-0 w-64 h-64 bg-black opacity-10 rounded-full blur-3xl transform -translate-x-1/2 translate-y-1/2"></div>
|
||||
|
||||
<h2 className="text-3xl md:text-4xl font-bold font-iransans mb-6 relative z-10 text-shadow-gray">
|
||||
آمادهاید تا سبک جدیدی از زیبایی را تجربه کنید؟
|
||||
</h2>
|
||||
<p className="text-salona-50 mb-10 max-w-2xl mx-auto text-lg relative z-10">
|
||||
همین حالا به جمع هزاران کاربر سالونا بپیوندید. نوبت خود را رزرو کنید یا سبد خریدتان را از
|
||||
محصولات شگفتانگیز پر کنید.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center relative z-10">
|
||||
<Link
|
||||
href="/register"
|
||||
className="bg-white text-salona-600 px-8 py-3 rounded-xl font-bold hover:bg-salona-50 transition-colors shadow-lg"
|
||||
>
|
||||
عضویت در سالونا
|
||||
</Link>
|
||||
<Link
|
||||
href="/contact"
|
||||
className="bg-transparent border border-white/50 text-white px-8 py-3 rounded-xl font-bold hover:bg-white/10 transition-colors"
|
||||
>
|
||||
تماس با ما
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
157
src/app/categories/[id]/page.tsx
Normal file
157
src/app/categories/[id]/page.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
"use client";
|
||||
|
||||
import { use } from "react";
|
||||
import Link from "next/link";
|
||||
import { ChevronLeft, SlidersHorizontal, PackageX } from "lucide-react";
|
||||
import { useCategories } from "@/services/useCategories";
|
||||
import { useProducts } from "@/services/useProducts";
|
||||
import { useFavorites } from "@/services/useFavorites";
|
||||
import { ProductCard } from "@/components/shared/ProductsCard";
|
||||
import { Category, Product } from "@/types/api";
|
||||
|
||||
// تابع کمکی برای پیدا کردن یک دسته خاص در ساختار درختی
|
||||
const findCategoryById = (categories: Category[], id: number): Category | null => {
|
||||
for (const cat of categories) {
|
||||
if (cat.id === id) return cat;
|
||||
if (cat.children && cat.children.length > 0) {
|
||||
const found = findCategoryById(cat.children, id);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export default function SingleCategoryPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
// باز کردن params در Next.js 15+ (اگر نسخه قدیمیتر است Promise نیاز نیست)
|
||||
const resolvedParams = use(params);
|
||||
const categoryId = parseInt(resolvedParams.id);
|
||||
|
||||
const user = typeof window !== "undefined" ? JSON.parse(localStorage.getItem("user") || "null") : null;
|
||||
|
||||
// دریافت کل دستهها برای پیدا کردن دسته فعلی
|
||||
const { data: categoriesData, isLoading: isCatLoading } = useCategories();
|
||||
const { data: favoritesData } = useFavorites(!!user);
|
||||
|
||||
// دریافت محصولات مختص این دسته
|
||||
const { data: productsData, isLoading: isProdLoading } = useProducts({
|
||||
category_id: categoryId,
|
||||
per_page: 20,
|
||||
});
|
||||
|
||||
const currentCategory = categoriesData?.categories ? findCategoryById(categoriesData.categories, categoryId) : null;
|
||||
|
||||
if (isCatLoading) {
|
||||
return <div className="container py-20 text-center text-salona-500 animate-pulse">در حال بارگذاری...</div>;
|
||||
}
|
||||
|
||||
if (!currentCategory && !isCatLoading) {
|
||||
return <div className="container py-20 text-center text-gray-500">دستهبندی مورد نظر یافت نشد.</div>;
|
||||
}
|
||||
|
||||
// جدا کردن مسیرها برای Breadcrumb (مثلا "محصولات مراقبتی > پوست" تبدیل به آرایه میشود)
|
||||
const breadcrumbs = currentCategory?.full_path.split(" > ") || [];
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-background font-vazir pb-20" dir="rtl">
|
||||
{/* بخش هدر دستهبندی */}
|
||||
<div className="bg-salona-50/50 border-b border-salona-100 py-8 mb-10">
|
||||
<div className="container">
|
||||
{/* مسیر راهنما (Breadcrumbs) */}
|
||||
<nav className="flex items-center gap-2 text-sm text-gray-500 mb-4 overflow-x-auto whitespace-nowrap scrollbar-hide">
|
||||
<Link href="/" className="hover:text-salona-500 transition-colors">
|
||||
خانه
|
||||
</Link>
|
||||
<ChevronLeft className="w-3 h-3" />
|
||||
<Link href="/categories" className="hover:text-salona-500 transition-colors">
|
||||
دستهبندیها
|
||||
</Link>
|
||||
{breadcrumbs.map((crumb, index) => (
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
<ChevronLeft className="w-3 h-3" />
|
||||
<span className={index === breadcrumbs.length - 1 ? "text-salona-600 font-bold" : ""}>
|
||||
{crumb}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<h1 className="text-3xl md:text-4xl font-bold text-gray-900 font-iransans mb-6">
|
||||
{currentCategory?.name}
|
||||
</h1>
|
||||
|
||||
{/* نمایش زیردستهها در صورت وجود */}
|
||||
{currentCategory?.children && currentCategory.children.length > 0 && (
|
||||
<div className="flex gap-3 overflow-x-auto pb-2 scrollbar-hide">
|
||||
{currentCategory.children.map((child: Category) => (
|
||||
<Link
|
||||
key={child.id}
|
||||
href={`/categories/${child.id}`}
|
||||
className="flex-shrink-0 bg-white border border-gray-200 text-gray-700 px-5 py-2.5 rounded-full text-sm font-medium hover:border-salona-400 hover:text-salona-600 hover:shadow-md transition-all"
|
||||
>
|
||||
{child.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* بخش محصولات */}
|
||||
<div className="container">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<p className="text-gray-600">
|
||||
{isProdLoading ? "در حال جستجو..." : `${productsData?.products?.length || 0} کالا یافت شد`}
|
||||
</p>
|
||||
<button className="flex items-center gap-2 text-gray-600 bg-gray-50 px-4 py-2 rounded-xl hover:bg-gray-100 transition-colors">
|
||||
<SlidersHorizontal className="w-4 h-4" />
|
||||
<span className="text-sm font-medium">مرتبسازی</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isProdLoading ? (
|
||||
// اسکلتون لودینگ محصولات
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="bg-white rounded-2xl border border-gray-100 h-80 animate-pulse"
|
||||
></div>
|
||||
))}
|
||||
</div>
|
||||
) : productsData?.products?.length > 0 ? (
|
||||
// گرید محصولات
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6 animate-fade-in">
|
||||
{productsData.products.map((product: Product) => {
|
||||
const favoriteRecord = favoritesData?.favorites?.find(
|
||||
(fav) => fav.product_id === product.id,
|
||||
);
|
||||
return (
|
||||
<ProductCard
|
||||
key={product.id}
|
||||
product={product}
|
||||
isFavorited={!!favoriteRecord}
|
||||
favoriteId={favoriteRecord?.id}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
// حالت خالی (Empty State)
|
||||
<div className="flex flex-col items-center justify-center py-20 bg-gray-50 rounded-3xl border border-dashed border-gray-200">
|
||||
<div className="w-20 h-20 bg-salona-100 text-salona-400 rounded-full flex items-center justify-center mb-4">
|
||||
<PackageX className="w-10 h-10" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-gray-800 mb-2">محصولی یافت نشد!</h3>
|
||||
<p className="text-gray-500">در حال حاضر محصولی در این دستهبندی وجود ندارد.</p>
|
||||
<Link
|
||||
href="/categories"
|
||||
className="mt-6 text-salona-500 font-medium hover:text-salona-600 underline underline-offset-4"
|
||||
>
|
||||
بازگشت به همه دستهبندیها
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
84
src/app/categories/page.tsx
Normal file
84
src/app/categories/page.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { Layers, ChevronLeft, Sparkles } from "lucide-react";
|
||||
import { useCategories } from "@/services/useCategories";
|
||||
import { Category } from "@/types/api";
|
||||
|
||||
export default function AllCategoriesPage() {
|
||||
const { data, isLoading } = useCategories();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="container py-12 space-y-4 animate-pulse" dir="rtl">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="h-24 bg-gray-100 rounded-2xl w-full"></div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// جدا کردن دستههای سطح صفر (اصلی)
|
||||
const rootCategories = data?.categories?.filter((cat: Category) => cat.level === 0) || [];
|
||||
|
||||
return (
|
||||
<main className="container py-12 font-vazir min-h-screen" dir="rtl">
|
||||
<div className="flex items-center gap-3 mb-10">
|
||||
<div className="p-3 bg-salona-100 text-salona-500 rounded-2xl">
|
||||
<Layers className="w-8 h-8" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-black text-gray-900 font-iransans">دستهبندی محصولات</h1>
|
||||
<p className="text-gray-500 mt-1">
|
||||
کشف زیبایی در میان {data?.total_categories} دستهبندی متنوع سالونا
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 animate-fade-in">
|
||||
{rootCategories.map((rootCategory: Category) => (
|
||||
<div
|
||||
key={rootCategory.id}
|
||||
className="bg-white border border-gray-100 rounded-3xl p-6 shadow-sm hover:shadow-xl hover:shadow-salona-50/50 transition-all duration-300"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-6 pb-4 border-b border-gray-50">
|
||||
<h2 className="text-xl font-bold text-gray-800 flex items-center gap-2">
|
||||
<Sparkles className="w-5 h-5 text-salona-400" />
|
||||
{rootCategory.name}
|
||||
</h2>
|
||||
<Link
|
||||
href={`/categories/${rootCategory.id}`}
|
||||
className="text-sm bg-salona-50 text-salona-600 px-4 py-1.5 rounded-full font-medium hover:bg-salona-500 hover:text-white transition-colors"
|
||||
>
|
||||
مشاهده همه
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* نمایش زیردستهها (فرزندان) */}
|
||||
{rootCategory.children && rootCategory.children.length > 0 ? (
|
||||
<ul className="space-y-3">
|
||||
{rootCategory.children.map((child: Category) => (
|
||||
<li key={child.id}>
|
||||
<Link
|
||||
href={`/categories/${child.id}`}
|
||||
className="group flex items-center justify-between p-3 rounded-xl hover:bg-salona-50 transition-colors"
|
||||
>
|
||||
<span className="text-gray-600 group-hover:text-salona-700 font-medium">
|
||||
{child.name}
|
||||
</span>
|
||||
<ChevronLeft className="w-4 h-4 text-gray-400 group-hover:text-salona-500 transition-transform group-hover:-translate-x-1" />
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<div className="text-gray-400 text-sm text-center py-4 bg-gray-50 rounded-xl">
|
||||
بدون زیردسته
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -110,7 +110,7 @@ export default function HomePage() {
|
||||
className="text-sm font-medium text-salona-500 hover:text-salona-600 flex items-center gap-1"
|
||||
>
|
||||
مشاهده همه
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
<ArrowLeft className="w-4 h-4 mt-1" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -42,10 +42,7 @@ export default function AuthForm() {
|
||||
const { mutate: registerUser, isPending: isRegisterPending } = useRegister();
|
||||
const { mutate: verifyOtp, isPending: isVerifyPending } = useVerifyLogin();
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
} = useForm<PhoneFormValues>();
|
||||
const { register, handleSubmit } = useForm<PhoneFormValues>();
|
||||
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout;
|
||||
@@ -115,7 +112,7 @@ export default function AuthForm() {
|
||||
if (!token) throw new Error("توکنی از سمت سرور دریافت نشد");
|
||||
|
||||
await setAuthCookie(token);
|
||||
localStorage.setItem('user',JSON.stringify(data))
|
||||
localStorage.setItem("user", JSON.stringify(data));
|
||||
|
||||
toast.success(
|
||||
mode === "REGISTER"
|
||||
@@ -124,7 +121,7 @@ export default function AuthForm() {
|
||||
);
|
||||
|
||||
router.push("/");
|
||||
router.refresh();
|
||||
router.refresh();
|
||||
} catch (err) {
|
||||
toast.error("خطا در ذخیرهسازی اطلاعات ورود");
|
||||
console.error(err);
|
||||
@@ -276,7 +273,9 @@ export default function AuthForm() {
|
||||
value={otpCode}
|
||||
onChange={setOtpCode}
|
||||
render={({ slots }) => (
|
||||
<div className="flex gap-2 sm:gap-3 justify-center"> {/* اصلاح فاصله برای جاگیری بهتر ۶ رقم */}
|
||||
<div className="flex gap-2 sm:gap-3 justify-center">
|
||||
{" "}
|
||||
{/* اصلاح فاصله برای جاگیری بهتر ۶ رقم */}
|
||||
{slots.map((slot, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
|
||||
@@ -3,29 +3,35 @@ import { useState, useEffect, useRef } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useSelector } from "react-redux";
|
||||
import {
|
||||
ShoppingCart,
|
||||
Heart,
|
||||
User,
|
||||
Menu,
|
||||
X,
|
||||
ChevronDown,
|
||||
MapPin,
|
||||
LogOut,
|
||||
import {
|
||||
ShoppingCart,
|
||||
Heart,
|
||||
User,
|
||||
Menu,
|
||||
X,
|
||||
ChevronDown,
|
||||
MapPin,
|
||||
LogOut,
|
||||
MessageSquare,
|
||||
Package,
|
||||
LayoutGrid,
|
||||
Info,
|
||||
Home
|
||||
Home,
|
||||
Camera,
|
||||
Trash2,
|
||||
UploadCloud,
|
||||
Loader2,
|
||||
} from "lucide-react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import Swal from "sweetalert2";
|
||||
import { useUserProfile } from "@/services/useUser";
|
||||
import { removeAuthCookie } from "@/app/actions";
|
||||
import Image from "next/image";
|
||||
import LOGO from "@/../public/assets/images/logo/logo.png"
|
||||
import LOGO from "@/../public/assets/images/logo/logo.png";
|
||||
|
||||
// توابع آپلود و حذف عکس که ایجاد کردهاید را از مسیر صحیح ایمپورت کنید
|
||||
import { useUploadProfilePicture, useDeleteProfilePicture } from "@/services/useUser";
|
||||
|
||||
// تعریف اینترفیس برای آدرس کاربر
|
||||
interface UserAddress {
|
||||
id: number;
|
||||
address: string;
|
||||
@@ -35,7 +41,6 @@ interface UserAddress {
|
||||
postal_code: string;
|
||||
}
|
||||
|
||||
// تعریف اینترفیس برای ساختار دیتای کاربر بر اساس API
|
||||
interface UserProfile {
|
||||
id: number;
|
||||
user_id: string;
|
||||
@@ -47,6 +52,7 @@ interface UserProfile {
|
||||
favorites_count: number;
|
||||
orders_count: number;
|
||||
wallet_balance: number;
|
||||
profile_picture?: string | null; // اضافه کردن فیلد عکس پروفایل
|
||||
}
|
||||
|
||||
export const Header = () => {
|
||||
@@ -60,10 +66,13 @@ export const Header = () => {
|
||||
const [isProfileDropdownOpen, setIsProfileDropdownOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// استیت و رفرنس برای مودال عکس پروفایل
|
||||
const [isProfilePicModalOpen, setIsProfilePicModalOpen] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// استیت برای حل مشکل Hydration سبد خرید
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
|
||||
// دریافت اطلاعات کاربر از لوکال استوریج به صورت امن
|
||||
const getUserFromStorage = () => {
|
||||
if (typeof window !== "undefined") {
|
||||
const storedUser = localStorage.getItem("user");
|
||||
@@ -71,32 +80,36 @@ export const Header = () => {
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
const user = getUserFromStorage();
|
||||
|
||||
// دریافت اطلاعات کاربر از هوک اختصاصی
|
||||
const { data, isLoading: isUserLoading } = useUserProfile(!!user);
|
||||
|
||||
//@ts-ignore
|
||||
const userData: UserProfile | null = data?.user || null;
|
||||
|
||||
// دریافت لیست محصولات سبد خرید از Redux
|
||||
const cartItems = useSelector((state: any) => state.cart.items);
|
||||
const uniqueProductCount = cartItems?.length || 0;
|
||||
|
||||
// فراخوانی هوکهای عکس پروفایل (خطاهای احتمالی مسیر ایمپورت را برطرف کنید)
|
||||
// در صورتی که ایمپورت بالا را انجام دادهاید، کامنت این دو خط را بردارید:
|
||||
const { mutate: uploadPicture, isPending: isUploading } = useUploadProfilePicture();
|
||||
const { mutate: deletePicture, isPending: isDeleting } = useDeleteProfilePicture();
|
||||
|
||||
// مقادیر فرضی موقت در صورت عدم وجود ایمپورت برای جلوگیری از ارور (بعد از ایمپورت، این 4 خط را پاک کنید)
|
||||
// const uploadPicture = (file: File, options: any) => options?.onSuccess();
|
||||
// const deletePicture = (data: any, options: any) => options?.onSuccess();
|
||||
// const isUploading = false;
|
||||
// const isDeleting = false;
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
|
||||
// منطق اسکرول هدر
|
||||
const handleScroll = () => {
|
||||
setIsScrolled(window.scrollY > 20);
|
||||
setIsScrolled(window.scrollY > 0);
|
||||
};
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
|
||||
return () => window.removeEventListener("scroll", handleScroll);
|
||||
}, []);
|
||||
|
||||
// مدیریت کلیک خارج از دراپدون پروفایل
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
@@ -107,45 +120,34 @@ export const Header = () => {
|
||||
if (isProfileDropdownOpen) {
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, [isProfileDropdownOpen]);
|
||||
|
||||
// متد خروج از حساب کاربری
|
||||
const handleLogout = async () => {
|
||||
// بستن منوها
|
||||
setIsProfileDropdownOpen(false);
|
||||
setIsMobileMenuOpen(false);
|
||||
|
||||
// نمایش پاپآپ تاییدیه
|
||||
const result = await Swal.fire({
|
||||
title: "آیا مطمئن هستید؟",
|
||||
text: "میخواهید از حساب کاربری خود خارج شوید؟",
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#ef4444",
|
||||
cancelButtonColor: "#9ca3af",
|
||||
confirmButtonColor: "#ef4444",
|
||||
cancelButtonColor: "#9ca3af",
|
||||
confirmButtonText: "بله، خارج میشوم",
|
||||
cancelButtonText: "انصراف",
|
||||
reverseButtons: true,
|
||||
reverseButtons: true,
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
// ۱. پاک کردن توکن از کوکی (فراخوانی Server Action)
|
||||
await removeAuthCookie();
|
||||
|
||||
// ۲. پاک کردن کش اطلاعات کاربر از React Query و لوکال استوریج
|
||||
queryClient.removeQueries({ queryKey: ["userProfile"] });
|
||||
localStorage.removeItem("user");
|
||||
|
||||
// ۳. رفرش کردن دیتای Next.js و انتقال به صفحه اصلی
|
||||
router.refresh();
|
||||
router.push("/");
|
||||
|
||||
// نمایش پیام موفقیت
|
||||
Swal.fire({
|
||||
title: "موفق",
|
||||
text: "با موفقیت از حساب کاربری خارج شدید.",
|
||||
@@ -153,7 +155,6 @@ export const Header = () => {
|
||||
timer: 1500,
|
||||
showConfirmButton: false,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
Swal.fire({
|
||||
title: "خطا",
|
||||
@@ -165,321 +166,462 @@ export const Header = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// تعیین عبارتی که باید به کاربر نمایش داده شود
|
||||
const displayName = userData?.name || userData?.phone_number;
|
||||
// هندلر تغییر فایل و آپلود
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
uploadPicture(file, {
|
||||
onSuccess: () => {
|
||||
setIsProfilePicModalOpen(false);
|
||||
// رفرش کردن دیتای کاربر برای دریافت عکس جدید
|
||||
queryClient.invalidateQueries({ queryKey: ["userProfile"] });
|
||||
},
|
||||
});
|
||||
// پاک کردن مقدار اینپوت برای اینکه انتخاب مجدد همان فایل ممکن باشد
|
||||
if (fileInputRef.current) fileInputRef.current.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
// متغیری برای بررسی وضعیت کلی بارگذاری بخش کاربری
|
||||
// هندلر حذف عکس
|
||||
const handleDeletePicture = () => {
|
||||
deletePicture(undefined, {
|
||||
onSuccess: () => {
|
||||
setIsProfilePicModalOpen(false);
|
||||
// رفرش کردن دیتای کاربر
|
||||
queryClient.invalidateQueries({ queryKey: ["userProfile"] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const displayName = userData?.name || userData?.phone_number;
|
||||
const isProfileSectionLoading = !isMounted || isUserLoading;
|
||||
|
||||
return (
|
||||
<header
|
||||
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
|
||||
isScrolled ? "bg-white shadow-md py-3" : "bg-white/90 backdrop-blur-md py-4 border-b border-gray-100"
|
||||
}`}
|
||||
>
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="flex items-center justify-between">
|
||||
|
||||
{/* بخش راست: لوگو و منوی دسکتاپ */}
|
||||
<div className="flex items-center gap-8 lg:gap-12">
|
||||
{/* منوی موبایل و لوگو */}
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
className="lg:hidden text-gray-700 hover:text-primary transition"
|
||||
onClick={() => setIsMobileMenuOpen(true)}
|
||||
aria-label="باز کردن منو"
|
||||
>
|
||||
<Menu className="w-6 h-6" />
|
||||
</button>
|
||||
{/* <Link href="/">
|
||||
<Image src={LOGO} className="w-50" alt="logo" />
|
||||
</Link> */}
|
||||
<Link href="/" className="text-2xl font-black text-primary tracking-tighter">
|
||||
SALONA<span className="text-gray-900">.</span>
|
||||
</Link>
|
||||
<>
|
||||
<header className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 bg-white shadow-md py-3`}>
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="flex items-center justify-between">
|
||||
{/* بخش راست */}
|
||||
<div className="flex items-center gap-8 lg:gap-12">
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
className="lg:hidden text-gray-700 hover:text-primary transition"
|
||||
onClick={() => setIsMobileMenuOpen(true)}
|
||||
aria-label="باز کردن منو"
|
||||
>
|
||||
<Menu className="w-6 h-6" />
|
||||
</button>
|
||||
<Link href="/">
|
||||
<Image src={LOGO} className="w-30" alt="logo" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<nav className="hidden lg:flex items-center gap-6">
|
||||
<Link
|
||||
href="/products"
|
||||
className="text-gray-600 hover:text-primary font-medium text-sm transition-colors relative group"
|
||||
>
|
||||
محصولات
|
||||
<span className="absolute -bottom-1 right-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full"></span>
|
||||
</Link>
|
||||
<Link
|
||||
href="/categories"
|
||||
className="text-gray-600 hover:text-primary font-medium text-sm transition-colors relative group"
|
||||
>
|
||||
دستهبندیها
|
||||
<span className="absolute -bottom-1 right-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full"></span>
|
||||
</Link>
|
||||
<Link
|
||||
href="/about"
|
||||
className="text-gray-600 hover:text-primary font-medium text-sm transition-colors relative group"
|
||||
>
|
||||
درباره ما
|
||||
<span className="absolute -bottom-1 right-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full"></span>
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* ناوبری دسکتاپ */}
|
||||
<nav className="hidden lg:flex items-center gap-6">
|
||||
<Link
|
||||
href="/products"
|
||||
className="text-gray-600 hover:text-primary font-medium text-sm transition-colors relative group"
|
||||
{/* بخش چپ */}
|
||||
<div className="flex items-center gap-3 sm:gap-5">
|
||||
<Link
|
||||
href="/profile/favorites"
|
||||
className="text-gray-600 hover:text-primary transition hidden sm:block"
|
||||
>
|
||||
محصولات
|
||||
<span className="absolute -bottom-1 right-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full"></span>
|
||||
<Heart className="w-6 h-6" />
|
||||
</Link>
|
||||
<Link
|
||||
href="/categories"
|
||||
className="text-gray-600 hover:text-primary font-medium text-sm transition-colors relative group"
|
||||
>
|
||||
دستهبندیها
|
||||
<span className="absolute -bottom-1 right-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full"></span>
|
||||
|
||||
<Link href="/cart" className="relative text-gray-600 hover:text-primary transition">
|
||||
<ShoppingCart className="w-6 h-6" />
|
||||
{isMounted && uniqueProductCount > 0 && (
|
||||
<span className="absolute -top-1.5 -right-1.5 bg-red-500 text-white text-[10px] font-bold w-4 h-4 rounded-full flex items-center justify-center">
|
||||
{uniqueProductCount}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
href="/about"
|
||||
className="text-gray-600 hover:text-primary font-medium text-sm transition-colors relative group"
|
||||
>
|
||||
درباره ما
|
||||
<span className="absolute -bottom-1 right-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full"></span>
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* بخش چپ: آیکونهای اکشن */}
|
||||
<div className="flex items-center gap-3 sm:gap-5">
|
||||
<Link
|
||||
href="/profile/favorites"
|
||||
className="text-gray-600 hover:text-primary transition hidden sm:block"
|
||||
aria-label="علاقهمندیها"
|
||||
>
|
||||
<Heart className="w-6 h-6" />
|
||||
</Link>
|
||||
|
||||
<Link href="/cart" className="relative text-gray-600 hover:text-primary transition" aria-label="سبد خرید">
|
||||
<ShoppingCart className="w-6 h-6" />
|
||||
{isMounted && uniqueProductCount > 0 && (
|
||||
<span className="absolute -top-1.5 -right-1.5 bg-red-500 text-white text-[10px] font-bold w-4 h-4 rounded-full flex items-center justify-center">
|
||||
{uniqueProductCount}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
|
||||
{/* بخش پروفایل کاربر */}
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
{isProfileSectionLoading ? (
|
||||
<div className="hidden sm:flex items-center gap-2">
|
||||
<div className="w-8 h-8 bg-gray-200 rounded-full animate-pulse"></div>
|
||||
<div className="w-24 h-8 bg-gray-200 rounded-full animate-pulse"></div>
|
||||
</div>
|
||||
) : userData ? (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => setIsProfileDropdownOpen(!isProfileDropdownOpen)}
|
||||
className="hidden cursor-pointer sm:flex items-center gap-2 py-1.5 pl-2 pr-3 border border-gray-200 rounded-full hover:border-primary hover:bg-primary/5 transition-all focus:outline-none"
|
||||
>
|
||||
<div className="bg-primary/10 py-1.5 rounded-full text-primary">
|
||||
<User className="w-4 h-4" />
|
||||
</div>
|
||||
<span className="font-medium text-sm text-gray-700 dir-ltr tracking-wide">
|
||||
{displayName}
|
||||
</span>
|
||||
<ChevronDown
|
||||
className={`w-4 h-4 text-gray-500 transition-transform duration-200 ${isProfileDropdownOpen ? "rotate-180 text-primary" : ""}`}
|
||||
/>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setIsProfileDropdownOpen(!isProfileDropdownOpen)}
|
||||
className="sm:hidden flex items-center justify-center bg-primary/10 text-primary p-2 rounded-full"
|
||||
>
|
||||
<User className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
{/* دراپدون حساب کاربری */}
|
||||
<div
|
||||
className={`absolute top-full left-0 mt-3 w-56 rounded-xl bg-white shadow-[0_10px_40px_-10px_rgba(0,0,0,0.1)] border border-gray-200 overflow-hidden transition-all duration-200 origin-top-left ${
|
||||
isProfileDropdownOpen
|
||||
? "opacity-100 scale-100 visible"
|
||||
: "opacity-0 scale-95 invisible"
|
||||
}`}
|
||||
>
|
||||
<div className="sm:hidden px-4 py-3 border-b border-gray-50 bg-gray-50/50">
|
||||
<span className="block text-xs text-gray-500 mb-1">کاربر سالونا</span>
|
||||
<span className="block font-semibold text-gray-800 text-sm dir-ltr text-left">
|
||||
{/* پروفایل کاربر */}
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
{isProfileSectionLoading ? (
|
||||
<div className="hidden sm:flex items-center gap-2">
|
||||
<div className="w-8 h-8 bg-gray-200 rounded-full animate-pulse"></div>
|
||||
<div className="w-24 h-8 bg-gray-200 rounded-full animate-pulse"></div>
|
||||
</div>
|
||||
) : userData ? (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => setIsProfileDropdownOpen(!isProfileDropdownOpen)}
|
||||
className="hidden cursor-pointer sm:flex items-center gap-2 py-1.5 pl-2 pr-3 border border-gray-200 rounded-full hover:border-primary hover:bg-primary/5 transition-all focus:outline-none"
|
||||
>
|
||||
<div className="bg-primary/10 py-1.5 rounded-full text-primary overflow-hidden w-7 h-7 flex items-center justify-center">
|
||||
{userData.profile_picture ? (
|
||||
<Image
|
||||
src={userData.profile_picture}
|
||||
alt="profile"
|
||||
width={28}
|
||||
height={28}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<User className="w-4 h-4" />
|
||||
)}
|
||||
</div>
|
||||
<span className="font-medium text-sm text-gray-700 dir-ltr tracking-wide">
|
||||
{displayName}
|
||||
</span>
|
||||
<ChevronDown
|
||||
className={`w-4 h-4 text-gray-500 transition-transform duration-200 ${isProfileDropdownOpen ? "rotate-180 text-primary" : ""}`}
|
||||
/>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setIsProfileDropdownOpen(!isProfileDropdownOpen)}
|
||||
className="sm:hidden flex items-center justify-center bg-primary/10 text-primary p-2 rounded-full overflow-hidden w-9 h-9"
|
||||
>
|
||||
{userData.profile_picture ? (
|
||||
<Image
|
||||
src={userData.profile_picture}
|
||||
alt="profile"
|
||||
width={36}
|
||||
height={36}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<User className="w-5 h-5" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* دراپدون حساب کاربری */}
|
||||
<div
|
||||
className={`absolute top-full left-0 mt-3 w-56 rounded-xl bg-white shadow-[0_10px_40px_-10px_rgba(0,0,0,0.1)] border border-gray-200 overflow-hidden transition-all duration-200 origin-top-left ${
|
||||
isProfileDropdownOpen
|
||||
? "opacity-100 scale-100 visible"
|
||||
: "opacity-0 scale-95 invisible"
|
||||
}`}
|
||||
>
|
||||
<div className="sm:hidden px-4 py-3 border-b border-gray-50 bg-gray-50/50">
|
||||
<span className="block text-xs text-gray-500 mb-1">کاربر سالونا</span>
|
||||
<span className="block font-semibold text-gray-800 text-sm dir-ltr text-left">
|
||||
{displayName}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="py-2">
|
||||
<Link
|
||||
href="/checkout"
|
||||
className="flex items-center gap-3 px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-50 hover:text-green-600 transition-colors"
|
||||
onClick={() => setIsProfileDropdownOpen(false)}
|
||||
>
|
||||
<MapPin className="w-4 h-4" />
|
||||
<span className="font-medium">ثبت آدرس</span>
|
||||
</Link>
|
||||
|
||||
<div className="h-px bg-gray-100 my-1"></div>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsProfileDropdownOpen(false);
|
||||
setIsProfilePicModalOpen(true);
|
||||
}}
|
||||
className="w-full cursor-pointer flex items-center gap-3 px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-50 hover:text-purple-600 transition-colors"
|
||||
>
|
||||
<Camera className="w-4 h-4" />
|
||||
<span className="font-medium">عکس پروفایل</span>
|
||||
</button>
|
||||
|
||||
<div className="h-px bg-gray-100 my-1"></div>
|
||||
|
||||
<Link
|
||||
href="/tickets"
|
||||
className="flex items-center gap-3 px-4 py-3 text-sm font-medium text-gray-700 transition-colors group hover:bg-gray-50 hover:text-blue-600"
|
||||
onClick={() => setIsProfileDropdownOpen(false)}
|
||||
>
|
||||
<MessageSquare className="w-4 h-4 text-gray-700 transition-colors group-hover:text-blue-500" />
|
||||
پشتیبانی و تیکتها
|
||||
</Link>
|
||||
|
||||
<div className="h-px bg-gray-100 my-1"></div>
|
||||
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full cursor-pointer flex items-center gap-3 px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-50 hover:text-red-600 transition-colors"
|
||||
>
|
||||
<LogOut className="w-4 h-4" />
|
||||
<span className="font-medium">خروج از حساب</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Link
|
||||
href="/login"
|
||||
className="flex items-center gap-2 text-gray-600 hover:text-primary transition"
|
||||
>
|
||||
<User className="w-6 h-6" />
|
||||
<span className="hidden sm:block text-sm font-medium">ورود | ثبتنام</span>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="py-2">
|
||||
<Link
|
||||
href="/checkout"
|
||||
className="flex items-center gap-3 px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-50 hover:text-green-600 transition-colors"
|
||||
onClick={() => setIsProfileDropdownOpen(false)}
|
||||
>
|
||||
<MapPin className="w-4 h-4" />
|
||||
<span className="font-medium">ثبت آدرس</span>
|
||||
</Link>
|
||||
{/* منوی موبایل */}
|
||||
<div
|
||||
className={`fixed inset-0 bg-black/50 z-[60] lg:hidden transition-opacity duration-300 ${
|
||||
isMobileMenuOpen ? "opacity-100 visible" : "opacity-0 invisible pointer-events-none"
|
||||
}`}
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
<div
|
||||
className={`fixed top-0 right-0 h-full w-3/4 max-w-sm bg-white shadow-2xl transition-transform duration-300 ease-in-out transform flex flex-col ${
|
||||
isMobileMenuOpen ? "translate-x-0" : "translate-x-full"
|
||||
}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between p-5 border-b border-gray-100">
|
||||
<span className="text-xl font-black text-primary">SALONA</span>
|
||||
<button
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
className="p-2 text-gray-500 hover:text-red-500 bg-gray-50 rounded-full transition"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="h-px bg-gray-100 my-1"></div>
|
||||
|
||||
<Link
|
||||
href="/tickets"
|
||||
className="flex items-center gap-3 px-4 py-3 text-sm font-medium text-gray-700 transition-colors group hover:bg-gray-50 hover:text-blue-600"
|
||||
onClick={() => setIsProfileDropdownOpen(false)}
|
||||
>
|
||||
<MessageSquare className="w-4 h-4 text-gray-700 transition-colors group-hover:text-blue-500" />
|
||||
پشتیبانی و تیکتها
|
||||
</Link>
|
||||
|
||||
<div className="h-px bg-gray-100 my-1"></div>
|
||||
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full cursor-pointer flex items-center gap-3 px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-50 hover:text-red-600 transition-colors"
|
||||
>
|
||||
<LogOut className="w-4 h-4" />
|
||||
<span className="font-medium">خروج از حساب</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-5 flex-1 overflow-y-auto">
|
||||
{isProfileSectionLoading ? (
|
||||
<div className="flex items-center gap-3 mb-8 p-4 bg-gray-50 rounded-2xl border border-gray-100 animate-pulse">
|
||||
<div className="w-11 h-11 bg-gray-200 rounded-full"></div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="w-16 h-3 bg-gray-200 rounded"></div>
|
||||
<div className="w-24 h-4 bg-gray-200 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
) : userData ? (
|
||||
<div className="flex items-center gap-3 mb-8 p-4 bg-primary/5 rounded-2xl border border-primary/10">
|
||||
<div className="bg-white p-2 rounded-full shadow-sm text-primary overflow-hidden w-11 h-11 flex items-center justify-center">
|
||||
{userData.profile_picture ? (
|
||||
<Image
|
||||
src={userData.profile_picture}
|
||||
alt="profile"
|
||||
width={44}
|
||||
height={44}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<User className="w-6 h-6" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs text-gray-500 mb-1">خوش آمدید،</span>
|
||||
<span className="font-bold text-gray-800 dir-ltr text-left tracking-wider">
|
||||
{displayName}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Link
|
||||
href="/login"
|
||||
className="flex items-center gap-2 text-gray-600 hover:text-primary transition"
|
||||
className="flex items-center justify-center gap-2 mb-8 p-3 bg-primary text-white rounded-xl font-medium hover:bg-primary/90 transition shadow-lg shadow-primary/20"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
<User className="w-6 h-6" />
|
||||
<span className="hidden sm:block text-sm font-medium">ورود | ثبتنام</span>
|
||||
<User className="w-5 h-5" />
|
||||
ورود به حساب کاربری
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* منوی موبایل */}
|
||||
<div
|
||||
className={`fixed inset-0 bg-black/50 z-50 lg:hidden transition-opacity duration-300 ${
|
||||
isMobileMenuOpen ? "opacity-100 visible" : "opacity-0 invisible pointer-events-none"
|
||||
}`}
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
<div
|
||||
className={`fixed top-0 right-0 h-full w-3/4 max-w-sm bg-white shadow-2xl transition-transform duration-300 ease-in-out transform flex flex-col ${
|
||||
isMobileMenuOpen ? "translate-x-0" : "translate-x-full"
|
||||
}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between p-5 border-b border-gray-100">
|
||||
<span className="text-xl font-black text-primary">SALONA</span>
|
||||
<button
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
className="p-2 text-gray-500 hover:text-red-500 bg-gray-50 rounded-full transition"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<nav className="flex flex-col gap-2">
|
||||
<span className="text-xs font-bold text-gray-400 mb-2 px-2">دسترسی سریع</span>
|
||||
|
||||
<div className="p-5 flex-1 overflow-y-auto">
|
||||
{isProfileSectionLoading ? (
|
||||
<div className="flex items-center gap-3 mb-8 p-4 bg-gray-50 rounded-2xl border border-gray-100 animate-pulse">
|
||||
<div className="w-11 h-11 bg-gray-200 rounded-full"></div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="w-16 h-3 bg-gray-200 rounded"></div>
|
||||
<div className="w-24 h-4 bg-gray-200 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
) : userData ? (
|
||||
<div className="flex items-center gap-3 mb-8 p-4 bg-primary/5 rounded-2xl border border-primary/10">
|
||||
<div className="bg-white p-2.5 rounded-full shadow-sm text-primary">
|
||||
<User className="w-6 h-6" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs text-gray-500 mb-1">خوش آمدید،</span>
|
||||
<span className="font-bold text-gray-800 dir-ltr text-left tracking-wider">
|
||||
{displayName}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Link
|
||||
href="/login"
|
||||
className="flex items-center justify-center gap-2 mb-8 p-3 bg-primary text-white rounded-xl font-medium hover:bg-primary/90 transition shadow-lg shadow-primary/20"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
<User className="w-5 h-5" />
|
||||
ورود به حساب کاربری
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<nav className="flex flex-col gap-2">
|
||||
<span className="text-xs font-bold text-gray-400 mb-2 px-2">دسترسی سریع</span>
|
||||
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center gap-3 px-3 py-3 rounded-lg text-gray-700 font-medium hover:bg-gray-50 hover:text-primary transition-colors"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
<Home className="w-5 h-5 text-gray-400" />
|
||||
صفحه اصلی
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/products"
|
||||
className="flex items-center gap-3 px-3 py-3 rounded-lg text-gray-700 font-medium hover:bg-gray-50 hover:text-primary transition-colors"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
<Package className="w-5 h-5 text-gray-400" />
|
||||
محصولات
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/categories"
|
||||
className="flex items-center gap-3 px-3 py-3 rounded-lg text-gray-700 font-medium hover:bg-gray-50 hover:text-primary transition-colors"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
<LayoutGrid className="w-5 h-5 text-gray-400" />
|
||||
دستهبندیها
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/about"
|
||||
className="flex items-center gap-3 px-3 py-3 rounded-lg text-gray-700 font-medium hover:bg-gray-50 hover:text-primary transition-colors"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
<Info className="w-5 h-5 text-gray-400" />
|
||||
درباره ما
|
||||
</Link>
|
||||
|
||||
<div className="h-px bg-gray-100 my-4"></div>
|
||||
|
||||
<span className="text-xs font-bold text-gray-400 mb-2 px-2">حساب کاربری</span>
|
||||
|
||||
{userData && (
|
||||
<Link
|
||||
href="/checkout"
|
||||
href="/"
|
||||
className="flex items-center gap-3 px-3 py-3 rounded-lg text-gray-700 font-medium hover:bg-gray-50 hover:text-primary transition-colors"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
<MapPin className="w-5 h-5 text-gray-400" />
|
||||
ثبت آدرس
|
||||
<Home className="w-5 h-5 text-gray-400" />
|
||||
صفحه اصلی
|
||||
</Link>
|
||||
<Link
|
||||
href="/products"
|
||||
className="flex items-center gap-3 px-3 py-3 rounded-lg text-gray-700 font-medium hover:bg-gray-50 hover:text-primary transition-colors"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
<Package className="w-5 h-5 text-gray-400" />
|
||||
محصولات
|
||||
</Link>
|
||||
<Link
|
||||
href="/categories"
|
||||
className="flex items-center gap-3 px-3 py-3 rounded-lg text-gray-700 font-medium hover:bg-gray-50 hover:text-primary transition-colors"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
<LayoutGrid className="w-5 h-5 text-gray-400" />
|
||||
دستهبندیها
|
||||
</Link>
|
||||
<Link
|
||||
href="/about"
|
||||
className="flex items-center gap-3 px-3 py-3 rounded-lg text-gray-700 font-medium hover:bg-gray-50 hover:text-primary transition-colors"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
<Info className="w-5 h-5 text-gray-400" />
|
||||
درباره ما
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<Link
|
||||
href="/profile/favorites"
|
||||
className="flex items-center gap-3 px-3 py-3 rounded-lg text-gray-700 font-medium hover:bg-gray-50 hover:text-primary transition-colors"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
<Heart className="w-5 h-5 text-gray-400" />
|
||||
علاقهمندیها
|
||||
</Link>
|
||||
<div className="h-px bg-gray-100 my-4"></div>
|
||||
|
||||
<Link
|
||||
href="/tickets"
|
||||
className="flex items-center gap-3 px-3 py-3 rounded-lg text-gray-700 font-medium hover:bg-gray-50 hover:text-primary transition-colors"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
<MessageSquare className="w-5 h-5 text-gray-400" />
|
||||
پشتیبانی و تیکتها
|
||||
</Link>
|
||||
</nav>
|
||||
<span className="text-xs font-bold text-gray-400 mb-2 px-2">حساب کاربری</span>
|
||||
|
||||
{userData && (
|
||||
<>
|
||||
<Link
|
||||
href="/checkout"
|
||||
className="flex items-center gap-3 px-3 py-3 rounded-lg text-gray-700 font-medium hover:bg-gray-50 hover:text-primary transition-colors"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
<MapPin className="w-5 h-5 text-gray-400" />
|
||||
ثبت آدرس
|
||||
</Link>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsMobileMenuOpen(false);
|
||||
setIsProfilePicModalOpen(true);
|
||||
}}
|
||||
className="flex cursor-pointer w-full items-center gap-3 px-3 py-3 rounded-lg text-gray-700 font-medium hover:bg-gray-50 hover:text-primary transition-colors"
|
||||
>
|
||||
<Camera className="w-5 h-5 text-gray-400" />
|
||||
عکس پروفایل
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Link
|
||||
href="/profile/favorites"
|
||||
className="flex items-center gap-3 px-3 py-3 rounded-lg text-gray-700 font-medium hover:bg-gray-50 hover:text-primary transition-colors"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
<Heart className="w-5 h-5 text-gray-400" />
|
||||
علاقهمندیها
|
||||
</Link>
|
||||
<Link
|
||||
href="/tickets"
|
||||
className="flex items-center gap-3 px-3 py-3 rounded-lg text-gray-700 font-medium hover:bg-gray-50 hover:text-primary transition-colors"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
<MessageSquare className="w-5 h-5 text-gray-400" />
|
||||
پشتیبانی و تیکتها
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{userData && (
|
||||
<div className="p-5 border-t border-gray-100 mt-auto bg-gray-50/50">
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="flex w-full items-center justify-center gap-3 p-3 bg-red-50 text-red-600 rounded-xl font-medium hover:bg-red-100 transition-colors"
|
||||
>
|
||||
<LogOut className="w-5 h-5" />
|
||||
خروج از حساب کاربری
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* دکمه خروج در پایین منوی موبایل (چسبیده به پایین) */}
|
||||
{userData && (
|
||||
<div className="p-5 border-t border-gray-100 mt-auto bg-gray-50/50">
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* مودال آپلود / حذف عکس پروفایل */}
|
||||
{isProfilePicModalOpen && (
|
||||
<div
|
||||
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4"
|
||||
onClick={() => setIsProfilePicModalOpen(false)}
|
||||
>
|
||||
<div
|
||||
className="bg-white rounded-2xl w-full max-w-sm p-6 relative shadow-2xl"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<button
|
||||
onClick={() => setIsProfilePicModalOpen(false)}
|
||||
className="absolute top-4 left-4 text-gray-400 hover:text-gray-700 transition-colors bg-gray-50 hover:bg-gray-100 p-1.5 rounded-full"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
<div className="flex flex-col items-center mb-6 mt-2">
|
||||
<div className="w-24 h-24 bg-gray-100 rounded-full flex items-center justify-center mb-4 border-4 border-gray-400 overflow-hidden relative">
|
||||
{userData?.profile_picture ? (
|
||||
<Image src={userData.profile_picture} alt="profile" fill className="object-cover" />
|
||||
) : (
|
||||
<Camera className="w-10 h-10 text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-gray-800">تصویر پروفایل</h3>
|
||||
<p className="text-sm text-gray-500 mt-1">تصویر کاربری خود را مدیریت کنید</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* Input مخفی برای دریافت فایل */}
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
hidden
|
||||
ref={fileInputRef}
|
||||
onChange={handleFileChange}
|
||||
disabled={isUploading}
|
||||
/>
|
||||
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="flex w-full items-center justify-center gap-3 p-3 bg-red-50 text-red-600 rounded-xl font-medium hover:bg-red-100 transition-colors"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={isUploading || isDeleting}
|
||||
className="w-full cursor-pointer flex justify-center items-center gap-2 py-3 bg-primary rounded-xl font-medium hover:bg-primary/90 transition-colors disabled:opacity-70"
|
||||
>
|
||||
<LogOut className="w-5 h-5" />
|
||||
خروج از حساب کاربری
|
||||
{isUploading ? (
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
) : (
|
||||
<UploadCloud className="w-5 h-5" />
|
||||
)}
|
||||
{isUploading ? "در حال آپلود..." : "انتخاب و آپلود عکس جدید"}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleDeletePicture}
|
||||
disabled={isUploading || isDeleting || !userData?.profile_picture}
|
||||
className={`w-full flex justify-center items-center gap-2 py-3 rounded-xl font-medium transition-colors border
|
||||
${
|
||||
!userData?.profile_picture
|
||||
? "bg-gray-50 text-gray-400 border-gray-100 cursor-not-allowed"
|
||||
: "bg-white text-red-500 border-red-200 hover:bg-red-50 disabled:opacity-70"
|
||||
}`}
|
||||
>
|
||||
{isDeleting ? (
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
) : (
|
||||
<Trash2 className="w-5 h-5" />
|
||||
)}
|
||||
{isDeleting ? "در حال حذف..." : "حذف عکس فعلی"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -47,7 +47,7 @@ axiosInstance.interceptors.response.use(
|
||||
// ۱. مدیریت خطای ۴۰۱ (عدم دسترسی / توکن نامعتبر یا منقضی شده)
|
||||
if (error.response?.status === 401) {
|
||||
|
||||
if (!isAuthAlertShown && typeof window !== "undefined") {
|
||||
if (!isAuthAlertShown && typeof window !== "undefined" && !window.location.href.includes("login")) {
|
||||
isAuthAlertShown = true;
|
||||
|
||||
Swal.fire({
|
||||
@@ -67,7 +67,7 @@ axiosInstance.interceptors.response.use(
|
||||
isAuthAlertShown = false;
|
||||
|
||||
if (result.isConfirmed) {
|
||||
window.location.href = "/auth";
|
||||
window.location.href = "/login";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { axiosInstance } from "@/lib/axios";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
// تعریف تایپهای مربوط به پروفایل کاربر
|
||||
export interface UserProfile {
|
||||
@@ -23,14 +24,14 @@ export const useUserProfile = (isAuthenticated: boolean = true) => {
|
||||
},
|
||||
// تا زمانی که isAuthenticated برابر با false باشد، این ریکوئست به سمت سرور ارسال نمیشود
|
||||
enabled: isAuthenticated,
|
||||
retry: false
|
||||
retry: false,
|
||||
});
|
||||
};
|
||||
|
||||
// 2. ویرایش اطلاعات کاربر (PUT)
|
||||
export const useUpdateUserProfile = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (payload: UpdateProfilePayload) => {
|
||||
const { data } = await axiosInstance.put<UserProfile>("/api/users/me", payload);
|
||||
@@ -41,6 +42,35 @@ export const useUpdateUserProfile = () => {
|
||||
// تا اطلاعات جدید بلافاصله از سرور دریافت و در رابط کاربری (UI) بروزرسانی شود
|
||||
queryClient.invalidateQueries({ queryKey: ["userProfile"] });
|
||||
},
|
||||
retry: false
|
||||
retry: false,
|
||||
});
|
||||
};
|
||||
|
||||
export const useUploadProfilePicture = () => {
|
||||
return useMutation({
|
||||
mutationFn: async (file: File) => {
|
||||
const bodyFormData = new FormData();
|
||||
if (file) bodyFormData.append("profile_picture", file);
|
||||
return await axiosInstance.post("/upload_profile_picture", bodyFormData, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
});
|
||||
},
|
||||
onSuccess: (response) => {
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
toast.success(response.data.message ?? "تصویر پروفایل با موفقیت به روز رسانی شد");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
export const useDeleteProfilePicture = () => {
|
||||
return useMutation({
|
||||
mutationFn: async () => await axiosInstance.delete(`/delete_profile_picture`),
|
||||
onSuccess: (response) => {
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
toast.success(response.data.message ?? "تصویر پروفایل با موفقیت حذف گردید");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user