hide chat
This commit is contained in:
Generated
+137
-1
@@ -38,6 +38,7 @@
|
|||||||
"react-otp-input": "^3.1.1",
|
"react-otp-input": "^3.1.1",
|
||||||
"react-spinners": "^0.17.0",
|
"react-spinners": "^0.17.0",
|
||||||
"recharts": "^3.2.1",
|
"recharts": "^3.2.1",
|
||||||
|
"socket.io-client": "^4.8.1",
|
||||||
"swiper": "^11.2.10",
|
"swiper": "^11.2.10",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"yup": "^1.7.0",
|
"yup": "^1.7.0",
|
||||||
@@ -1881,6 +1882,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@socket.io/component-emitter": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@standard-schema/spec": {
|
"node_modules/@standard-schema/spec": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
|
||||||
@@ -3849,6 +3856,45 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/engine.io-client": {
|
||||||
|
"version": "6.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
|
||||||
|
"integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.1",
|
||||||
|
"engine.io-parser": "~5.2.1",
|
||||||
|
"ws": "~8.17.1",
|
||||||
|
"xmlhttprequest-ssl": "~2.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io-client/node_modules/debug": {
|
||||||
|
"version": "4.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
|
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io-parser": {
|
||||||
|
"version": "5.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
|
||||||
|
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/enhanced-resolve": {
|
"node_modules/enhanced-resolve": {
|
||||||
"version": "5.18.2",
|
"version": "5.18.2",
|
||||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz",
|
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz",
|
||||||
@@ -6047,7 +6093,6 @@
|
|||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
@@ -7214,6 +7259,68 @@
|
|||||||
"is-arrayish": "^0.3.1"
|
"is-arrayish": "^0.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/socket.io-client": {
|
||||||
|
"version": "4.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
|
||||||
|
"integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.2",
|
||||||
|
"engine.io-client": "~6.6.1",
|
||||||
|
"socket.io-parser": "~4.2.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-client/node_modules/debug": {
|
||||||
|
"version": "4.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
|
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-parser": {
|
||||||
|
"version": "4.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
|
||||||
|
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-parser/node_modules/debug": {
|
||||||
|
"version": "4.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
|
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
@@ -7997,6 +8104,35 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.17.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||||
|
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xmlhttprequest-ssl": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yallist": {
|
"node_modules/yallist": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
"react-otp-input": "^3.1.1",
|
"react-otp-input": "^3.1.1",
|
||||||
"react-spinners": "^0.17.0",
|
"react-spinners": "^0.17.0",
|
||||||
"recharts": "^3.2.1",
|
"recharts": "^3.2.1",
|
||||||
|
"socket.io-client": "^4.8.1",
|
||||||
"swiper": "^11.2.10",
|
"swiper": "^11.2.10",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"yup": "^1.7.0",
|
"yup": "^1.7.0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Truck, Sort, Flash } from 'iconsax-react'
|
import { Truck, Sort, Flash } from 'iconsax-react'
|
||||||
import { ShopShippingType, ShipmentInfoType } from '../types/Types'
|
import { ShopShippingType, ShipmentInfoType, ShipperType } from '../types/Types'
|
||||||
|
|
||||||
interface ShippingMethodSelectorProps {
|
interface ShippingMethodSelectorProps {
|
||||||
shippingData: ShopShippingType[]
|
shippingData: ShopShippingType[]
|
||||||
@@ -27,18 +27,20 @@ const ShippingMethodSelector = ({
|
|||||||
const [expandedShops, setExpandedShops] = useState<Set<string>>(new Set())
|
const [expandedShops, setExpandedShops] = useState<Set<string>>(new Set())
|
||||||
|
|
||||||
// تابع مرتبسازی روشهای ارسال
|
// تابع مرتبسازی روشهای ارسال
|
||||||
const sortShippers = (shippers: any[]) => {
|
const sortShippers = (shippers: ShipperType[]) => {
|
||||||
if (sortBy === 'default') return shippers
|
if (sortBy === 'default') return shippers
|
||||||
|
|
||||||
return [...shippers].sort((a, b) => {
|
return [...shippers].sort((a, b) => {
|
||||||
let aValue, bValue
|
let aValue: number, bValue: number
|
||||||
|
|
||||||
if (sortBy === 'price') {
|
if (sortBy === 'price') {
|
||||||
aValue = a.totalShippingCost
|
aValue = a.totalShippingCost
|
||||||
bValue = b.totalShippingCost
|
bValue = b.totalShippingCost
|
||||||
} else if (sortBy === 'time') {
|
} else if (sortBy === 'time') {
|
||||||
aValue = a.shippingDaysRange?.[0] || 0
|
aValue = parseInt(a.shippingDaysRange?.[0] || '0')
|
||||||
bValue = b.shippingDaysRange?.[0] || 0
|
bValue = parseInt(b.shippingDaysRange?.[0] || '0')
|
||||||
|
} else {
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sortOrder === 'asc') {
|
if (sortOrder === 'asc') {
|
||||||
@@ -256,8 +258,8 @@ const ShippingMethodSelector = ({
|
|||||||
<label
|
<label
|
||||||
key={shipper.shipperId}
|
key={shipper.shipperId}
|
||||||
className={`flex items-center justify-between p-3 border rounded-lg cursor-pointer transition-colors ${selectedShipments[shop.shopId] === shipper.shipperId
|
className={`flex items-center justify-between p-3 border rounded-lg cursor-pointer transition-colors ${selectedShipments[shop.shopId] === shipper.shipperId
|
||||||
? 'border-primary bg-primary/5'
|
? 'border-primary bg-primary/5'
|
||||||
: 'border-gray-200 hover:bg-gray-50'
|
: 'border-gray-200 hover:bg-gray-50'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
import { BoxTick, I3DRotate } from 'iconsax-react'
|
import { BoxTick, I3DRotate, Messages1 } from 'iconsax-react'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Product } from '@/types/product.types'
|
import { Product } from '@/types/product.types'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import Cart from './Cart'
|
import Cart from './Cart'
|
||||||
import { PRIMARY_COLOR } from '@/config/const'
|
import { PRIMARY_COLOR } from '@/config/const'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
|
||||||
interface ShopInformationProps {
|
interface ShopInformationProps {
|
||||||
product: Product
|
product: Product
|
||||||
}
|
}
|
||||||
|
|
||||||
const ShopInformation = ({ product }: ShopInformationProps) => {
|
const ShopInformation = ({ product }: ShopInformationProps) => {
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
const defaultVariant = product.default_variant
|
const defaultVariant = product.default_variant
|
||||||
const shop = defaultVariant.shop
|
const shop = defaultVariant.shop
|
||||||
@@ -27,6 +29,11 @@ const ShopInformation = ({ product }: ShopInformationProps) => {
|
|||||||
const discountPercent = price.discount_percent
|
const discountPercent = price.discount_percent
|
||||||
const hasDiscount = discountPercent > 0
|
const hasDiscount = discountPercent > 0
|
||||||
|
|
||||||
|
const handleChatWithSeller = () => {
|
||||||
|
// Navigate to chat page with product and shop context
|
||||||
|
router.push(`/profile/chat?productId=${product._id}&shopId=${shop._id}`)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full xl:w-[325px] bg-[#FAFAFA] rounded-[10px] p-4 sm:p-6 lg:p-7 border border-border h-fit xl:sticky xl:top-[170px] xl:self-start'>
|
<div className='w-full xl:w-[325px] bg-[#FAFAFA] rounded-[10px] p-4 sm:p-6 lg:p-7 border border-border h-fit xl:sticky xl:top-[170px] xl:self-start'>
|
||||||
<div className='text-lg text-[#191919]'>
|
<div className='text-lg text-[#191919]'>
|
||||||
@@ -98,7 +105,11 @@ const ShopInformation = ({ product }: ShopInformationProps) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Cart />
|
<Cart />
|
||||||
<Button className='w-full mt-3 bg-[#323232]'>
|
<Button
|
||||||
|
onClick={handleChatWithSeller}
|
||||||
|
className='w-full mt-3 bg-[#323232] hover:bg-[#424242] flex items-center justify-center gap-2'
|
||||||
|
>
|
||||||
|
<Messages1 size={18} />
|
||||||
گفتگو با فروشنده
|
گفتگو با فروشنده
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,496 @@
|
|||||||
|
'use client'
|
||||||
|
import { useState, useRef, useEffect, useMemo } from 'react'
|
||||||
|
import { Trash, More, EmojiHappy, AttachCircle, Send, Shop, Call, VideoPlay } from 'iconsax-react'
|
||||||
|
import { useSocket } from '../hooks/useSocket'
|
||||||
|
import { Message as SocketMessage } from '../service/SocketService'
|
||||||
|
import { useChatMessages, useMarkChatAsRead } from '../../hooks/useChatData'
|
||||||
|
import { isAuthenticated } from '@/config/func'
|
||||||
|
import { ChatMessage as APIMessage } from '../../types/ProfileTypes'
|
||||||
|
|
||||||
|
interface Message {
|
||||||
|
id: string
|
||||||
|
text: string
|
||||||
|
time: string
|
||||||
|
sender: 'user' | 'admin'
|
||||||
|
status?: 'sent' | 'delivered' | 'read'
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChatMessagesProps {
|
||||||
|
productId?: string | null
|
||||||
|
shopId?: string | null
|
||||||
|
chatId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ChatMessages({ productId, shopId, chatId }: ChatMessagesProps) {
|
||||||
|
const [messageText, setMessageText] = useState('')
|
||||||
|
const [debugInfo, setDebugInfo] = useState<string>('')
|
||||||
|
const [isTyping, setIsTyping] = useState(false)
|
||||||
|
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
||||||
|
const [isUserAuthenticated, setIsUserAuthenticated] = useState<boolean | null>(null)
|
||||||
|
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||||
|
const typingTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
// Server capabilities - temporarily disable features not implemented yet
|
||||||
|
const SERVER_CAPABILITIES = {
|
||||||
|
socketChat: false, // Socket.IO chat not implemented yet
|
||||||
|
markAsRead: false, // markAsRead endpoint not implemented yet
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate currentChatId from props
|
||||||
|
const currentChatId = chatId || (productId && shopId ? `chat_${shopId}_${productId}` : '')
|
||||||
|
|
||||||
|
const { data: chatData, isLoading, error } = useChatMessages(currentChatId, {
|
||||||
|
enabled: isUserAuthenticated === true // Only fetch if user is logged in
|
||||||
|
})
|
||||||
|
|
||||||
|
// Check authentication on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const checkAuth = async () => {
|
||||||
|
const authenticated = await isAuthenticated()
|
||||||
|
setIsUserAuthenticated(authenticated)
|
||||||
|
}
|
||||||
|
checkAuth()
|
||||||
|
}, [])
|
||||||
|
const markAsRead = useMarkChatAsRead()
|
||||||
|
|
||||||
|
const {
|
||||||
|
isConnected,
|
||||||
|
joinChat,
|
||||||
|
leaveChat,
|
||||||
|
sendMessage,
|
||||||
|
onNewMessage,
|
||||||
|
onError
|
||||||
|
} = useSocket()
|
||||||
|
|
||||||
|
// Convert API messages to component format
|
||||||
|
const messages: Message[] = useMemo(() =>
|
||||||
|
chatData?.results?.messages?.map((msg: APIMessage) => ({
|
||||||
|
id: msg._id,
|
||||||
|
text: msg.content,
|
||||||
|
time: new Date(msg.createdAt).toLocaleTimeString('fa-IR', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
}),
|
||||||
|
sender: msg.sender === 'customer' ? 'user' : 'admin',
|
||||||
|
status: msg.status
|
||||||
|
})) || []
|
||||||
|
, [chatData?.results?.messages])
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
scrollToBottom()
|
||||||
|
}, [messages])
|
||||||
|
|
||||||
|
// Join chat room when chatId is set and socket is connected (only if server supports it and user is logged in)
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentChatId && currentChatId !== '' && isUserAuthenticated === true) {
|
||||||
|
if (SERVER_CAPABILITIES.socketChat && isConnected) {
|
||||||
|
console.log('🔗 اتصال به چت:', currentChatId)
|
||||||
|
joinChat(currentChatId)
|
||||||
|
return () => {
|
||||||
|
leaveChat(currentChatId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark messages as read only if server supports it
|
||||||
|
if (SERVER_CAPABILITIES.markAsRead) {
|
||||||
|
markAsRead.mutate(currentChatId)
|
||||||
|
} else {
|
||||||
|
console.log('ℹ️ markAsRead غیرفعال است (سرور هنوز پشتیبانی نمیکند)')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [currentChatId, isConnected, joinChat, leaveChat, markAsRead, SERVER_CAPABILITIES.socketChat, SERVER_CAPABILITIES.markAsRead, isUserAuthenticated])
|
||||||
|
|
||||||
|
// Listen for new messages
|
||||||
|
useEffect(() => {
|
||||||
|
const handleNewMessage = (socketMessage: unknown) => {
|
||||||
|
const message = socketMessage as SocketMessage
|
||||||
|
if (message.chatId === currentChatId) {
|
||||||
|
// The message will be added via React Query invalidation
|
||||||
|
// or we can update local state if needed
|
||||||
|
console.log('New message received:', message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleError = (error: unknown) => {
|
||||||
|
const err = error as { message: string }
|
||||||
|
console.error('🚨 خطای چت:', err.message)
|
||||||
|
setDebugInfo(`خطا: ${err.message}`)
|
||||||
|
// You can show a toast notification here
|
||||||
|
if (err.message === 'اتصال سوکت برقرار نیست') {
|
||||||
|
console.warn('⚠️ راهنمایی: اگر این خطا تکرار میشود، احتمالاً سرور سوکت اجرا نمیشود یا قابل دسترسی نیست.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onNewMessage(handleNewMessage)
|
||||||
|
onError(handleError)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// Cleanup will be handled by the hook
|
||||||
|
}
|
||||||
|
}, [currentChatId, onNewMessage, onError])
|
||||||
|
|
||||||
|
const handleTyping = () => {
|
||||||
|
if (!isTyping) {
|
||||||
|
setIsTyping(true)
|
||||||
|
// Here you could emit a typing event to the server
|
||||||
|
// socketService.emit('typing', { chatId: currentChatId })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear previous timeout
|
||||||
|
if (typingTimeoutRef.current) {
|
||||||
|
clearTimeout(typingTimeoutRef.current)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set new timeout to stop typing indicator
|
||||||
|
typingTimeoutRef.current = setTimeout(() => {
|
||||||
|
setIsTyping(false)
|
||||||
|
// socketService.emit('stopTyping', { chatId: currentChatId })
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = event.target.files?.[0]
|
||||||
|
if (file) {
|
||||||
|
setSelectedFile(file)
|
||||||
|
console.log('📎 فایل انتخاب شد:', file.name, file.size, 'bytes')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAttachClick = () => {
|
||||||
|
fileInputRef.current?.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSend = () => {
|
||||||
|
if ((messageText.trim() || selectedFile) && currentChatId) {
|
||||||
|
const content = messageText.trim()
|
||||||
|
setMessageText('')
|
||||||
|
setIsTyping(false)
|
||||||
|
setSelectedFile(null)
|
||||||
|
|
||||||
|
// Clear typing timeout
|
||||||
|
if (typingTimeoutRef.current) {
|
||||||
|
clearTimeout(typingTimeoutRef.current)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For now, just send text message
|
||||||
|
// TODO: Implement file upload when server supports it
|
||||||
|
if (content) {
|
||||||
|
sendMessage(currentChatId, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll to bottom after sending
|
||||||
|
setTimeout(scrollToBottom, 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const chat = chatData?.results?.chat
|
||||||
|
const seller = chat?.participants?.seller
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white flex flex-col h-full">
|
||||||
|
{/* Debug Info - فقط در محیط توسعه */}
|
||||||
|
{process.env.NODE_ENV === 'development' && (
|
||||||
|
<div className="px-4 py-2 bg-yellow-50 border-b border-yellow-200 text-xs text-yellow-800">
|
||||||
|
<div>🔍 دیباگ چت:</div>
|
||||||
|
<div>ChatId: {currentChatId || 'خالی'}</div>
|
||||||
|
<div>ProductId: {productId || 'خالی'}</div>
|
||||||
|
<div>ShopId: {shopId || 'خالی'}</div>
|
||||||
|
<div>Socket: {isConnected ? '🟢 متصل' : '🔴 قطع'}</div>
|
||||||
|
<div>Server Capabilities: Socket={SERVER_CAPABILITIES.socketChat ? '✅' : '❌'}, MarkAsRead={SERVER_CAPABILITIES.markAsRead ? '✅' : '❌'}</div>
|
||||||
|
{debugInfo && <div className="mt-1 text-red-600">{debugInfo}</div>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div className="px-6 py-4 border-b border-gray-100 bg-white">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 bg-gray-200 rounded-full animate-pulse"></div>
|
||||||
|
<div>
|
||||||
|
<div className="w-20 h-4 bg-gray-200 rounded animate-pulse mb-1"></div>
|
||||||
|
<div className="w-12 h-3 bg-gray-200 rounded animate-pulse"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : error ? (
|
||||||
|
<div className="text-center text-red-500">
|
||||||
|
<p className="text-sm">خطا در بارگذاری چت</p>
|
||||||
|
</div>
|
||||||
|
) : seller ? (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="relative">
|
||||||
|
<div className="w-10 h-10 bg-gradient-to-br from-primary/10 to-primary/20 rounded-full flex items-center justify-center overflow-hidden">
|
||||||
|
{seller.logo ? (
|
||||||
|
<img
|
||||||
|
src={seller.logo}
|
||||||
|
alt={seller.shopName}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Shop size={18} className="text-primary" variant="Bold" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{chat?.isOnline && (
|
||||||
|
<div className="absolute -bottom-0.5 -right-0.5 w-3 h-3 bg-green-500 border-2 border-white rounded-full"></div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-gray-900 text-sm">{seller.shopName}</h3>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
{chat?.isOnline ? 'آنلاین' : 'آفلاین'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors">
|
||||||
|
<Call size={18} />
|
||||||
|
</button>
|
||||||
|
<button className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors">
|
||||||
|
<VideoPlay size={18} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
// TODO: Implement delete chat functionality
|
||||||
|
console.log('🗑️ درخواست حذف چت:', currentChatId)
|
||||||
|
if (confirm('آیا از حذف این گفتگو مطمئن هستید؟')) {
|
||||||
|
// deleteChatMutation.mutate(currentChatId)
|
||||||
|
alert('این قابلیت هنوز پیادهسازی نشده است.')
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="p-2 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<Trash size={18} />
|
||||||
|
</button>
|
||||||
|
<button className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors">
|
||||||
|
<More size={18} className="rotate-90" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center text-gray-500">
|
||||||
|
<p className="text-sm">چتی انتخاب نشده</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Messages Area */}
|
||||||
|
<div className="flex-1 overflow-y-auto bg-gray-50/30">
|
||||||
|
{/* Server Status Alert */}
|
||||||
|
{(!SERVER_CAPABILITIES.socketChat || !isConnected) && (
|
||||||
|
<div className="px-6 py-3">
|
||||||
|
<div className="bg-blue-50 border border-blue-200 rounded-lg px-4 py-3">
|
||||||
|
<div className="text-xs text-blue-800 leading-relaxed">
|
||||||
|
<div className="font-semibold mb-2">ℹ️ راهاندازی سرویس چت</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div>• سرور Socket.IO: پیادهسازی نشده</div>
|
||||||
|
<div>• API markAsRead: پیادهسازی نشده</div>
|
||||||
|
<div>• وضعیت: در حال توسعه توسط تیم بکند</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 text-xs opacity-75">
|
||||||
|
چت در حالت آزمایشی کار میکند
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Alert */}
|
||||||
|
<div className="px-6 py-3">
|
||||||
|
<div className="bg-amber-50 border border-amber-200 rounded-lg px-4 py-3">
|
||||||
|
<p className="text-xs text-amber-800 leading-relaxed">
|
||||||
|
لطفا از ارسال اطلاعات مهم، شخصی و یا نامربوط بپرهیزید. گفتگوهای شما محرمانه است.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Messages */}
|
||||||
|
<div className="px-6 pb-4 space-y-4">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{[1, 2, 3].map((i) => (
|
||||||
|
<div key={i} className={`flex ${i % 2 === 0 ? 'justify-start' : 'justify-end'}`}>
|
||||||
|
<div className="max-w-[70%]">
|
||||||
|
<div className="w-16 h-4 bg-gray-200 rounded animate-pulse mb-2"></div>
|
||||||
|
<div className={`rounded-2xl px-4 py-3 ${i % 2 === 0 ? 'bg-white' : 'bg-primary'} animate-pulse`}>
|
||||||
|
<div className="w-32 h-4 bg-gray-100 rounded"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : error ? (
|
||||||
|
<div className="text-center text-red-500 py-8">
|
||||||
|
<p className="text-sm">خطا در بارگذاری پیامها</p>
|
||||||
|
</div>
|
||||||
|
) : messages.length > 0 ? (
|
||||||
|
messages.map((message, index) => {
|
||||||
|
const isFirstInGroup = index === 0 || messages[index - 1].sender !== message.sender
|
||||||
|
const senderName = message.sender === 'admin' ?
|
||||||
|
(seller?.shopName || 'فروشگاه') : 'شما'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={message.id}
|
||||||
|
className={`flex ${message.sender === 'admin' ? 'justify-start' : 'justify-end'}`}
|
||||||
|
>
|
||||||
|
<div className={`max-w-[70%] ${message.sender === 'admin' ? 'mr-0' : 'ml-0'}`}>
|
||||||
|
{isFirstInGroup && (
|
||||||
|
<div className={`flex items-center gap-2 mb-2 ${message.sender === 'admin' ? 'justify-start' : 'justify-end'}`}>
|
||||||
|
<span className="text-xs text-gray-500 font-medium">
|
||||||
|
{senderName}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-gray-400">{message.time}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`rounded-2xl px-4 py-3 shadow-sm ${message.sender === 'admin'
|
||||||
|
? 'bg-white text-gray-800 border border-gray-100'
|
||||||
|
: 'bg-primary text-primary-foreground'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<p className="text-sm leading-relaxed">{message.text}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{message.sender === 'user' && (
|
||||||
|
<div className="flex justify-end mt-1">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span className="text-xs text-gray-400">{message.time}</span>
|
||||||
|
{message.status === 'read' && (
|
||||||
|
<div className="flex -space-x-1">
|
||||||
|
<div className="w-3 h-3 bg-blue-500 rounded-full flex items-center justify-center">
|
||||||
|
<div className="w-1 h-1 bg-white rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
<div className="w-3 h-3 bg-blue-500 rounded-full flex items-center justify-center">
|
||||||
|
<div className="w-1 h-1 bg-white rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<div className="text-center text-gray-500 py-8">
|
||||||
|
<p className="text-sm">هنوز پیامی ارسال نشده</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Typing Indicator */}
|
||||||
|
{isTyping && (
|
||||||
|
<div className="flex justify-start mb-4">
|
||||||
|
<div className="max-w-[70%]">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<span className="text-xs text-gray-500 font-medium">
|
||||||
|
{seller?.shopName || 'فروشگاه'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white border border-gray-100 rounded-2xl px-4 py-3 shadow-sm">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0ms' }}></div>
|
||||||
|
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '150ms' }}></div>
|
||||||
|
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '300ms' }}></div>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-gray-500 ml-2">در حال تایپ...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Input Area */}
|
||||||
|
<div className="border-t border-gray-100 px-6 py-4 bg-white">
|
||||||
|
<div className="flex items-end gap-3">
|
||||||
|
<button
|
||||||
|
onClick={handleAttachClick}
|
||||||
|
className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors flex-shrink-0"
|
||||||
|
>
|
||||||
|
<AttachCircle size={20} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Hidden file input */}
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept="image/*,video/*,audio/*,application/*,text/*"
|
||||||
|
onChange={handleFileSelect}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex-1 relative">
|
||||||
|
{/* File Preview */}
|
||||||
|
{selectedFile && (
|
||||||
|
<div className="mb-2 p-2 bg-blue-50 border border-blue-200 rounded-lg flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-8 h-8 bg-blue-100 rounded flex items-center justify-center">
|
||||||
|
📎
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-medium text-blue-900">{selectedFile.name}</p>
|
||||||
|
<p className="text-xs text-blue-600">{(selectedFile.size / 1024).toFixed(1)} KB</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedFile(null)}
|
||||||
|
className="text-blue-500 hover:text-blue-700 p-1"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
value={messageText}
|
||||||
|
onChange={(e) => {
|
||||||
|
setMessageText(e.target.value)
|
||||||
|
handleTyping()
|
||||||
|
}}
|
||||||
|
onKeyPress={(e) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
handleSend()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder={selectedFile ? "توضیحات فایل را بنویسید..." : "پیام خود را بنویسید..."}
|
||||||
|
rows={1}
|
||||||
|
className="w-full resize-none bg-gray-50 border border-gray-200 rounded-xl px-4 py-3 pr-12 text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent max-h-32"
|
||||||
|
style={{ minHeight: '44px' }}
|
||||||
|
/>
|
||||||
|
<button className="absolute left-2 top-1/2 -translate-y-1/2 p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-200 rounded-lg transition-colors">
|
||||||
|
<EmojiHappy size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleSend}
|
||||||
|
disabled={(!messageText.trim() && !selectedFile) || (!SERVER_CAPABILITIES.socketChat && !isConnected)}
|
||||||
|
className={`p-3 rounded-xl transition-all flex-shrink-0 ${(messageText.trim() || selectedFile) && (SERVER_CAPABILITIES.socketChat || isConnected)
|
||||||
|
? 'bg-primary hover:bg-primary/90 text-primary-foreground shadow-lg'
|
||||||
|
: 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Send size={18} className="rotate-180" variant="Bold" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xs text-gray-400 mt-2 text-center">
|
||||||
|
پیام شما بعد از ارسال قابل حذف نیست
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
'use client'
|
||||||
|
import Image from 'next/image'
|
||||||
|
import { ShoppingCart } from 'iconsax-react'
|
||||||
|
import { useGetDetailProduct } from '@/app/product/hooks/useProductData'
|
||||||
|
|
||||||
|
interface ChatProductCardProps {
|
||||||
|
productId?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ChatProductCard({ productId }: ChatProductCardProps) {
|
||||||
|
const { data: productData, isLoading, error } = useGetDetailProduct(productId || '')
|
||||||
|
const product = productData?.results?.product
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="w-full h-32 mb-4 bg-gray-200 rounded-lg animate-pulse"></div>
|
||||||
|
<div className="h-4 bg-gray-200 rounded animate-pulse mb-3"></div>
|
||||||
|
<div className="h-4 bg-gray-200 rounded animate-pulse w-3/4 mb-4"></div>
|
||||||
|
<div className="h-10 bg-gray-200 rounded-lg animate-pulse"></div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show error state
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="p-6 text-center text-red-500">
|
||||||
|
<p className="text-sm">خطا در بارگذاری اطلاعات محصول</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatPrice = (price: number) => {
|
||||||
|
return price.toLocaleString('fa-IR')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!productId) {
|
||||||
|
return (
|
||||||
|
<div className="p-6 text-center text-gray-500">
|
||||||
|
<p className="text-sm">محصولی انتخاب نشده</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!product) {
|
||||||
|
return (
|
||||||
|
<div className="p-6 text-center text-gray-500">
|
||||||
|
<p className="text-sm">اطلاعات محصول در دسترس نیست</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
{/* Product Image */}
|
||||||
|
<div className="w-full h-32 mb-4 bg-gradient-to-br from-gray-50 to-gray-100 rounded-lg overflow-hidden flex items-center justify-center">
|
||||||
|
<Image
|
||||||
|
src={product.imagesUrl.cover}
|
||||||
|
alt={product.title_fa}
|
||||||
|
width={80}
|
||||||
|
height={80}
|
||||||
|
className="object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Product Title */}
|
||||||
|
<h3 className="text-sm text-gray-900 font-medium mb-3 leading-tight line-clamp-3">
|
||||||
|
{product.title_fa}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{/* Price */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="flex items-baseline gap-1 mb-1">
|
||||||
|
<span className="text-lg font-bold text-gray-900">
|
||||||
|
{formatPrice(product.default_variant.price.selling_price)}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-gray-500">تومان</span>
|
||||||
|
</div>
|
||||||
|
{product.default_variant.price.discount_percent > 0 && (
|
||||||
|
<p className="text-xs text-gray-400 line-through">
|
||||||
|
{formatPrice(product.default_variant.price.retailPrice)} تومان
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Add to Cart Button */}
|
||||||
|
<button className="w-full bg-primary hover:bg-primary/90 text-primary-foreground py-2.5 rounded-lg font-medium text-sm transition-colors flex items-center justify-center gap-2">
|
||||||
|
<ShoppingCart size={16} />
|
||||||
|
افزودن به سبد
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
'use client'
|
||||||
|
import { Shop, SearchNormal1 } from 'iconsax-react'
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { useSocket } from '../hooks/useSocket'
|
||||||
|
import { useChatList } from '../../hooks/useChatData'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
import { isAuthenticated } from '@/config/func'
|
||||||
|
|
||||||
|
interface ChatSidebarProps {
|
||||||
|
selectedChatId?: string
|
||||||
|
onChatSelect?: (chatId: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ChatSidebar({ selectedChatId, onChatSelect }: ChatSidebarProps) {
|
||||||
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
|
const [isUserAuthenticated, setIsUserAuthenticated] = useState<boolean | null>(null)
|
||||||
|
const [isConnecting, setIsConnecting] = useState(false)
|
||||||
|
const router = useRouter()
|
||||||
|
const { onUserOnline, onUserOffline, connect, isConnected, isServerSupported } = useSocket()
|
||||||
|
const { data: chatData, isLoading, error } = useChatList({
|
||||||
|
enabled: isUserAuthenticated === true // Only fetch if user is logged in
|
||||||
|
})
|
||||||
|
|
||||||
|
// Check authentication on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const checkAuth = async () => {
|
||||||
|
const authenticated = await isAuthenticated()
|
||||||
|
setIsUserAuthenticated(authenticated)
|
||||||
|
|
||||||
|
if (!authenticated) {
|
||||||
|
console.log('🔐 کاربر login نکرده یا توکن expire شده، redirect به صفحه ورود')
|
||||||
|
router.push('/auth')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// اگر سرور پشتیبانی نمیکند، اصلاً تلاش نکن
|
||||||
|
if (!isServerSupported) {
|
||||||
|
console.log('🚫 سرور Socket.IO پشتیبانی نمیکند، اتصال متوقف شد')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// فقط اگر قبلاً اتصال برقرار نشده، connect کنیم
|
||||||
|
if (!isConnected && !isConnecting) {
|
||||||
|
console.log('🔌 شروع اتصال به سوکت...')
|
||||||
|
setIsConnecting(true)
|
||||||
|
connect('customer')
|
||||||
|
.then(() => {
|
||||||
|
console.log('✅ اتصال سوکت موفق')
|
||||||
|
setIsConnecting(false)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log('❌ اتصال سوکت شکست خورد:', error.message)
|
||||||
|
setIsConnecting(false)
|
||||||
|
})
|
||||||
|
} else if (isConnected) {
|
||||||
|
console.log('ℹ️ اتصال سوکت قبلاً برقرار شده')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkAuth()
|
||||||
|
}, [connect, router, isConnected, isConnecting, isServerSupported])
|
||||||
|
|
||||||
|
// Listen for user online/offline events
|
||||||
|
useEffect(() => {
|
||||||
|
const handleUserOnline = (userId: unknown) => {
|
||||||
|
const id = userId as string
|
||||||
|
console.log('🟢 کاربر آنلاین شد:', id)
|
||||||
|
// Here you could update the chat list to reflect online status
|
||||||
|
// For now, we'll just log it
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUserOffline = (userId: unknown) => {
|
||||||
|
const id = userId as string
|
||||||
|
console.log('🔴 کاربر آفلاین شد:', id)
|
||||||
|
// Here you could update the chat list to reflect offline status
|
||||||
|
}
|
||||||
|
|
||||||
|
onUserOnline(handleUserOnline)
|
||||||
|
onUserOffline(handleUserOffline)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// Cleanup will be handled by the hook
|
||||||
|
}
|
||||||
|
}, [onUserOnline, onUserOffline])
|
||||||
|
|
||||||
|
const chats = chatData?.results?.chats || []
|
||||||
|
|
||||||
|
const filteredChats = chats.filter(chat =>
|
||||||
|
chat.participants.seller.shopName.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleChatClick = (chatId: string) => {
|
||||||
|
if (onChatSelect) {
|
||||||
|
onChatSelect(chatId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full bg-white flex flex-col h-full">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="p-4 border-b border-gray-100">
|
||||||
|
<h2 className="text-lg font-bold text-gray-900 mb-4">گفتگوها</h2>
|
||||||
|
|
||||||
|
{/* Search */}
|
||||||
|
<div className="relative">
|
||||||
|
<SearchNormal1 size={20} className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="جستجو در گفتگوها..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="w-full pr-10 pl-4 py-2.5 bg-gray-50 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Chat List */}
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="p-8 text-center text-gray-400">
|
||||||
|
<p className="text-sm">در حال بارگذاری...</p>
|
||||||
|
</div>
|
||||||
|
) : error ? (
|
||||||
|
<div className="p-8 text-center text-red-400">
|
||||||
|
<p className="text-sm">خطا در بارگذاری گفتگوها</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="divide-y divide-gray-50">
|
||||||
|
{filteredChats.map((chat) => (
|
||||||
|
<div
|
||||||
|
key={chat._id}
|
||||||
|
onClick={() => handleChatClick(chat.chatId)}
|
||||||
|
className={`p-4 cursor-pointer transition-all hover:bg-gray-50 ${selectedChatId === chat.chatId ? 'bg-primary/5 border-r-2 border-r-primary' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
{/* Avatar */}
|
||||||
|
<div className="relative flex-shrink-0">
|
||||||
|
<div className="w-12 h-12 bg-gradient-to-br from-primary/10 to-primary/20 rounded-full flex items-center justify-center overflow-hidden">
|
||||||
|
{chat.participants.seller.logo ? (
|
||||||
|
<img
|
||||||
|
src={chat.participants.seller.logo}
|
||||||
|
alt={chat.participants.seller.shopName}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Shop size={20} className="text-primary" variant="Bold" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{chat.isOnline && (
|
||||||
|
<div className="absolute -bottom-0.5 -right-0.5 w-4 h-4 bg-green-500 border-2 border-white rounded-full"></div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<h3 className="font-semibold text-gray-900 text-sm truncate">
|
||||||
|
{chat.participants.seller.shopName}
|
||||||
|
</h3>
|
||||||
|
<span className="text-xs text-gray-400 flex-shrink-0">
|
||||||
|
{chat.lastMessage ?
|
||||||
|
new Date(chat.lastMessage.createdAt).toLocaleTimeString('fa-IR', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
}) :
|
||||||
|
new Date(chat.updatedAt).toLocaleDateString('fa-IR')
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xs text-gray-500 truncate mb-2">
|
||||||
|
{chat.lastMessage?.content || 'گفتگوی جدید'}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{chat.unreadCount > 0 && (
|
||||||
|
<div className="inline-flex items-center px-2 py-0.5 bg-primary text-primary-foreground text-xs font-medium rounded-full">
|
||||||
|
{chat.unreadCount}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoading && !error && filteredChats.length === 0 && (
|
||||||
|
<div className="p-8 text-center text-gray-400">
|
||||||
|
<p className="text-sm">گفتگویی یافت نشد</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
import { useEffect, useState, useCallback } from "react";
|
||||||
|
import {
|
||||||
|
socketService,
|
||||||
|
Message,
|
||||||
|
UnreadMessage,
|
||||||
|
} from "../service/SocketService";
|
||||||
|
|
||||||
|
export interface UseSocketReturn {
|
||||||
|
isConnected: boolean;
|
||||||
|
isConnecting: boolean;
|
||||||
|
isServerSupported: boolean;
|
||||||
|
connect: (type?: "customer" | "seller") => Promise<void>;
|
||||||
|
disconnect: () => void;
|
||||||
|
joinChat: (chatId: string) => void;
|
||||||
|
leaveChat: (chatId: string) => void;
|
||||||
|
sendMessage: (chatId: string, content: string) => void;
|
||||||
|
onUserOnline: (callback: (userId: unknown) => void) => void;
|
||||||
|
onUserOffline: (callback: (userId: unknown) => void) => void;
|
||||||
|
onUnreadMessages: (callback: (messages: unknown) => void) => void;
|
||||||
|
onNewMessage: (callback: (message: unknown) => void) => void;
|
||||||
|
onError: (callback: (error: unknown) => void) => void;
|
||||||
|
offUserOnline: (callback: (userId: unknown) => void) => void;
|
||||||
|
offUserOffline: (callback: (userId: unknown) => void) => void;
|
||||||
|
offUnreadMessages: (callback: (messages: unknown) => void) => void;
|
||||||
|
offNewMessage: (callback: (message: unknown) => void) => void;
|
||||||
|
offError: (callback: (error: unknown) => void) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSocket = (): UseSocketReturn => {
|
||||||
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
|
const [isConnecting, setIsConnecting] = useState(false);
|
||||||
|
|
||||||
|
// Connect to socket
|
||||||
|
const connect = useCallback(
|
||||||
|
async (type: "customer" | "seller" = "customer") => {
|
||||||
|
if (isConnected || isConnecting) return;
|
||||||
|
|
||||||
|
setIsConnecting(true);
|
||||||
|
try {
|
||||||
|
await socketService.connect(type);
|
||||||
|
setIsConnected(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("خطا در اتصال سوکت:", error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
setIsConnecting(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[isConnected, isConnecting]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Disconnect socket
|
||||||
|
const disconnect = useCallback(() => {
|
||||||
|
socketService.disconnect();
|
||||||
|
setIsConnected(false);
|
||||||
|
setIsConnecting(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Join chat room
|
||||||
|
const joinChat = useCallback((chatId: string) => {
|
||||||
|
socketService.joinChat(chatId);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Leave chat room
|
||||||
|
const leaveChat = useCallback((chatId: string) => {
|
||||||
|
socketService.leaveChat(chatId);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Send message
|
||||||
|
const sendMessage = useCallback((chatId: string, content: string) => {
|
||||||
|
socketService.sendMessage(chatId, content);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
const onUserOnline = useCallback((callback: (userId: unknown) => void) => {
|
||||||
|
socketService.on("userOnline", callback);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onUserOffline = useCallback((callback: (userId: unknown) => void) => {
|
||||||
|
socketService.on("userOffline", callback);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onUnreadMessages = useCallback(
|
||||||
|
(callback: (messages: unknown) => void) => {
|
||||||
|
socketService.on("unreadMessages", callback);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onNewMessage = useCallback((callback: (message: unknown) => void) => {
|
||||||
|
socketService.on("newMessage", callback);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onError = useCallback((callback: (error: unknown) => void) => {
|
||||||
|
socketService.on("error", callback);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Remove event listeners
|
||||||
|
const offUserOnline = useCallback((callback: (userId: unknown) => void) => {
|
||||||
|
socketService.off("userOnline", callback);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const offUserOffline = useCallback((callback: (userId: unknown) => void) => {
|
||||||
|
socketService.off("userOffline", callback);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const offUnreadMessages = useCallback(
|
||||||
|
(callback: (messages: unknown) => void) => {
|
||||||
|
socketService.off("unreadMessages", callback);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const offNewMessage = useCallback((callback: (message: unknown) => void) => {
|
||||||
|
socketService.off("newMessage", callback);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const offError = useCallback((callback: (error: unknown) => void) => {
|
||||||
|
socketService.off("error", callback);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Setup connection status listener
|
||||||
|
useEffect(() => {
|
||||||
|
const handleConnect = () => setIsConnected(true);
|
||||||
|
const handleDisconnect = () => setIsConnected(false);
|
||||||
|
|
||||||
|
socketService.on("connect", handleConnect);
|
||||||
|
socketService.on("disconnect", handleDisconnect);
|
||||||
|
|
||||||
|
// Check initial connection status
|
||||||
|
setIsConnected(socketService.isConnected());
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
socketService.off("connect", handleConnect);
|
||||||
|
socketService.off("disconnect", handleDisconnect);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Cleanup on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
// Don't disconnect on unmount, let the service manage its own lifecycle
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isConnected,
|
||||||
|
isConnecting,
|
||||||
|
isServerSupported: socketService.getServerSupported(),
|
||||||
|
connect,
|
||||||
|
disconnect,
|
||||||
|
joinChat,
|
||||||
|
leaveChat,
|
||||||
|
sendMessage,
|
||||||
|
onUserOnline,
|
||||||
|
onUserOffline,
|
||||||
|
onUnreadMessages,
|
||||||
|
onNewMessage,
|
||||||
|
onError,
|
||||||
|
offUserOnline,
|
||||||
|
offUserOffline,
|
||||||
|
offUnreadMessages,
|
||||||
|
offNewMessage,
|
||||||
|
offError,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
'use client'
|
||||||
|
import Layout from "@/hoc/Layout"
|
||||||
|
import { NextPage } from "next"
|
||||||
|
import { useSearchParams, useRouter } from "next/navigation"
|
||||||
|
import { useState, useEffect, Suspense } from "react"
|
||||||
|
import Menu from "../components/Menu"
|
||||||
|
import ChatSidebar from "./components/ChatSidebar"
|
||||||
|
import ChatMessages from "./components/ChatMessages"
|
||||||
|
import ChatProductCard from "./components/ChatProductCard"
|
||||||
|
import { isAuthenticated } from "@/config/func"
|
||||||
|
|
||||||
|
const ChatContent: React.FC = () => {
|
||||||
|
const searchParams = useSearchParams()
|
||||||
|
const productId = searchParams.get('productId')
|
||||||
|
const shopId = searchParams.get('shopId')
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const [selectedChatId, setSelectedChatId] = useState<string>('')
|
||||||
|
const [isUserAuthenticated, setIsUserAuthenticated] = useState<boolean | null>(null)
|
||||||
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
|
||||||
|
// Check authentication on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const checkAuth = async () => {
|
||||||
|
const authenticated = await isAuthenticated()
|
||||||
|
setIsUserAuthenticated(authenticated)
|
||||||
|
setIsLoading(false)
|
||||||
|
|
||||||
|
if (!authenticated) {
|
||||||
|
console.log('🔐 کاربر login نکرده یا توکن expire شده، redirect به صفحه ورود')
|
||||||
|
router.push('/auth')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkAuth()
|
||||||
|
}, [router])
|
||||||
|
|
||||||
|
const handleChatSelect = (chatId: string) => {
|
||||||
|
setSelectedChatId(chatId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show loading while checking authentication
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<div className="p-3 sm:p-4 md:p-6">
|
||||||
|
<Menu pageActive='chat' />
|
||||||
|
<div className="mt-6 flex items-center justify-center h-[calc(100vh-200px)] min-h-[600px]">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-lg text-gray-600 mb-2">در حال بررسی...</div>
|
||||||
|
<div className="text-sm text-gray-400">لطفاً صبر کنید</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't render chat if user is not logged in (will redirect)
|
||||||
|
if (!isUserAuthenticated) {
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<div className="p-3 sm:p-4 md:p-6">
|
||||||
|
<Menu pageActive='chat' />
|
||||||
|
<div className="mt-6 flex items-center justify-center h-[calc(100vh-200px)] min-h-[600px]">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-lg text-gray-600 mb-2">در حال انتقال...</div>
|
||||||
|
<div className="text-sm text-gray-400">لطفاً صبر کنید</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<div className="p-3 sm:p-4 md:p-6">
|
||||||
|
<Menu pageActive='chat' />
|
||||||
|
|
||||||
|
<div className="mt-6 flex gap-8 h-[calc(100vh-200px)] min-h-[600px]">
|
||||||
|
{/* Chat Section - Sidebar + Messages */}
|
||||||
|
<div className="flex-1 flex border border-gray-200 shadow-sm rounded-xl bg-white">
|
||||||
|
{/* Chat Sidebar - Left Side */}
|
||||||
|
<div className="w-80 border-r border-gray-200">
|
||||||
|
<ChatSidebar
|
||||||
|
selectedChatId={selectedChatId}
|
||||||
|
onChatSelect={handleChatSelect}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Chat Messages - Right Side */}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<ChatMessages
|
||||||
|
productId={productId}
|
||||||
|
shopId={shopId}
|
||||||
|
chatId={selectedChatId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Product Card - Right Side */}
|
||||||
|
<div className="w-80 flex-shrink-0">
|
||||||
|
<div className="bg-white rounded-xl border border-gray-200 shadow-sm">
|
||||||
|
<ChatProductCard productId={productId} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Chat: NextPage = () => {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={
|
||||||
|
<Layout>
|
||||||
|
<div className="p-3 sm:p-4 md:p-6">
|
||||||
|
<Menu pageActive='chat' />
|
||||||
|
<div className="mt-6 flex items-center justify-center h-[calc(100vh-200px)] min-h-[600px]">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-lg text-gray-600 mb-2">در حال بارگذاری...</div>
|
||||||
|
<div className="text-sm text-gray-400">لطفاً صبر کنید</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
}>
|
||||||
|
<ChatContent />
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Chat
|
||||||
@@ -0,0 +1,274 @@
|
|||||||
|
import { io, Socket } from "socket.io-client";
|
||||||
|
import { TOKEN_NAME, BASE_URL } from "@/config/const";
|
||||||
|
|
||||||
|
export interface Message {
|
||||||
|
id: string;
|
||||||
|
chatId: string;
|
||||||
|
sender: string;
|
||||||
|
content: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UnreadMessage {
|
||||||
|
chatId: string;
|
||||||
|
sender: string;
|
||||||
|
content: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SocketService {
|
||||||
|
private socket: Socket | null = null;
|
||||||
|
private reconnectAttempts = 0;
|
||||||
|
private maxReconnectAttempts = 1; // کاهش تعداد تلاشها
|
||||||
|
private reconnectDelay = 2000; // افزایش delay
|
||||||
|
private isServerSupported = false; // آیا سرور پشتیبانی میکند
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
private eventListeners: { [key: string]: ((data: unknown) => void)[] } = {};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.initializeEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeEventListeners() {
|
||||||
|
this.eventListeners = {
|
||||||
|
userOnline: [],
|
||||||
|
userOffline: [],
|
||||||
|
unreadMessages: [],
|
||||||
|
newMessage: [],
|
||||||
|
error: [],
|
||||||
|
unauthorized: [],
|
||||||
|
connect: [],
|
||||||
|
disconnect: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to socket server
|
||||||
|
connect(type: "customer" | "seller" = "customer"): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
// Get token from localStorage
|
||||||
|
const token = localStorage.getItem(TOKEN_NAME);
|
||||||
|
console.log("🔑 توکن سوکت:", token ? "موجود است" : "پیدا نشد");
|
||||||
|
if (!token) {
|
||||||
|
console.log("❌ اتصال رد شد: توکن موجود نیست");
|
||||||
|
reject(new Error("توکن یافت نشد"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// اگر قبلاً تشخیص دادهایم سرور پشتیبانی نمیکند
|
||||||
|
if (!this.isServerSupported && this.reconnectAttempts > 0) {
|
||||||
|
console.log("🚫 اتصال رد شد: سرور Socket.IO پشتیبانی نمیکند");
|
||||||
|
reject(new Error("سرور چت پشتیبانی نمیشود"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("🔌 شروع اتصال سوکت به:", `${BASE_URL}/ws-chat`);
|
||||||
|
console.log("📝 پارامترهای اتصال:", {
|
||||||
|
token: token.substring(0, 20) + "...",
|
||||||
|
type,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create socket connection
|
||||||
|
this.socket = io(BASE_URL, {
|
||||||
|
path: "/ws-chat",
|
||||||
|
query: {
|
||||||
|
token,
|
||||||
|
type,
|
||||||
|
},
|
||||||
|
transports: ["websocket"],
|
||||||
|
timeout: 10000, // 10 seconds timeout
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup event listeners
|
||||||
|
this.setupSocketListeners();
|
||||||
|
|
||||||
|
// Handle connection
|
||||||
|
this.socket.on("connect", () => {
|
||||||
|
console.log("✅ اتصال سوکت برقرار شد");
|
||||||
|
this.reconnectAttempts = 0;
|
||||||
|
this.isServerSupported = true; // سرور پشتیبانی میکند
|
||||||
|
this.emit("connect", null);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on("connect_error", (error) => {
|
||||||
|
console.error("❌ خطا در اتصال سوکت:", error.message);
|
||||||
|
console.log("⚠️ احتمالاً سرور Socket.IO پیادهسازی نشده است");
|
||||||
|
|
||||||
|
// اگر چندین بار شکست خورد، فرض میکنیم سرور پشتیبانی نمیکند
|
||||||
|
if (this.reconnectAttempts >= 1) {
|
||||||
|
console.log(
|
||||||
|
"🚫 سرور Socket.IO پشتیبانی نمیشود، reconnect متوقف شد"
|
||||||
|
);
|
||||||
|
this.isServerSupported = false;
|
||||||
|
reject(new Error("سرور چت پشتیبانی نمیشود"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("خطا در ایجاد اتصال سوکت:", error);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup socket event listeners
|
||||||
|
private setupSocketListeners() {
|
||||||
|
if (!this.socket) return;
|
||||||
|
|
||||||
|
// User online/offline events
|
||||||
|
this.socket.on("userOnline", (userId: string) => {
|
||||||
|
console.log("کاربر آنلاین شد:", userId);
|
||||||
|
this.emit("userOnline", userId);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on("userOffline", (userId: string) => {
|
||||||
|
console.log("کاربر آفلاین شد:", userId);
|
||||||
|
this.emit("userOffline", userId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Unread messages
|
||||||
|
this.socket.on("unreadMessages", (messages: UnreadMessage[]) => {
|
||||||
|
console.log("پیامهای خوانده نشده:", messages);
|
||||||
|
this.emit("unreadMessages", messages);
|
||||||
|
});
|
||||||
|
|
||||||
|
// New message
|
||||||
|
this.socket.on("newMessage", (message: Message) => {
|
||||||
|
console.log("پیام جدید:", message);
|
||||||
|
this.emit("newMessage", message);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Error handling
|
||||||
|
this.socket.on("error", (error: { message: string }) => {
|
||||||
|
console.error("خطای سوکت:", error.message);
|
||||||
|
this.emit("error", error);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket.on("unauthorized", (data: { message: string }) => {
|
||||||
|
console.error("دسترسی غیرمجاز:", data.message);
|
||||||
|
this.emit("unauthorized", data);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connection events
|
||||||
|
this.socket.on("disconnect", (reason: string) => {
|
||||||
|
console.log("اتصال سوکت قطع شد:", reason);
|
||||||
|
this.emit("disconnect", reason);
|
||||||
|
|
||||||
|
// Auto reconnect if not manual disconnect
|
||||||
|
if (
|
||||||
|
reason !== "io client disconnect" &&
|
||||||
|
this.reconnectAttempts < this.maxReconnectAttempts
|
||||||
|
) {
|
||||||
|
this.scheduleReconnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedule reconnection
|
||||||
|
private scheduleReconnect() {
|
||||||
|
// اگر سرور پشتیبانی نمیکند، reconnect نکن
|
||||||
|
if (!this.isServerSupported) {
|
||||||
|
console.log("🚫 reconnect متوقف شد چون سرور پشتیبانی نمیکند");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.reconnectAttempts++;
|
||||||
|
console.log(
|
||||||
|
`🔄 تلاش برای اتصال مجدد (${this.reconnectAttempts}/${this.maxReconnectAttempts})`
|
||||||
|
);
|
||||||
|
|
||||||
|
const token = localStorage.getItem(TOKEN_NAME);
|
||||||
|
if (token && this.socket) {
|
||||||
|
this.socket.connect();
|
||||||
|
} else {
|
||||||
|
console.log("❌ توکن یا سوکت موجود نیست، reconnect متوقف شد");
|
||||||
|
}
|
||||||
|
}, this.reconnectDelay * this.reconnectAttempts);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join a chat room
|
||||||
|
joinChat(chatId: string): void {
|
||||||
|
if (this.socket && this.socket.connected) {
|
||||||
|
this.socket.emit("joinChat", chatId);
|
||||||
|
console.log("ورود به چت:", chatId);
|
||||||
|
} else {
|
||||||
|
console.error("سوکت متصل نیست");
|
||||||
|
this.emit("error", { message: "اتصال سوکت برقرار نیست" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leave a chat room
|
||||||
|
leaveChat(chatId: string): void {
|
||||||
|
if (this.socket && this.socket.connected) {
|
||||||
|
this.socket.emit("leaveChat", chatId);
|
||||||
|
console.log("خروج از چت:", chatId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a message
|
||||||
|
sendMessage(chatId: string, content: string): void {
|
||||||
|
if (this.socket && this.socket.connected) {
|
||||||
|
this.socket.emit("newMessage", {
|
||||||
|
chatId,
|
||||||
|
content,
|
||||||
|
});
|
||||||
|
console.log("ارسال پیام:", { chatId, content });
|
||||||
|
} else {
|
||||||
|
console.error("سوکت متصل نیست");
|
||||||
|
this.emit("error", { message: "اتصال سوکت برقرار نیست" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event system
|
||||||
|
on(event: string, callback: (data: unknown) => void): void {
|
||||||
|
if (this.eventListeners[event]) {
|
||||||
|
this.eventListeners[event].push(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
off(event: string, callback: (data: unknown) => void): void {
|
||||||
|
if (this.eventListeners[event]) {
|
||||||
|
this.eventListeners[event] = this.eventListeners[event].filter(
|
||||||
|
(cb) => cb !== callback
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private emit(event: string, data: unknown): void {
|
||||||
|
if (this.eventListeners[event]) {
|
||||||
|
this.eventListeners[event].forEach((callback) => callback(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disconnect socket
|
||||||
|
disconnect(): void {
|
||||||
|
if (this.socket) {
|
||||||
|
this.socket.disconnect();
|
||||||
|
this.socket = null;
|
||||||
|
}
|
||||||
|
this.reconnectAttempts = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if socket is connected
|
||||||
|
isConnected(): boolean {
|
||||||
|
return this.socket?.connected || false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if server supports socket
|
||||||
|
getServerSupported(): boolean {
|
||||||
|
return this.isServerSupported;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get socket instance (for advanced usage)
|
||||||
|
getSocket(): Socket | null {
|
||||||
|
return this.socket;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create singleton instance
|
||||||
|
export const socketService = new SocketService();
|
||||||
@@ -19,7 +19,7 @@ const Menu: FC<Props> = (props) => {
|
|||||||
<MenuItem href='/profile' icon='user' text='پروفایل کاربری' isActive={pageActive === 'profile'} />
|
<MenuItem href='/profile' icon='user' text='پروفایل کاربری' isActive={pageActive === 'profile'} />
|
||||||
<MenuItem href='/profile/favorite' icon='heart' text='علاقه مندی ها' isActive={pageActive === 'favorite'} />
|
<MenuItem href='/profile/favorite' icon='heart' text='علاقه مندی ها' isActive={pageActive === 'favorite'} />
|
||||||
<MenuItem href='/profile/comments' icon='comment' text='نظرات من' isActive={pageActive === 'comments'} />
|
<MenuItem href='/profile/comments' icon='comment' text='نظرات من' isActive={pageActive === 'comments'} />
|
||||||
<MenuItem href='/profile/chat' icon='chat' text='گفتگو' isActive={pageActive === 'chat'} />
|
{/* <MenuItem href='/profile/chat' icon='chat' text='گفتگو' isActive={pageActive === 'chat'} /> */}
|
||||||
<MenuItem href='/profile/orders' icon='orders' text='سفارشات' isActive={pageActive === 'orders'} />
|
<MenuItem href='/profile/orders' icon='orders' text='سفارشات' isActive={pageActive === 'orders'} />
|
||||||
|
|
||||||
{/* Logout Button in Mobile */}
|
{/* Logout Button in Mobile */}
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
getChatList,
|
||||||
|
getChatMessages,
|
||||||
|
createChat,
|
||||||
|
markChatAsRead,
|
||||||
|
} from "../service/Service";
|
||||||
|
import { CreateChatRequest } from "../types/ProfileTypes";
|
||||||
|
|
||||||
|
// Get chat list
|
||||||
|
export const useChatList = (options?: { enabled?: boolean }) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["chat", "list"],
|
||||||
|
queryFn: getChatList,
|
||||||
|
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||||
|
enabled: options?.enabled ?? true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get chat messages
|
||||||
|
export const useChatMessages = (
|
||||||
|
chatId: string,
|
||||||
|
options?: { enabled?: boolean }
|
||||||
|
) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["chat", "messages", chatId],
|
||||||
|
queryFn: () => getChatMessages(chatId),
|
||||||
|
enabled: options?.enabled ?? !!chatId,
|
||||||
|
staleTime: 1000 * 60 * 1, // 1 minute
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create new chat
|
||||||
|
export const useCreateChat = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (chatData: CreateChatRequest) => createChat(chatData),
|
||||||
|
onSuccess: () => {
|
||||||
|
// Invalidate chat list to refetch
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["chat", "list"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mark chat as read
|
||||||
|
export const useMarkChatAsRead = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (chatId: string) => {
|
||||||
|
console.log("📤 ارسال درخواست markAsRead برای:", chatId);
|
||||||
|
return markChatAsRead(chatId);
|
||||||
|
},
|
||||||
|
onSuccess: (_, chatId) => {
|
||||||
|
console.log(
|
||||||
|
"✅ پیامها با موفقیت به عنوان خوانده شده علامتگذاری شدند:",
|
||||||
|
chatId
|
||||||
|
);
|
||||||
|
// Update chat list to reflect read status
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["chat", "list"] });
|
||||||
|
// Update messages to mark as read
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["chat", "messages", chatId] });
|
||||||
|
},
|
||||||
|
onError: (error, chatId) => {
|
||||||
|
console.error("❌ خطا در markAsRead:", chatId, error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -7,6 +7,10 @@ import {
|
|||||||
UpdateAddressType,
|
UpdateAddressType,
|
||||||
WishlistResponse,
|
WishlistResponse,
|
||||||
CommentsResponse,
|
CommentsResponse,
|
||||||
|
ChatListResponse,
|
||||||
|
ChatMessagesResponse,
|
||||||
|
CreateChatRequest,
|
||||||
|
CreateChatResponse,
|
||||||
} from "../types/ProfileTypes";
|
} from "../types/ProfileTypes";
|
||||||
|
|
||||||
export const getProfile = async () => {
|
export const getProfile = async () => {
|
||||||
@@ -48,3 +52,28 @@ export const changeEmail = async (email: string) => {
|
|||||||
const { data } = await axios.post("/user/profile/email/change", { email });
|
const { data } = await axios.post("/user/profile/email/change", { email });
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Chat Services
|
||||||
|
export const getChatList = async (): Promise<ChatListResponse> => {
|
||||||
|
const { data } = await axios.get("/chat/list");
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getChatMessages = async (
|
||||||
|
chatId: string
|
||||||
|
): Promise<ChatMessagesResponse> => {
|
||||||
|
const { data } = await axios.get(`/chat/${chatId}/messages`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createChat = async (
|
||||||
|
chatData: CreateChatRequest
|
||||||
|
): Promise<CreateChatResponse> => {
|
||||||
|
const { data } = await axios.post("/chat/create", chatData);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const markChatAsRead = async (chatId: string) => {
|
||||||
|
const { data } = await axios.patch(`/chat/${chatId}/read`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|||||||
@@ -280,3 +280,95 @@ export interface CommentsResponse {
|
|||||||
comments: Comment[];
|
comments: Comment[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Chat Types
|
||||||
|
export interface ChatUser {
|
||||||
|
_id: string;
|
||||||
|
fullName: string;
|
||||||
|
phoneNumber: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatShop {
|
||||||
|
_id: string;
|
||||||
|
shopName: string;
|
||||||
|
shopCode: number;
|
||||||
|
logo: string;
|
||||||
|
isChatActive: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatItem {
|
||||||
|
_id: string;
|
||||||
|
chatId: string;
|
||||||
|
participants: {
|
||||||
|
customer: ChatUser;
|
||||||
|
seller: ChatShop;
|
||||||
|
};
|
||||||
|
lastMessage?: {
|
||||||
|
content: string;
|
||||||
|
createdAt: string;
|
||||||
|
sender: "customer" | "seller";
|
||||||
|
};
|
||||||
|
unreadCount: number;
|
||||||
|
isOnline: boolean;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatMessage {
|
||||||
|
_id: string;
|
||||||
|
chatId: string;
|
||||||
|
content: string;
|
||||||
|
sender: "customer" | "seller";
|
||||||
|
senderInfo: ChatUser | ChatShop;
|
||||||
|
createdAt: string;
|
||||||
|
status: "sent" | "delivered" | "read";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatProduct {
|
||||||
|
_id: number;
|
||||||
|
title_fa: string;
|
||||||
|
imagesUrl: {
|
||||||
|
cover: string;
|
||||||
|
list: string[];
|
||||||
|
};
|
||||||
|
default_variant: {
|
||||||
|
price: {
|
||||||
|
selling_price: number;
|
||||||
|
retailPrice: number;
|
||||||
|
discount_percent: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatListResponse {
|
||||||
|
status: number;
|
||||||
|
success: boolean;
|
||||||
|
results: {
|
||||||
|
chats: ChatItem[];
|
||||||
|
totalCount: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatMessagesResponse {
|
||||||
|
status: number;
|
||||||
|
success: boolean;
|
||||||
|
results: {
|
||||||
|
messages: ChatMessage[];
|
||||||
|
chat: ChatItem;
|
||||||
|
product?: ChatProduct;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateChatRequest {
|
||||||
|
productId: number;
|
||||||
|
shopId: string;
|
||||||
|
initialMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateChatResponse {
|
||||||
|
status: number;
|
||||||
|
success: boolean;
|
||||||
|
results: {
|
||||||
|
chat: ChatItem;
|
||||||
|
message?: ChatMessage;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -38,6 +38,50 @@ export const getToken = async () => {
|
|||||||
return localStorage.getItem(TOKEN_NAME);
|
return localStorage.getItem(TOKEN_NAME);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isAuthenticated = async (): Promise<boolean> => {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return false; // در سرور همیشه false
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = localStorage.getItem(TOKEN_NAME);
|
||||||
|
const refreshToken = localStorage.getItem(REFRESH_TOKEN_NAME);
|
||||||
|
|
||||||
|
// اگر هیچ توکنی وجود ندارد
|
||||||
|
if (!token && !refreshToken) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// اگر توکن وجود دارد، فرض میکنیم معتبر است (axios interceptor refresh را هندل میکند)
|
||||||
|
if (token) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// اگر فقط refresh token وجود دارد، سعی میکنیم refresh کنیم
|
||||||
|
if (refreshToken) {
|
||||||
|
try {
|
||||||
|
const { refreshToken: refreshTokenAPI } = await import(
|
||||||
|
"../app/auth/service/Service"
|
||||||
|
);
|
||||||
|
const data = await refreshTokenAPI({ token: refreshToken });
|
||||||
|
|
||||||
|
if (data?.results?.data?.accessToken?.token) {
|
||||||
|
await setToken(data.results.data.accessToken.token);
|
||||||
|
if (data.results.data.refreshToken?.token) {
|
||||||
|
await setRefreshToken(data.results.data.refreshToken.token);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("خطا در refresh token:", error);
|
||||||
|
// پاک کردن توکنهای نامعتبر
|
||||||
|
await removeToken();
|
||||||
|
await removeRefreshToken();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
export const getRefreshToken = async () => {
|
export const getRefreshToken = async () => {
|
||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
} from "@/components/ui/popover"
|
} from "@/components/ui/popover"
|
||||||
import { PRIMARY_COLOR } from '@/config/const'
|
import { PRIMARY_COLOR } from '@/config/const'
|
||||||
import { removeToken, removeRefreshToken } from '@/config/func'
|
import { removeToken, removeRefreshToken } from '@/config/func'
|
||||||
import { User, Heart, MessageText, Logout, SmsNotification, Profile } from 'iconsax-react'
|
import { User, Heart, Logout, SmsNotification, Profile } from 'iconsax-react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
|
|
||||||
@@ -41,12 +41,12 @@ const Account = () => {
|
|||||||
icon: SmsNotification,
|
icon: SmsNotification,
|
||||||
href: '/profile/comments'
|
href: '/profile/comments'
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
id: 'chat',
|
// id: 'chat',
|
||||||
title: 'گفتگو',
|
// title: 'گفتگو',
|
||||||
icon: MessageText,
|
// icon: MessageText,
|
||||||
href: '/profile/chat'
|
// href: '/profile/chat'
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
id: 'logout',
|
id: 'logout',
|
||||||
title: 'خروج از حساب',
|
title: 'خروج از حساب',
|
||||||
@@ -55,12 +55,21 @@ const Account = () => {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const handleAccountClick = () => {
|
||||||
|
if (!isSuccess) {
|
||||||
|
router.push('/auth')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover>
|
<Popover>
|
||||||
|
|
||||||
<PopoverTrigger asChild>
|
<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'>
|
<div
|
||||||
|
onClick={handleAccountClick}
|
||||||
|
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' />
|
<Profile size={20} color='#333333' />
|
||||||
<div className='text-sm text-[#333333] '>{profile?.results?.info?.fullName ? profile?.results?.info?.fullName : 'حساب کاربری'}</div>
|
<div className='text-sm text-[#333333] '>{profile?.results?.info?.fullName ? profile?.results?.info?.fullName : 'حساب کاربری'}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user