comment + favorive
This commit is contained in:
Generated
+31
@@ -33,6 +33,7 @@
|
|||||||
"react-hook-form": "^7.62.0",
|
"react-hook-form": "^7.62.0",
|
||||||
"react-infinite-scroll-component": "^6.1.0",
|
"react-infinite-scroll-component": "^6.1.0",
|
||||||
"react-leaflet": "^5.0.0",
|
"react-leaflet": "^5.0.0",
|
||||||
|
"react-multi-date-picker": "^4.5.2",
|
||||||
"react-otp-input": "^3.1.1",
|
"react-otp-input": "^3.1.1",
|
||||||
"react-spinners": "^0.17.0",
|
"react-spinners": "^0.17.0",
|
||||||
"swiper": "^11.2.10",
|
"swiper": "^11.2.10",
|
||||||
@@ -6244,6 +6245,12 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/react-dom": {
|
||||||
"version": "19.1.0",
|
"version": "19.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
||||||
@@ -6256,6 +6263,16 @@
|
|||||||
"react": "^19.1.0"
|
"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": {
|
"node_modules/react-hook-form": {
|
||||||
"version": "7.62.0",
|
"version": "7.62.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz",
|
||||||
@@ -6303,6 +6320,20 @@
|
|||||||
"react-dom": "^19.0.0"
|
"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": {
|
"node_modules/react-otp-input": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-otp-input/-/react-otp-input-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-otp-input/-/react-otp-input-3.1.1.tgz",
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
"react-hook-form": "^7.62.0",
|
"react-hook-form": "^7.62.0",
|
||||||
"react-infinite-scroll-component": "^6.1.0",
|
"react-infinite-scroll-component": "^6.1.0",
|
||||||
"react-leaflet": "^5.0.0",
|
"react-leaflet": "^5.0.0",
|
||||||
|
"react-multi-date-picker": "^4.5.2",
|
||||||
"react-otp-input": "^3.1.1",
|
"react-otp-input": "^3.1.1",
|
||||||
"react-spinners": "^0.17.0",
|
"react-spinners": "^0.17.0",
|
||||||
"swiper": "^11.2.10",
|
"swiper": "^11.2.10",
|
||||||
|
|||||||
@@ -16,6 +16,6 @@ export const verifyOtp = async (params: VerifyOtpType) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const refreshToken = async (params: RefreshTokenType) => {
|
export const refreshToken = async (params: RefreshTokenType) => {
|
||||||
const { data } = await axios.post(`/auth/refresh`, params);
|
const { data } = await axios.post(`/user/token`, params);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -265,3 +265,20 @@ textarea.place-black::placeholder {
|
|||||||
|
|
||||||
backdrop-filter: blur(44px);
|
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%;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
|
|||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import "../../public/css/fontiran.css";
|
import "../../public/css/fontiran.css";
|
||||||
import 'swiper/css';
|
import 'swiper/css';
|
||||||
|
import 'rc-rate/assets/index.css'
|
||||||
import QueryProvider from "@/config/QueryProvider";
|
import QueryProvider from "@/config/QueryProvider";
|
||||||
import ToastContainer from "@/components/Toast";
|
import ToastContainer from "@/components/Toast";
|
||||||
import MobileBottomMenu from "@/components/MobileBottomMenu";
|
import MobileBottomMenu from "@/components/MobileBottomMenu";
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { yupResolver } from '@hookform/resolvers/yup'
|
|||||||
import { Add } from 'iconsax-react'
|
import { Add } from 'iconsax-react'
|
||||||
import { PRIMARY_COLOR } from '@/config/const'
|
import { PRIMARY_COLOR } from '@/config/const'
|
||||||
import Rate from 'rc-rate'
|
import Rate from 'rc-rate'
|
||||||
import 'rc-rate/assets/index.css'
|
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
import { useParams } from 'next/navigation'
|
import { useParams } from 'next/navigation'
|
||||||
import { useAddComment } from '../hooks/useProductData'
|
import { useAddComment } from '../hooks/useProductData'
|
||||||
|
|||||||
@@ -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 * as api from "../service/Service";
|
||||||
import { ProductsResponse } from "@/types/products.types";
|
import { ProductsResponse } from "@/types/products.types";
|
||||||
|
|
||||||
@@ -36,13 +36,23 @@ export const useCategoryProductsData = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useAddWishlist = () => {
|
export const useAddWishlist = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: api.addWishlist,
|
mutationFn: api.addWishlist,
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["with-list"] });
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useRemoveWishlist = () => {
|
export const useRemoveWishlist = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: api.removeWishlist,
|
mutationFn: api.removeWishlist,
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["with-list"] });
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,85 +1,137 @@
|
|||||||
|
'use client'
|
||||||
import Layout from '@/hoc/Layout'
|
import Layout from '@/hoc/Layout'
|
||||||
import { NextPage } from 'next'
|
import { NextPage } from 'next'
|
||||||
import Menu from '../components/Menu'
|
import Menu from '../components/Menu'
|
||||||
import Image from 'next/image'
|
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 (
|
||||||
|
<div className='bg-white rounded-[20px] p-4 sm:p-6 mb-4 sm:mb-6 shadow-sm border border-border'>
|
||||||
|
<div className='flex flex-col sm:flex-row gap-4 sm:gap-6'>
|
||||||
|
<div className='flex justify-center sm:justify-start'>
|
||||||
|
<Image
|
||||||
|
src={comment.product.imagesUrl.cover || 'https://picsum.photos/200/300'}
|
||||||
|
alt={comment.product.title_fa}
|
||||||
|
width={120}
|
||||||
|
height={120}
|
||||||
|
className='max-w-[100px] sm:max-w-[120px] max-h-[100px] sm:max-h-[120px] object-contain rounded-lg'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex-1'>
|
||||||
|
<div className='flex flex-col sm:flex-row sm:items-start justify-between gap-2 mb-3'>
|
||||||
|
<h3 className='text-sm sm:text-base font-medium text-[#383E43] leading-5'>
|
||||||
|
{comment.product.title_fa}
|
||||||
|
</h3>
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<Rate value={comment.rate} disabled />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex flex-wrap items-center gap-2 text-xs text-[#999999] mb-4'>
|
||||||
|
<span>{comment.user.fullName}</span>
|
||||||
|
<span className="size-1 rounded-full bg-[#E5E5E5]"></span>
|
||||||
|
<span>{formatDate(comment.createdAt)}</span>
|
||||||
|
<span className="size-1 rounded-full bg-[#E5E5E5]"></span>
|
||||||
|
<span className='px-2 py-0.5 rounded bg-[#F1F1F1] text-[#7F7F7F]'>خریدار</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{comment.title && (
|
||||||
|
<div className='text-sm font-medium text-[#333333] mb-2'>
|
||||||
|
{comment.title}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{comment.content && (
|
||||||
|
<div className='text-xs sm:text-sm text-[#666666] leading-6 sm:leading-7 mb-3'>
|
||||||
|
{comment.content}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className='grid grid-cols-1 sm:grid-cols-2 gap-3'>
|
||||||
|
{comment.advantage && comment.advantage.length > 0 && (
|
||||||
|
<div className='bg-green-50 rounded-lg p-3'>
|
||||||
|
<div className='text-xs sm:text-sm text-green-700 font-medium mb-2'>مزایا:</div>
|
||||||
|
<ul className='text-xs sm:text-sm text-green-600 space-y-1'>
|
||||||
|
{comment.advantage.map((adv, index) => (
|
||||||
|
<li key={index} className='flex items-start gap-2'>
|
||||||
|
<span className='text-green-500 mt-0.5'>•</span>
|
||||||
|
<span>{adv}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{comment.disAdvantage && comment.disAdvantage.length > 0 && (
|
||||||
|
<div className='bg-red-50 rounded-lg p-3'>
|
||||||
|
<div className='text-xs sm:text-sm text-red-700 font-medium mb-2'>معایب:</div>
|
||||||
|
<ul className='text-xs sm:text-sm text-red-600 space-y-1'>
|
||||||
|
{comment.disAdvantage.map((disadv, index) => (
|
||||||
|
<li key={index} className='flex items-start gap-2'>
|
||||||
|
<span className='text-red-500 mt-0.5'>•</span>
|
||||||
|
<span>{disadv}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const Comments: NextPage = () => {
|
const Comments: NextPage = () => {
|
||||||
|
|
||||||
|
const { data: comments, isLoading } = useGetComments()
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <PageLoading />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!comments?.results?.comments?.length) {
|
||||||
|
return (
|
||||||
|
<div className='p-3 sm:p-4 md:p-6'>
|
||||||
|
<Menu pageActive='comments' />
|
||||||
|
<div className='mt-6 sm:mt-8 md:mt-10 lg:mt-14'>
|
||||||
|
|
||||||
|
<div className='bg-white rounded-[20px] p-8 sm:p-12 text-center shadow-sm border border-border'>
|
||||||
|
<div className='text-lg sm:text-xl text-gray-600 mb-2'>
|
||||||
|
کامنتی یافت نشد
|
||||||
|
</div>
|
||||||
|
<div className='text-sm text-gray-500'>
|
||||||
|
شما هنوز کامنتی ثبت نکردهاید
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='p-3 sm:p-4 md:p-6'>
|
<div className='p-3 sm:p-4 md:p-6'>
|
||||||
<Menu pageActive='comments' />
|
<Menu pageActive='comments' />
|
||||||
|
|
||||||
<div className='mt-6 sm:mt-8 md:mt-10 lg:mt-14'>
|
<div className='mt-6 sm:mt-8 md:mt-10 lg:mt-14'>
|
||||||
<div className='flex flex-col sm:flex-row gap-3 sm:gap-4 lg:gap-6 border-b border-border pb-4 sm:pb-6'>
|
<div className='space-y-4 sm:space-y-6'>
|
||||||
<Image
|
{comments?.results?.comments.map((comment) => (
|
||||||
src='https://picsum.photos/200/300'
|
<CommentItem key={comment._id} comment={comment} />
|
||||||
alt='comment'
|
))}
|
||||||
width={100}
|
|
||||||
height={100}
|
|
||||||
className='max-w-16 sm:max-w-20 max-h-16 sm:max-h-20 object-contain mx-auto sm:mx-0'
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className='flex-1'>
|
|
||||||
<div className='border-b border-border pb-3 sm:pb-4'>
|
|
||||||
<div className='text-sm sm:text-base'>
|
|
||||||
نان بربری سنتی سبوس دار
|
|
||||||
</div>
|
|
||||||
<div className='mt-1.5 text-xs sm:text-sm text-[#999999]'>
|
|
||||||
2 آذر 1401
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-3 sm:mt-4 text-[#4C4C4C] text-xs sm:text-sm'>
|
|
||||||
خیلی راضیم از خریدم
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='flex flex-col sm:flex-row gap-3 sm:gap-4 lg:gap-6 border-b border-border py-4 sm:py-6'>
|
|
||||||
<Image
|
|
||||||
src='https://picsum.photos/200/300'
|
|
||||||
alt='comment'
|
|
||||||
width={100}
|
|
||||||
height={100}
|
|
||||||
className='max-w-16 sm:max-w-20 max-h-16 sm:max-h-20 object-contain mx-auto sm:mx-0'
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className='flex-1'>
|
|
||||||
<div className='border-b border-border pb-3 sm:pb-4'>
|
|
||||||
<div className='text-sm sm:text-base'>
|
|
||||||
نان بربری سنتی سبوس دار
|
|
||||||
</div>
|
|
||||||
<div className='mt-1.5 text-xs sm:text-sm text-[#999999]'>
|
|
||||||
2 آذر 1401
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-3 sm:mt-4 text-[#4C4C4C] text-xs sm:text-sm'>
|
|
||||||
خیلی راضیم از خریدم
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='flex flex-col sm:flex-row gap-3 sm:gap-4 lg:gap-6 border-b border-border py-4 sm:py-6'>
|
|
||||||
<Image
|
|
||||||
src='https://picsum.photos/200/300'
|
|
||||||
alt='comment'
|
|
||||||
width={100}
|
|
||||||
height={100}
|
|
||||||
className='max-w-16 sm:max-w-20 max-h-16 sm:max-h-20 object-contain mx-auto sm:mx-0'
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className='flex-1'>
|
|
||||||
<div className='border-b border-border pb-3 sm:pb-4'>
|
|
||||||
<div className='text-sm sm:text-base'>
|
|
||||||
نان بربری سنتی سبوس دار
|
|
||||||
</div>
|
|
||||||
<div className='mt-1.5 text-xs sm:text-sm text-[#999999]'>
|
|
||||||
2 آذر 1401
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-3 sm:mt-4 text-[#4C4C4C] text-xs sm:text-sm'>
|
|
||||||
خیلی راضیم از خریدم
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,21 +1,87 @@
|
|||||||
|
'use client'
|
||||||
import Layout from "@/hoc/Layout"
|
import Layout from "@/hoc/Layout"
|
||||||
import { NextPage } from "next"
|
import { NextPage } from "next"
|
||||||
import Menu from "../components/Menu"
|
import Menu from "../components/Menu"
|
||||||
import GridWrapper from "@/components/GridWrapper"
|
import GridWrapper from "@/components/GridWrapper"
|
||||||
import ProductCard from "@/components/ProductCard"
|
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 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 (
|
return (
|
||||||
<div className="p-3 sm:p-4 md:p-6">
|
<div className="p-3 sm:p-4 md:p-6">
|
||||||
<Menu pageActive='favorite' />
|
<Menu pageActive='favorite' />
|
||||||
|
|
||||||
|
{products.length === 0 && !isLoading ? (
|
||||||
|
<div className="flex flex-col items-center justify-center mt-12 text-center">
|
||||||
|
<div className="text-gray-400 text-lg mb-4">لیست علاقهمندیهای شما خالی است</div>
|
||||||
|
<div className="text-gray-500 text-sm">محصولات مورد علاقه خود را اضافه کنید</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<GridWrapper desktop={5} mobile={2} className="mt-6 sm:mt-8 md:mt-10 lg:mt-14">
|
<GridWrapper desktop={5} mobile={2} className="mt-6 sm:mt-8 md:mt-10 lg:mt-14">
|
||||||
<ProductCard />
|
{products.map((product) => (
|
||||||
<ProductCard />
|
<ProductCard key={product._id} item={product} />
|
||||||
<ProductCard />
|
))}
|
||||||
<ProductCard />
|
|
||||||
</GridWrapper>
|
</GridWrapper>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,3 +38,17 @@ export const useUpdateAddress = () => {
|
|||||||
api.updateAddress(addressData),
|
api.updateAddress(addressData),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGetWithList = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["with-list"],
|
||||||
|
queryFn: api.getWithList,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetComments = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["comments"],
|
||||||
|
queryFn: api.getComments,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ import { Button } from '@/components/ui/button'
|
|||||||
import Input from '@/components/Input'
|
import Input from '@/components/Input'
|
||||||
import { Edit2 } from 'lucide-react'
|
import { Edit2 } from 'lucide-react'
|
||||||
import Layout from '@/hoc/Layout'
|
import Layout from '@/hoc/Layout'
|
||||||
import { Heart, ShoppingCart, Edit } from 'iconsax-react'
|
import { Heart, ShoppingCart } from 'iconsax-react'
|
||||||
import Menu from './components/Menu'
|
import Menu from './components/Menu'
|
||||||
import AddressModal from './components/AddressModal'
|
import AddressModal from './components/AddressModal'
|
||||||
import { useGetProfile, useUpdateProfile } from './hooks/useProfileData'
|
import { useGetProfile, useUpdateProfile } from './hooks/useProfileData'
|
||||||
import { UpdateAddressType } from './types/ProfileTypes'
|
import { UpdateAddressType } from './types/ProfileTypes'
|
||||||
|
import DatePickerComponent from '@/components/DatePicker'
|
||||||
|
|
||||||
// Schema validation
|
// Schema validation
|
||||||
const profileSchema = yup.object({
|
const profileSchema = yup.object({
|
||||||
@@ -146,11 +147,13 @@ const ProfilePage = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Input
|
<DatePickerComponent
|
||||||
label="تاریخ تولد"
|
label="تاریخ تولد"
|
||||||
{...register('dateOfBirth')}
|
placeholder="تاریخ تولد خود را انتخاب کنید"
|
||||||
readOnly={!isEditing}
|
onChange={(date) => setValue('dateOfBirth', date)}
|
||||||
|
defaulValue={profile?.results?.info?.dateOfBirth || ''}
|
||||||
error_text={errors.dateOfBirth?.message}
|
error_text={errors.dateOfBirth?.message}
|
||||||
|
readOnly={!isEditing}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import {
|
|||||||
UpdateProfileResponse,
|
UpdateProfileResponse,
|
||||||
AddressLocationResponse,
|
AddressLocationResponse,
|
||||||
UpdateAddressType,
|
UpdateAddressType,
|
||||||
|
WishlistResponse,
|
||||||
|
CommentsResponse,
|
||||||
} from "../types/ProfileTypes";
|
} from "../types/ProfileTypes";
|
||||||
|
|
||||||
export const getProfile = async () => {
|
export const getProfile = async () => {
|
||||||
@@ -31,3 +33,13 @@ export const updateAddress = async (addressData: UpdateAddressType) => {
|
|||||||
const { data } = await axios.post("/address/user/save", addressData);
|
const { data } = await axios.post("/address/user/save", addressData);
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getWithList = async (): Promise<WishlistResponse> => {
|
||||||
|
const { data } = await axios.get("/user/profile/wishlist");
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getComments = async (): Promise<CommentsResponse> => {
|
||||||
|
const { data } = await axios.get("/user/profile/comments");
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|||||||
@@ -90,3 +90,193 @@ export type UpdateAddressType = {
|
|||||||
address: string;
|
address: string;
|
||||||
plaque: 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[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -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: Props) => {
|
||||||
|
const [value, setValue] = useState<DateObject | null>(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 (
|
||||||
|
<div className="w-full">
|
||||||
|
<div className='text-sm'>
|
||||||
|
{props.label}
|
||||||
|
</div>
|
||||||
|
<div className={clx(
|
||||||
|
'relative mt-1.5',
|
||||||
|
props.readOnly && 'readOny'
|
||||||
|
)}>
|
||||||
|
<DatePicker
|
||||||
|
placeholder={props.placeholder}
|
||||||
|
value={value}
|
||||||
|
onChange={(date) => setValue(date as DateObject)}
|
||||||
|
calendar={persian}
|
||||||
|
locale={persian_fa}
|
||||||
|
calendarPosition="bottom-right"
|
||||||
|
className={props.className}
|
||||||
|
readOnly={props.readOnly}
|
||||||
|
/>
|
||||||
|
{props.error_text && props.error_text !== '' && (
|
||||||
|
<div className="text-xs text-right text-red-600 mt-2 mr-2 font-medium">
|
||||||
|
{props.error_text}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* <img src={CalenderIcon} className='absolute top-0 bottom-0 my-auto left-2' /> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DatePickerComponent;
|
||||||
@@ -22,7 +22,10 @@ const ProductCard: FC<Props> = ({ item }) => {
|
|||||||
const { mutate: removeWishlist } = useRemoveWishlist()
|
const { mutate: removeWishlist } = useRemoveWishlist()
|
||||||
|
|
||||||
|
|
||||||
const handleWish = () => {
|
const handleWish = (e: React.MouseEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
if (!isLogin) return
|
if (!isLogin) return
|
||||||
if (!item?._id || !item?.variants[0]?._id) return
|
if (!item?._id || !item?.variants[0]?._id) return
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user