diff --git a/public/assets/images/logo/logo.png b/public/assets/images/logo/logo.png index 4798681..b8c5195 100644 Binary files a/public/assets/images/logo/logo.png and b/public/assets/images/logo/logo.png differ diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx new file mode 100644 index 0000000..a24f702 --- /dev/null +++ b/src/app/about/page.tsx @@ -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 ( + // تخصیص فونت وزیرمتن و راست‌چین کردن کل صفحه +
+ {/* بخش هیرو (Hero Section) */} +
+
+
+
+ داستان سالونا +
+

+ تلاقی هنر زیبایی و
+ تکنولوژی مدرن +

+

+ سالونا تنها یک وب‌سایت نیست؛ یک همراه همیشگی برای بانوانی است که به زمان و زیبایی خود اهمیت + می‌دهند. ما با درک دغدغه‌های روزمره شما، پلتفرمی را خلق کرده‌ایم که نه‌تنها فرآیند رزرو نوبت + در بهترین آرایشگاه‌های زنانه را بی‌دردسر می‌کند، بلکه دسترسی به مرغوب‌ترین محصولات آرایشی و + بهداشتی را در یک مکان فراهم آورده است. +

+
+ + + رزرو نوبت + + + + فروشگاه محصولات + +
+
+ +
+ {/* در پروژه واقعی از عکس با کیفیت خودتان استفاده کنید */} + سالن زیبایی سالونا +
+
+

سالونا

+

تجربه متفاوت زیبایی

+
+
+
+
+ + {/* بخش خدمات اصلی (Core Services) */} +
+
+
+

+ ما چه کاری انجام می‌دهیم؟ +

+

+ سالونا دو بال قدرتمند برای پرواز به سوی زیبایی دارد. ما خدمات حضوری و محصولات خانگی را به + یکدیگر پیوند داده‌ایم. +

+
+ +
+ {/* کارت رزرو */} +
+
+ +
+

مدیریت نوبت‌دهی آرایشگاه‌ها

+

+ دیگر نیازی به تماس‌های مکرر و هماهنگی‌های زمان‌بر نیست. با سالونا می‌توانید تقویم کاری + بهترین سالن‌های زیبایی را مشاهده کنید، نمونه کارها را ببینید و در ساعت دلخواه خود، + نوبتتان را قطعی کنید. +

+
+ + {/* کارت فروشگاه */} +
+
+ +
+

فروشگاه تخصصی محصولات

+

+ مراقبت از زیبایی پس از خروج از سالن متوقف نمی‌شود. ما مجموعه‌ای از بهترین و معتبرترین + برندهای آرایشی، مراقبت از پوست و مو را با ضمانت اصالت کالا برای شما گردآوری کرده‌ایم. +

+
+
+
+
+ + {/* بخش ارزش‌های کلیدی (Core Values) */} +
+
+

چرا سالونا را انتخاب کنید؟

+
+
+ +
+ {[ + { + icon: , + title: "صرفه‌جویی در زمان", + desc: "رزرو سریع در کمتر از ۲ دقیقه و ارسال فوری محصولات به سراسر کشور.", + }, + { + icon: , + title: "تضمین اصالت", + desc: "تمامی محصولات فروشگاه و خدمات سالن‌ها مورد تایید و دارای ضمانت هستند.", + }, + { + icon: , + title: "تنوع بی‌نظیر", + desc: "دسترسی به صدها سالن زیبایی و هزاران محصول آرایشی در یک پلتفرم.", + }, + { + icon: , + title: "پشتیبانی دلسوزانه", + desc: "تیم پشتیبانی سالونا در تمام مراحل رزرو و خرید در کنار شماست.", + }, + ].map((item, index) => ( +
+
+ {item.icon} +
+

{item.title}

+

{item.desc}

+
+ ))} +
+
+ + {/* بخش دعوت به اقدام (Call to Action) */} +
+
+ {/* افکت‌های پس‌زمینه تزئینی */} +
+
+ +

+ آماده‌اید تا سبک جدیدی از زیبایی را تجربه کنید؟ +

+

+ همین حالا به جمع هزاران کاربر سالونا بپیوندید. نوبت خود را رزرو کنید یا سبد خریدتان را از + محصولات شگفت‌انگیز پر کنید. +

+
+ + عضویت در سالونا + + + تماس با ما + +
+
+
+
+ ); +} diff --git a/src/app/categories/[id]/page.tsx b/src/app/categories/[id]/page.tsx new file mode 100644 index 0000000..5664723 --- /dev/null +++ b/src/app/categories/[id]/page.tsx @@ -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
در حال بارگذاری...
; + } + + if (!currentCategory && !isCatLoading) { + return
دسته‌بندی مورد نظر یافت نشد.
; + } + + // جدا کردن مسیرها برای Breadcrumb (مثلا "محصولات مراقبتی > پوست" تبدیل به آرایه می‌شود) + const breadcrumbs = currentCategory?.full_path.split(" > ") || []; + + return ( +
+ {/* بخش هدر دسته‌بندی */} +
+
+ {/* مسیر راهنما (Breadcrumbs) */} + + +

+ {currentCategory?.name} +

+ + {/* نمایش زیردسته‌ها در صورت وجود */} + {currentCategory?.children && currentCategory.children.length > 0 && ( +
+ {currentCategory.children.map((child: Category) => ( + + {child.name} + + ))} +
+ )} +
+
+ + {/* بخش محصولات */} +
+
+

+ {isProdLoading ? "در حال جستجو..." : `${productsData?.products?.length || 0} کالا یافت شد`} +

+ +
+ + {isProdLoading ? ( + // اسکلتون لودینگ محصولات +
+ {Array.from({ length: 8 }).map((_, i) => ( +
+ ))} +
+ ) : productsData?.products?.length > 0 ? ( + // گرید محصولات +
+ {productsData.products.map((product: Product) => { + const favoriteRecord = favoritesData?.favorites?.find( + (fav) => fav.product_id === product.id, + ); + return ( + + ); + })} +
+ ) : ( + // حالت خالی (Empty State) +
+
+ +
+

محصولی یافت نشد!

+

در حال حاضر محصولی در این دسته‌بندی وجود ندارد.

+ + بازگشت به همه دسته‌بندی‌ها + +
+ )} +
+
+ ); +} diff --git a/src/app/categories/page.tsx b/src/app/categories/page.tsx new file mode 100644 index 0000000..fd20ca8 --- /dev/null +++ b/src/app/categories/page.tsx @@ -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 ( +
+ {[1, 2, 3].map((i) => ( +
+ ))} +
+ ); + } + + // جدا کردن دسته‌های سطح صفر (اصلی) + const rootCategories = data?.categories?.filter((cat: Category) => cat.level === 0) || []; + + return ( +
+
+
+ +
+
+

دسته‌بندی محصولات

+

+ کشف زیبایی در میان {data?.total_categories} دسته‌بندی متنوع سالونا +

+
+
+ +
+ {rootCategories.map((rootCategory: Category) => ( +
+
+

+ + {rootCategory.name} +

+ + مشاهده همه + +
+ + {/* نمایش زیردسته‌ها (فرزندان) */} + {rootCategory.children && rootCategory.children.length > 0 ? ( +
    + {rootCategory.children.map((child: Category) => ( +
  • + + + {child.name} + + + +
  • + ))} +
+ ) : ( +
+ بدون زیردسته +
+ )} +
+ ))} +
+
+ ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx index fe16d53..78eab6b 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -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" > مشاهده همه - + diff --git a/src/components/auth/AuthForm.tsx b/src/components/auth/AuthForm.tsx index a1d7c7a..dbc6af6 100644 --- a/src/components/auth/AuthForm.tsx +++ b/src/components/auth/AuthForm.tsx @@ -42,10 +42,7 @@ export default function AuthForm() { const { mutate: registerUser, isPending: isRegisterPending } = useRegister(); const { mutate: verifyOtp, isPending: isVerifyPending } = useVerifyLogin(); - const { - register, - handleSubmit, - } = useForm(); + const { register, handleSubmit } = useForm(); 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 }) => ( -
{/* اصلاح فاصله برای جاگیری بهتر ۶ رقم */} +
+ {" "} + {/* اصلاح فاصله برای جاگیری بهتر ۶ رقم */} {slots.map((slot, idx) => (
{ @@ -60,10 +66,13 @@ export const Header = () => { const [isProfileDropdownOpen, setIsProfileDropdownOpen] = useState(false); const dropdownRef = useRef(null); + // استیت و رفرنس برای مودال عکس پروفایل + const [isProfilePicModalOpen, setIsProfilePicModalOpen] = useState(false); + const fileInputRef = useRef(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) => { + 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 ( -
-
-
- - {/* بخش راست: لوگو و منوی دسکتاپ */} -
- {/* منوی موبایل و لوگو */} -
- - {/* - logo - */} - - SALONA. - + <> +
+
+
+ {/* بخش راست */} +
+
+ + + logo + +
+ +
- {/* ناوبری دسکتاپ */} - -
- {/* بخش چپ: آیکون‌های اکشن */} -
- - - - - - - {isMounted && uniqueProductCount > 0 && ( - - {uniqueProductCount} - - )} - - - {/* بخش پروفایل کاربر */} -
- {isProfileSectionLoading ? ( -
-
-
-
- ) : userData ? ( -
- - - - - {/* دراپ‌دون حساب کاربری */} -
-
- کاربر سالونا - + {/* پروفایل کاربر */} +
+ {isProfileSectionLoading ? ( +
+
+
+
+ ) : userData ? ( +
+ + + + + {/* دراپ‌دون حساب کاربری */} +
+
+ کاربر سالونا + + {displayName} + +
+ +
+ setIsProfileDropdownOpen(false)} + > + + ثبت آدرس + + +
+ + + +
+ + setIsProfileDropdownOpen(false)} + > + + پشتیبانی و تیکت‌ها + + +
+ + +
+
+ ) : ( + + + ورود | ثبت‌نام + + )} +
+
+
+
-
- setIsProfileDropdownOpen(false)} - > - - ثبت آدرس - + {/* منوی موبایل */} +
setIsMobileMenuOpen(false)} + > +
e.stopPropagation()} + > +
+ SALONA + +
-
- - setIsProfileDropdownOpen(false)} - > - - پشتیبانی و تیکت‌ها - - -
- - -
+
+ {isProfileSectionLoading ? ( +
+
+
+
+
+
+
+ ) : userData ? ( +
+
+ {userData.profile_picture ? ( + profile + ) : ( + + )} +
+
+ خوش آمدید، + + {displayName} +
) : ( setIsMobileMenuOpen(false)} > - - ورود | ثبت‌نام + + ورود به حساب کاربری )} -
-
-
-
- {/* منوی موبایل */} -
setIsMobileMenuOpen(false)} - > -
e.stopPropagation()} - > -
- SALONA - -
+ +
+ + {userData && ( +
+ +
+ )}
- - {/* دکمه خروج در پایین منوی موبایل (چسبیده به پایین) */} - {userData && ( -
+
+
+ + {/* مودال آپلود / حذف عکس پروفایل */} + {isProfilePicModalOpen && ( +
setIsProfilePicModalOpen(false)} + > +
e.stopPropagation()} + > + + +
+
+ {userData?.profile_picture ? ( + profile + ) : ( + + )} +
+

تصویر پروفایل

+

تصویر کاربری خود را مدیریت کنید

+
+ +
+ {/* Input مخفی برای دریافت فایل */} + + + +
- )} +
-
-
+ )} + ); }; diff --git a/src/lib/axios.ts b/src/lib/axios.ts index 23c474a..a73729b 100644 --- a/src/lib/axios.ts +++ b/src/lib/axios.ts @@ -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"; } }); } diff --git a/src/services/useUser.ts b/src/services/useUser.ts index c14a69e..9ac8082 100644 --- a/src/services/useUser.ts +++ b/src/services/useUser.ts @@ -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("/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 ?? "تصویر پروفایل با موفقیت حذف گردید"); + } + }, }); };