comment + favorive

This commit is contained in:
hamid zarghami
2025-09-17 12:29:58 +03:30
parent bc22bbd6d9
commit f849ebdb42
15 changed files with 562 additions and 85 deletions
+123 -71
View File
@@ -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 (
<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 { 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 (
<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='flex flex-col sm:flex-row gap-3 sm:gap-4 lg:gap-6 border-b border-border pb-4 sm:pb-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 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 className='space-y-4 sm:space-y-6'>
{comments?.results?.comments.map((comment) => (
<CommentItem key={comment._id} comment={comment} />
))}
</div>
</div>
</div>
+72 -6
View File
@@ -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 (
<div className="p-3 sm:p-4 md:p-6">
<Menu pageActive='favorite' />
<GridWrapper desktop={5} mobile={2} className="mt-6 sm:mt-8 md:mt-10 lg:mt-14">
<ProductCard />
<ProductCard />
<ProductCard />
<ProductCard />
</GridWrapper>
{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">
{products.map((product) => (
<ProductCard key={product._id} item={product} />
))}
</GridWrapper>
)}
</div>
)
}
+14
View File
@@ -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,
});
};
+7 -4
View File
@@ -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 = () => {
</div>
<div>
<Input
<DatePickerComponent
label="تاریخ تولد"
{...register('dateOfBirth')}
readOnly={!isEditing}
placeholder="تاریخ تولد خود را انتخاب کنید"
onChange={(date) => setValue('dateOfBirth', date)}
defaulValue={profile?.results?.info?.dateOfBirth || ''}
error_text={errors.dateOfBirth?.message}
readOnly={!isEditing}
/>
</div>
</div>
+12
View File
@@ -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<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;
};
+190
View File
@@ -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[];
};
}