This commit is contained in:
Reza Taghizadeh
2026-03-17 17:07:08 +03:30
parent 52f38fd3af
commit 7a72bf9b5c
9 changed files with 923 additions and 328 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 122 KiB

183
src/app/about/page.tsx Normal file
View 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>
);
}

View 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>
);
}

View 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>
);
}

View File

@@ -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>

View File

@@ -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}

View File

@@ -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>
)}
</>
);
};

View File

@@ -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";
}
});
}

View File

@@ -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 ?? "تصویر پروفایل با موفقیت حذف گردید");
}
},
});
};