popover account + popover cart

This commit is contained in:
hamid zarghami
2025-09-15 15:59:45 +03:30
parent 220c8be526
commit ab29dc26dd
7 changed files with 741 additions and 13 deletions
+48
View File
@@ -0,0 +1,48 @@
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
function Popover({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />
}
function PopoverTrigger({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
}
function PopoverContent({
className,
align = "center",
sideOffset = 4,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-slot="popover-content"
align={align}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
)
}
function PopoverAnchor({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
}
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
+2 -2
View File
@@ -59,14 +59,14 @@ export const setRefreshToken = (refreshToken: string) => {
localStorage.setItem(REFRESH_TOKEN_NAME, refreshToken);
};
export const removeToken = () => {
export const removeToken = async () => {
if (typeof window === "undefined") {
return;
}
localStorage.removeItem(TOKEN_NAME);
};
export const removeRefreshToken = () => {
export const removeRefreshToken = async () => {
if (typeof window === "undefined") {
return;
}
+6 -11
View File
@@ -1,18 +1,18 @@
'use client'
import Input from '@/components/Input'
import { Separator } from '@/components/ui/separator'
import { DocumentText, Home, Profile, ShoppingCart, HambergerMenu } from 'iconsax-react'
import { DocumentText, Home, Profile, HambergerMenu } from 'iconsax-react'
import { FC, Fragment, useEffect, useState } from 'react'
import Menu from './components/Menu'
import { useSharedStore } from './store/sharedStore'
import { getToken } from '@/config/func'
import { useGetProfile } from '@/app/profile/hooks/useProfileData'
import Account from './components/Account'
import Cart from './components/Cart'
const Header: FC = () => {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
const { setIsLogin } = useSharedStore()
const { data: profile } = useGetProfile()
const handleSetIsLogin = async () => {
const token = await getToken()
@@ -49,11 +49,8 @@ const Header: FC = () => {
</div>
<div className='flex items-center gap-1 sm:gap-2 md:gap-6'>
<div className='hidden md:flex h-10 border border-border py-3 px-4 rounded-[12px] items-center gap-2.5'>
<Profile size={20} color='#333333' />
{
<div className='text-sm text-[#333333]'>{profile?.results?.info?.fullName ? profile?.results?.info?.fullName : 'حساب کاربری'}</div>
}
<div>
<Account />
</div>
<div className='md:hidden'>
@@ -62,9 +59,7 @@ const Header: FC = () => {
<Separator className='hidden md:block' style={{ height: 25 }} orientation='vertical' />
<div className='h-10 p-3 rounded-[12px] border border-border flex items-center gap-2.5'>
<ShoppingCart size={20} color='#333333' />
</div>
<Cart />
</div>
</div>
+118
View File
@@ -0,0 +1,118 @@
import { useGetProfile } from '@/app/profile/hooks/useProfileData'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
import { PRIMARY_COLOR } from '@/config/const'
import { removeToken, removeRefreshToken } from '@/config/func'
import { User, Heart, MessageText, Logout, SmsNotification, Profile } from 'iconsax-react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
const Account = () => {
const { data: profile, isSuccess } = useGetProfile()
const router = useRouter()
const handleLogout = async () => {
await removeToken()
await removeRefreshToken()
router.push('/auth')
}
const menuItems = [
{
id: 'profile',
title: 'پروفایل کاربری',
icon: User,
href: '/profile',
isActive: false
},
{
id: 'favorites',
title: 'علاقه‌مندی‌ها',
icon: Heart,
href: '/profile/favorite'
},
{
id: 'comments',
title: 'نظرات من',
icon: SmsNotification,
href: '/profile/comments'
},
{
id: 'chat',
title: 'گفتگو',
icon: MessageText,
href: '/profile/chat'
},
{
id: 'logout',
title: 'خروج از حساب',
icon: Logout,
onClick: handleLogout
}
]
return (
<Popover>
<PopoverTrigger asChild>
<div className='hidden md:flex h-10 border border-border py-3 cursor-pointer px-4 rounded-[12px] items-center gap-2.5'>
<Profile size={20} color='#333333' />
<div className='text-sm text-[#333333] '>{profile?.results?.info?.fullName ? profile?.results?.info?.fullName : 'حساب کاربری'}</div>
</div>
</PopoverTrigger>
{
isSuccess && (
<PopoverContent className="w-[200px] z-[9999] p-0" style={{ left: 4 }}>
<div className="py-2">
{menuItems.map((item) => {
const IconComponent = item.icon
if (item.id === 'logout') {
return (
<button
key={item.id}
onClick={item.onClick}
className="flex items-center gap-3 px-4 py-3 hover:bg-gray-50 transition-colors w-full text-right text-gray-700"
>
<IconComponent
size={20}
variant="Outline"
color="#666"
className="text-gray-700"
/>
<span className="text-sm font-medium">{item.title}</span>
</button>
)
}
return (
<Link
key={item.id}
href={item.href || '#'}
className={`flex items-center gap-3 px-4 py-3 hover:bg-gray-50 transition-colors ${item.isActive ? 'text-primary' : 'text-gray-700'
}`}
>
<IconComponent
size={20}
variant="Outline"
color={item.isActive ? PRIMARY_COLOR : '#666'}
className={item.isActive ? 'text-primary' : 'text-gray-700'}
/>
<span className="text-sm font-medium">{item.title}</span>
</Link>
)
})}
</div>
</PopoverContent>
)
}
</Popover>
)
}
export default Account
+127
View File
@@ -0,0 +1,127 @@
import { ShoppingCart, Trash } from 'iconsax-react'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
import { useGetCart } from '@/app/cart/hooks/useCartData'
import { useRemoveFromCart } from '@/app/cart/hooks/useCartData'
import { CartItemType } from '@/app/cart/types/Types'
import { NumberFormat } from '@/config/func'
import Image from 'next/image'
import Link from 'next/link'
import { Button } from '@/components/ui/button'
const Cart = () => {
const { data: cartData, isLoading } = useGetCart()
const removeFromCartMutation = useRemoveFromCart()
const handleRemoveItem = async (productId: number, variantId: string) => {
try {
await removeFromCartMutation.mutateAsync({
productId,
variantId
})
} catch (error) {
console.error('Error removing item from cart:', error)
}
}
const cart = cartData?.results?.cart
const items = cart?.items || []
const itemsCount = cart?.items_count || 0
return (
<Popover>
<PopoverTrigger asChild>
<div className='h-10 p-3 rounded-[12px] border border-border flex items-center gap-2.5 relative'>
<ShoppingCart size={20} color='#333333' />
{itemsCount > 0 && (
<div className='absolute -top-2 -right-2 bg-primary text-white text-xs rounded-full w-5 h-5 flex items-center justify-center'>
{itemsCount}
</div>
)}
</div>
</PopoverTrigger>
<PopoverContent className="w-[350px] ml-2 z-[9999] p-0 max-h-[600px] overflow-y-auto" style={{ left: 4 }}>
<div className='p-4'>
{/* Header */}
{
itemsCount > 0 && (
<div className='text-primary text-sm font-medium mb-4'>
{itemsCount} کالا در سبد خرید شما ثبت شده
</div>
)
}
{/* Cart Items */}
<div className='space-y-3 mb-4'>
{isLoading ? (
<div className='text-center py-4 text-gray-500'>در حال بارگذاری...</div>
) : items.length > 0 ? (
items.slice(0, 3).map((item: CartItemType) => (
<CartItemCard
key={`${item.product._id}-${item.variant._id}`}
item={item}
onRemove={() => handleRemoveItem(item.product._id, item.variant._id)}
/>
))
) : (
<div className='text-center py-4 text-gray-500'>سبد خرید شما خالی است</div>
)}
</div>
{/* View Cart Button */}
{itemsCount > 0 && (
<Link href="/cart" className='block'>
<Button className='w-full bg-primary hover:bg-primary/90 text-white rounded-lg py-3'>
مشاهده سبد خرید
</Button>
</Link>
)}
</div>
</PopoverContent>
</Popover>
)
}
// Cart Item Card Component
const CartItemCard = ({ item, onRemove }: { item: CartItemType, onRemove: () => void }) => {
return (
<div className='bg-white rounded-lg p-3 flex gap-3 shadow-sm border border-gray-100'>
{/* Product Image */}
<div className='flex-shrink-0'>
<Image
src={item.product.imagesUrl.cover}
alt={item.product.title_fa}
width={60}
height={60}
className='w-15 h-15 object-contain rounded'
/>
</div>
{/* Product Details */}
<div className='flex-1 min-w-0'>
<div className='text-xs text-gray-500 mb-1'>
{item.product.category?._id || 'دسته‌بندی'}
</div>
<div className='text-sm text-gray-800 mb-2 line-clamp-2'>
{item.product.title_fa}
</div>
<div className='flex items-center justify-between'>
<div className='text-sm font-semibold text-gray-900'>
{NumberFormat(item.variant.price.selling_price)} تومان
</div>
<button
onClick={onRemove}
className='p-1 hover:bg-red-100 rounded'
>
<Trash size={16} color='#AD3434' />
</button>
</div>
</div>
</div>
)
}
export default Cart