diff --git a/package-lock.json b/package-lock.json index 7706633..8ef3c9a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "react-hook-form": "^7.62.0", "react-infinite-scroll-component": "^6.1.0", "react-leaflet": "^5.0.0", + "react-multi-date-picker": "^4.5.2", "react-otp-input": "^3.1.1", "react-spinners": "^0.17.0", "swiper": "^11.2.10", @@ -6244,6 +6245,12 @@ "node": ">=0.10.0" } }, + "node_modules/react-date-object": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/react-date-object/-/react-date-object-2.1.9.tgz", + "integrity": "sha512-BHxD/quWOTo9fLKV/cfL/M31ePoj4a1JaJ/CnOf8Ndg3mrkh4x9wEMMkCfTrzduxDOgU8ZgR8uarhqI5G71sTg==", + "license": "MIT" + }, "node_modules/react-dom": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", @@ -6256,6 +6263,16 @@ "react": "^19.1.0" } }, + "node_modules/react-element-popper": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/react-element-popper/-/react-element-popper-2.1.7.tgz", + "integrity": "sha512-tuM2OxKlW32h+6uFSK6EENHPeZ2OGgOipHfOAl+VLWEv9/j3QkSGbD+ADX3A9uJlmq24i37n28RjJmAbGTfpEg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/react-hook-form": { "version": "7.62.0", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz", @@ -6303,6 +6320,20 @@ "react-dom": "^19.0.0" } }, + "node_modules/react-multi-date-picker": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/react-multi-date-picker/-/react-multi-date-picker-4.5.2.tgz", + "integrity": "sha512-FgWjZB3Z6IA6XpcWiLPk85PwcRUhOiYhKK42o5k672gD/n2I6rzPfQ8bUrldOIiF/Z7FfOCdH7a6FeubzqteLg==", + "license": "MIT", + "dependencies": { + "react-date-object": "^2.1.8", + "react-element-popper": "^2.1.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/react-otp-input": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/react-otp-input/-/react-otp-input-3.1.1.tgz", diff --git a/package.json b/package.json index acaf2d6..c0b4e96 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "react-hook-form": "^7.62.0", "react-infinite-scroll-component": "^6.1.0", "react-leaflet": "^5.0.0", + "react-multi-date-picker": "^4.5.2", "react-otp-input": "^3.1.1", "react-spinners": "^0.17.0", "swiper": "^11.2.10", diff --git a/src/app/auth/service/Service.ts b/src/app/auth/service/Service.ts index 535b50f..3e7d1b2 100644 --- a/src/app/auth/service/Service.ts +++ b/src/app/auth/service/Service.ts @@ -16,6 +16,6 @@ export const verifyOtp = async (params: VerifyOtpType) => { }; export const refreshToken = async (params: RefreshTokenType) => { - const { data } = await axios.post(`/auth/refresh`, params); + const { data } = await axios.post(`/user/token`, params); return data; }; diff --git a/src/app/globals.css b/src/app/globals.css index b918994..1db13e4 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -265,3 +265,20 @@ textarea.place-black::placeholder { backdrop-filter: blur(44px); } + +.rmdp-input { + min-height: 40px; + border-radius: 12px !important; + /* border: 1px solid #d0d0d0 !important; */ + font-size: 12px !important; + width: 100% !important; + margin: 0px; + padding-right: 16px !important; +} +.readOny .rmdp-input { + background-color: #f5f5f5 !important; + border: none !important; +} +.rmdp-container { + width: 100%; +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 5a7ff96..f91834f 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -2,6 +2,7 @@ import type { Metadata } from "next"; import "./globals.css"; import "../../public/css/fontiran.css"; import 'swiper/css'; +import 'rc-rate/assets/index.css' import QueryProvider from "@/config/QueryProvider"; import ToastContainer from "@/components/Toast"; import MobileBottomMenu from "@/components/MobileBottomMenu"; diff --git a/src/app/product/components/AddComment.tsx b/src/app/product/components/AddComment.tsx index 0bc6228..2369226 100644 --- a/src/app/product/components/AddComment.tsx +++ b/src/app/product/components/AddComment.tsx @@ -9,7 +9,6 @@ import { yupResolver } from '@hookform/resolvers/yup' import { Add } from 'iconsax-react' import { PRIMARY_COLOR } from '@/config/const' import Rate from 'rc-rate' -import 'rc-rate/assets/index.css' import { Textarea } from '@/components/ui/textarea' import { useParams } from 'next/navigation' import { useAddComment } from '../hooks/useProductData' diff --git a/src/app/products/hooks/useProductsData.ts b/src/app/products/hooks/useProductsData.ts index be30b70..f1c6083 100644 --- a/src/app/products/hooks/useProductsData.ts +++ b/src/app/products/hooks/useProductsData.ts @@ -1,4 +1,4 @@ -import { useMutation, useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import * as api from "../service/Service"; import { ProductsResponse } from "@/types/products.types"; @@ -36,13 +36,23 @@ export const useCategoryProductsData = ( }; export const useAddWishlist = () => { + const queryClient = useQueryClient(); + return useMutation({ mutationFn: api.addWishlist, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["with-list"] }); + }, }); }; export const useRemoveWishlist = () => { + const queryClient = useQueryClient(); + return useMutation({ mutationFn: api.removeWishlist, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["with-list"] }); + }, }); }; diff --git a/src/app/profile/comments/page.tsx b/src/app/profile/comments/page.tsx index c8d7556..e1bea30 100644 --- a/src/app/profile/comments/page.tsx +++ b/src/app/profile/comments/page.tsx @@ -1,85 +1,137 @@ +'use client' import Layout from '@/hoc/Layout' import { NextPage } from 'next' import Menu from '../components/Menu' import Image from 'next/image' +import PageLoading from '@/components/PageLoading' +import { useGetComments } from '../hooks/useProfileData' +import { Comment } from '../types/ProfileTypes' +import Rate from 'rc-rate' + +const CommentItem: React.FC<{ comment: Comment }> = ({ comment }) => { + const formatDate = (dateString: string) => { + const date = new Date(dateString) + return date.toLocaleDateString('fa-IR', { + year: 'numeric', + month: 'long', + day: 'numeric' + }) + } + + return ( +
+
+
+ {comment.product.title_fa} +
+ +
+
+

+ {comment.product.title_fa} +

+
+ +
+
+ +
+ {comment.user.fullName} + + {formatDate(comment.createdAt)} + + خریدار +
+ + {comment.title && ( +
+ {comment.title} +
+ )} + + {comment.content && ( +
+ {comment.content} +
+ )} + +
+ {comment.advantage && comment.advantage.length > 0 && ( +
+
مزایا:
+
    + {comment.advantage.map((adv, index) => ( +
  • + + {adv} +
  • + ))} +
+
+ )} + + {comment.disAdvantage && comment.disAdvantage.length > 0 && ( +
+
معایب:
+
    + {comment.disAdvantage.map((disadv, index) => ( +
  • + + {disadv} +
  • + ))} +
+
+ )} +
+
+
+
+ ) +} const Comments: NextPage = () => { + + const { data: comments, isLoading } = useGetComments() + + if (isLoading) { + return + } + + if (!comments?.results?.comments?.length) { + return ( +
+ +
+ +
+
+ کامنتی یافت نشد +
+
+ شما هنوز کامنتی ثبت نکرده‌اید +
+
+
+
+ ) + } + return (
-
- comment - -
-
-
- نان بربری سنتی سبوس دار -
-
- 2 آذر 1401 -
-
- -
- خیلی راضیم از خریدم -
-
-
-
- comment - -
-
-
- نان بربری سنتی سبوس دار -
-
- 2 آذر 1401 -
-
- -
- خیلی راضیم از خریدم -
-
-
-
- comment - -
-
-
- نان بربری سنتی سبوس دار -
-
- 2 آذر 1401 -
-
- -
- خیلی راضیم از خریدم -
-
+
+ {comments?.results?.comments.map((comment) => ( + + ))}
diff --git a/src/app/profile/favorite/page.tsx b/src/app/profile/favorite/page.tsx index dcf1acd..5185155 100644 --- a/src/app/profile/favorite/page.tsx +++ b/src/app/profile/favorite/page.tsx @@ -1,21 +1,87 @@ +'use client' import Layout from "@/hoc/Layout" import { NextPage } from "next" import Menu from "../components/Menu" import GridWrapper from "@/components/GridWrapper" import ProductCard from "@/components/ProductCard" +import { useGetWithList } from "../hooks/useProfileData" +import { IProduct } from "@/types/landing.types" +import { WishlistItem } from "../types/ProfileTypes" const FavoritePage: NextPage = () => { + + const { data: withList, isLoading } = useGetWithList() + + // تبدیل داده‌های wishlist به فرمت IProduct + const transformWishlistToProducts = (wishlistItems: WishlistItem[]): IProduct[] => { + return wishlistItems.map((item) => ({ + _id: item.product._id, + title_fa: item.product.title_fa, + title_en: item.product.title_en, + seoTitle: item.product.seoTitle, + seoDescription: item.product.seoDescription, + model: '', // این فیلد در wishlist وجود ندارد + description: item.product.description, + tags: item.product.tags, + shop: { + _id: item.variant.shop._id, + shopName: item.variant.shop.shopName, + shopDescription: item.variant.shop.shopDescription, + logo: item.variant.shop.logo, + }, + status: 'active', // فرض می‌کنیم فعال است + imagesUrl: item.product.imagesUrl, + url: item.product.url, + default_variant: { + _id: item.variant._id, + market_status: item.variant.market_status, + price: item.variant.price, + stock: item.variant.stock, + isFreeShip: item.variant.isFreeShip, + isWholeSale: item.variant.isWholeSale, + }, + variants: [{ + _id: item.variant._id, + market_status: item.variant.market_status as "Marketable" | "Unmarketable" | "OutOfStock", + price: item.variant.price, + stock: item.variant.stock, + postingTime: item.variant.postingTime, + isFreeShip: item.variant.isFreeShip, + isWholeSale: item.variant.isWholeSale, + shop: { + _id: item.variant.shop._id, + shopName: item.variant.shop.shopName, + shopDescription: item.variant.shop.shopDescription, + logo: item.variant.shop.logo, + }, + shipmentMethod: [], // این فیلد در wishlist وجود ندارد + warranty: item.variant.warranty, + meterage: item.variant.meterage ? { _id: 0, value: item.variant.meterage.toString() } : undefined, + size: item.variant.size ? { _id: 0, value: item.variant.size.toString() } : undefined, + }], + isWishlist: true, // چون در لیست علاقه‌مندی‌ها است + })) + } + + const products = withList?.results?.wishlist ? transformWishlistToProducts(withList.results.wishlist) : [] + return (
- - - - - - + {products.length === 0 && !isLoading ? ( +
+
لیست علاقه‌مندی‌های شما خالی است
+
محصولات مورد علاقه خود را اضافه کنید
+
+ ) : ( + + {products.map((product) => ( + + ))} + + )}
) } diff --git a/src/app/profile/hooks/useProfileData.ts b/src/app/profile/hooks/useProfileData.ts index 1126d4b..11b5e82 100644 --- a/src/app/profile/hooks/useProfileData.ts +++ b/src/app/profile/hooks/useProfileData.ts @@ -38,3 +38,17 @@ export const useUpdateAddress = () => { api.updateAddress(addressData), }); }; + +export const useGetWithList = () => { + return useQuery({ + queryKey: ["with-list"], + queryFn: api.getWithList, + }); +}; + +export const useGetComments = () => { + return useQuery({ + queryKey: ["comments"], + queryFn: api.getComments, + }); +}; diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx index 82742d5..b035d75 100644 --- a/src/app/profile/page.tsx +++ b/src/app/profile/page.tsx @@ -7,11 +7,12 @@ import { Button } from '@/components/ui/button' import Input from '@/components/Input' import { Edit2 } from 'lucide-react' import Layout from '@/hoc/Layout' -import { Heart, ShoppingCart, Edit } from 'iconsax-react' +import { Heart, ShoppingCart } from 'iconsax-react' import Menu from './components/Menu' import AddressModal from './components/AddressModal' import { useGetProfile, useUpdateProfile } from './hooks/useProfileData' import { UpdateAddressType } from './types/ProfileTypes' +import DatePickerComponent from '@/components/DatePicker' // Schema validation const profileSchema = yup.object({ @@ -146,11 +147,13 @@ const ProfilePage = () => {
- setValue('dateOfBirth', date)} + defaulValue={profile?.results?.info?.dateOfBirth || ''} error_text={errors.dateOfBirth?.message} + readOnly={!isEditing} />
diff --git a/src/app/profile/service/Service.ts b/src/app/profile/service/Service.ts index aec687c..ea4db5a 100644 --- a/src/app/profile/service/Service.ts +++ b/src/app/profile/service/Service.ts @@ -5,6 +5,8 @@ import { UpdateProfileResponse, AddressLocationResponse, UpdateAddressType, + WishlistResponse, + CommentsResponse, } from "../types/ProfileTypes"; export const getProfile = async () => { @@ -31,3 +33,13 @@ export const updateAddress = async (addressData: UpdateAddressType) => { const { data } = await axios.post("/address/user/save", addressData); return data; }; + +export const getWithList = async (): Promise => { + const { data } = await axios.get("/user/profile/wishlist"); + return data; +}; + +export const getComments = async (): Promise => { + const { data } = await axios.get("/user/profile/comments"); + return data; +}; diff --git a/src/app/profile/types/ProfileTypes.ts b/src/app/profile/types/ProfileTypes.ts index 9e5552f..2310ca6 100644 --- a/src/app/profile/types/ProfileTypes.ts +++ b/src/app/profile/types/ProfileTypes.ts @@ -90,3 +90,193 @@ export type UpdateAddressType = { address: string; plaque: string; }; + +// Wishlist Types +export interface WishlistUser { + _id: string; + fullName: string; + phoneNumber: string; + dateOfBirth: string; + address: { + _id: string; + }; + nationalCode: string; + homeNumber: string | null; +} + +export interface WishlistBrand { + _id: string; +} + +export interface WishlistCategory { + _id: string; +} + +export interface WishlistSpecification { + title: string; + values: string[]; +} + +export interface WishlistImagesUrl { + cover: string; + list: string[]; +} + +export interface WishlistProduct { + _id: number; + url: string; + title_fa: string; + title_en: string; + seoTitle: string | null; + seoDescription: string | null; + source: string; + description: string; + metaDescription: string; + tags: string[]; + advantages: string[]; + disAdvantages: string[]; + imagesUrl: WishlistImagesUrl; + isFake: string; + voice: string | null; + specifications: WishlistSpecification[]; + brand: WishlistBrand; + category: WishlistCategory; +} + +export interface WishlistShop { + _id: string; + shopName: string; + shopCode: number; + owner: string; + shopDescription: string; + telephoneNumber: string; + isChatActive: boolean; + shopPostalCode: string; + logo: string; +} + +export interface WishlistWarranty { + _id: number; + duration: string; + logoUrl: string; + name: string; +} + +export interface WishlistPrice { + order_limit: number; + retailPrice: number; + selling_price: number; + is_specialSale: boolean; + discount_percent: number; + specialSale_order_limit: number | null; + specialSale_quantity: number | null; + specialSale_endDate: string | null; +} + +export interface WishlistVariant { + _id: string; + market_status: string; + price: WishlistPrice; + stock: number; + postingTime: number; + isFreeShip: boolean; + isWholeSale: boolean; + shop: WishlistShop; + warranty: WishlistWarranty; + size?: number; + meterage?: number; +} + +export interface WishlistItem { + _id: string; + user: WishlistUser; + product: WishlistProduct; + variant: WishlistVariant; +} + +export interface WishlistResponse { + status: number; + success: boolean; + results: { + wishlist: WishlistItem[]; + }; +} + +// Comments Types +export interface CommentUser { + fullName: string; +} + +export interface CommentBrand { + _id: string; + status: string; + title_en: string; + title_fa: string; + images: string[]; + logoUrl: string; +} + +export interface CommentCategory { + _id: string; + title_fa: string; + title_en: string; + icon: string; + imageUrl: string; + theme: string; + description: string; + leaf: boolean; + parent: string; +} + +export interface CommentSpecification { + title: string; + values: string[]; +} + +export interface CommentImagesUrl { + cover: string; + list: string[]; +} + +export interface CommentProduct { + _id: number; + url: string; + title_fa: string; + title_en: string; + seoTitle: string; + seoDescription: string | null; + source: string; + description: string; + metaDescription: string; + tags: string[]; + advantages: string[]; + disAdvantages: string[]; + imagesUrl: CommentImagesUrl; + isFake: string; + voice: string | null; + specifications: CommentSpecification[]; + brand: CommentBrand; + category: CommentCategory; + createdAt: string; +} + +export interface Comment { + _id: string; + title: string; + advantage: string[]; + disAdvantage: string[]; + content: string; + status: string; + rate: number; + user: CommentUser; + product: CommentProduct; + createdAt: string; +} + +export interface CommentsResponse { + status: number; + success: boolean; + results: { + comments: Comment[]; + }; +} diff --git a/src/components/DatePicker.tsx b/src/components/DatePicker.tsx new file mode 100644 index 0000000..b94d9ce --- /dev/null +++ b/src/components/DatePicker.tsx @@ -0,0 +1,78 @@ +import { useState, useEffect, FC } from 'react'; +import DatePicker from 'react-multi-date-picker'; +import persian from 'react-date-object/calendars/persian'; +import persian_fa from 'react-date-object/locales/persian_fa'; +import DateObject from 'react-date-object'; +import { clx } from '../helpers/utils'; + +type Props = { + onChange: (date: string) => void; + defaulValue?: string; + error_text?: string; + placeholder: string; + reset?: boolean; + isDateTime?: boolean; + className?: string; + label: string, + readOnly?: boolean; +}; + +const DatePickerComponent: FC = (props: Props) => { + const [value, setValue] = useState(null); + + useEffect(() => { + if (props.reset) { + setValue(null); + } + }, [props.reset]); + + useEffect(() => { + if (value) { + const formattedDate = `${value.year}/${value.month.number}/${value.day}`; + props.onChange(formattedDate); + } + }, [value]); + + useEffect(() => { + if (props.defaulValue && !value) { + const defaultDate = new DateObject({ + date: props.defaulValue, + calendar: persian, + locale: persian_fa, + }); + setValue(defaultDate); + } + }, [props.defaulValue, value]); + + return ( +
+
+ {props.label} +
+
+ setValue(date as DateObject)} + calendar={persian} + locale={persian_fa} + calendarPosition="bottom-right" + className={props.className} + readOnly={props.readOnly} + /> + {props.error_text && props.error_text !== '' && ( +
+ {props.error_text} +
+ )} + + {/* */} +
+
+ ); +}; + +export default DatePickerComponent; \ No newline at end of file diff --git a/src/components/ProductCard.tsx b/src/components/ProductCard.tsx index 5e923eb..48bb4fa 100644 --- a/src/components/ProductCard.tsx +++ b/src/components/ProductCard.tsx @@ -22,7 +22,10 @@ const ProductCard: FC = ({ item }) => { const { mutate: removeWishlist } = useRemoveWishlist() - const handleWish = () => { + const handleWish = (e: React.MouseEvent) => { + e.preventDefault() + e.stopPropagation() + if (!isLogin) return if (!item?._id || !item?.variants[0]?._id) return