use socket
This commit is contained in:
@@ -2,3 +2,5 @@ VITE_TOKEN_NAME = 'dmnu_a_t'
|
||||
VITE_REFRESH_TOKEN_NAME = 'dmnu-a-rt'
|
||||
VITE_BASE_URL = 'https://dmenuplus-api.dev.danakcorp.com'
|
||||
# VITE_BASE_URL = 'http://192.168.1.112:4001'
|
||||
# VITE_SOCKET_URL = 'https://dmenuplus-api.dev.danakcorp.com/pager'
|
||||
VITE_SOCKET_URL = 'http://192.168.99.242:4000/pager'
|
||||
Generated
+137
-1
@@ -32,6 +32,7 @@
|
||||
"react-router-dom": "^7.8.2",
|
||||
"react-spinners": "^0.17.0",
|
||||
"react-toastify": "^11.0.5",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"swiper": "^11.2.10",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"yup": "^1.7.0",
|
||||
@@ -1766,6 +1767,12 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"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/@swc/helpers": {
|
||||
"version": "0.5.17",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
|
||||
@@ -2933,6 +2940,45 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"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": {
|
||||
"version": "5.18.3",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
|
||||
@@ -4303,7 +4349,6 @@
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
@@ -4935,6 +4980,68 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
@@ -5403,6 +5510,35 @@
|
||||
"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": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"react-router-dom": "^7.8.2",
|
||||
"react-spinners": "^0.17.0",
|
||||
"react-toastify": "^11.0.5",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"swiper": "^11.2.10",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"yup": "^1.7.0",
|
||||
|
||||
@@ -75,4 +75,7 @@ export const Pages = {
|
||||
list: "/reviews/list",
|
||||
detail: "/reviews/detail/",
|
||||
},
|
||||
pagers: {
|
||||
list: "/pagers/list",
|
||||
},
|
||||
};
|
||||
|
||||
+2
-1
@@ -65,7 +65,8 @@
|
||||
"payment_methods": "روش های پرداخت",
|
||||
"admins": "مدیران",
|
||||
"shipment_methods": "روش های ارسال",
|
||||
"reviews": "نظرات"
|
||||
"reviews": "نظرات",
|
||||
"pagers": "پیجرها"
|
||||
},
|
||||
"shipment": {
|
||||
"dineIn": "سرو در محل",
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { type FC, useEffect, useState, useMemo } from 'react'
|
||||
import { useSocket } from '@/pages/pager/hooks/useSocket'
|
||||
import { getToken } from '@/config/func'
|
||||
import type { PagersType, Pager } from './types/Types'
|
||||
import Table from '@/components/Table'
|
||||
import Filters from '@/components/Filters'
|
||||
import { usePagerFilters } from './hooks/usePagerFilters'
|
||||
import { getPagerTableColumns } from './components/PagerTableColumns'
|
||||
import { usePagerFiltersFields } from './components/PagerFiltersFields'
|
||||
import type { RowDataType } from '@/components/types/TableTypes'
|
||||
|
||||
const PagersList: FC = () => {
|
||||
const token = `Bearer ${getToken()}`
|
||||
const { connect, isConnected, socket } = useSocket(import.meta.env.VITE_SOCKET_URL, token)
|
||||
|
||||
const [pagers, setPagers] = useState<PagersType>()
|
||||
|
||||
const {
|
||||
filters,
|
||||
currentPage,
|
||||
handleFiltersChange,
|
||||
handlePageChange,
|
||||
filteredData,
|
||||
paginatedData,
|
||||
totalPages,
|
||||
} = usePagerFilters()
|
||||
|
||||
useEffect(() => {
|
||||
connect()
|
||||
}, [connect])
|
||||
|
||||
const handleJoinRestaurant = () => {
|
||||
socket?.emit('get:pagers')
|
||||
}
|
||||
|
||||
const handlePagersList = (data: PagersType) => {
|
||||
setPagers(data)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isConnected && socket) {
|
||||
socket.emit('join:restaurant')
|
||||
}
|
||||
}, [isConnected, socket])
|
||||
|
||||
useEffect(() => {
|
||||
if (socket) {
|
||||
socket.on('joined', handleJoinRestaurant)
|
||||
socket.on('pagers:list', handlePagersList)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (socket) {
|
||||
socket.off('joined', handleJoinRestaurant)
|
||||
socket.off('pagers:list', handlePagersList)
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [socket])
|
||||
|
||||
const pagersList = useMemo(() => {
|
||||
return pagers?.pagers || []
|
||||
}, [pagers])
|
||||
|
||||
const filteredPagers = useMemo(() => {
|
||||
return filteredData(pagersList) as Pager[]
|
||||
}, [pagersList, filteredData])
|
||||
|
||||
const paginatedPagers = useMemo(() => {
|
||||
return paginatedData(filteredPagers) as Pager[]
|
||||
}, [filteredPagers, paginatedData])
|
||||
|
||||
const totalPagesCount = useMemo(() => {
|
||||
return totalPages(filteredPagers.length)
|
||||
}, [filteredPagers.length, totalPages])
|
||||
|
||||
const columns = getPagerTableColumns()
|
||||
const filterFields = usePagerFiltersFields()
|
||||
|
||||
return (
|
||||
<div className='mt-5'>
|
||||
<div className='flex justify-between items-center'>
|
||||
<h1 className='text-lg font-light'>لیست پیجرها</h1>
|
||||
</div>
|
||||
|
||||
<div className='mt-8'>
|
||||
<Filters
|
||||
fields={filterFields}
|
||||
onChange={handleFiltersChange}
|
||||
initialValues={filters}
|
||||
searchField="search"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Table<Pager & RowDataType>
|
||||
columns={columns}
|
||||
data={paginatedPagers as (Pager & RowDataType)[]}
|
||||
isloading={false}
|
||||
pagination={{
|
||||
currentPage,
|
||||
totalPages: totalPagesCount,
|
||||
onPageChange: handlePageChange,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PagersList
|
||||
@@ -0,0 +1,22 @@
|
||||
import { useMemo } from 'react'
|
||||
import type { FieldType } from '@/components/Filters'
|
||||
|
||||
export const usePagerFiltersFields = (): FieldType[] => {
|
||||
return useMemo(() => [
|
||||
{
|
||||
type: 'input',
|
||||
name: 'search',
|
||||
placeholder: 'جستجو',
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
name: 'status',
|
||||
placeholder: 'وضعیت',
|
||||
options: [
|
||||
{ label: 'همه', value: '' },
|
||||
{ label: 'فعال', value: 'active' },
|
||||
{ label: 'غیرفعال', value: 'inactive' },
|
||||
],
|
||||
},
|
||||
], [])
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { ColumnType } from '@/components/types/TableTypes'
|
||||
import type { Pager } from '../types/Types'
|
||||
import { formatOptionalDate } from '@/helpers/func'
|
||||
import Status from '@/components/Status'
|
||||
|
||||
export const getPagerTableColumns = (): ColumnType<Pager>[] => {
|
||||
return [
|
||||
{
|
||||
key: 'tableNumber',
|
||||
title: 'شماره میز',
|
||||
render: (item: Pager) => {
|
||||
return item.tableNumber || '-'
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'message',
|
||||
title: 'پیام',
|
||||
render: (item: Pager) => {
|
||||
return item.message || '-'
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
title: 'وضعیت',
|
||||
render: (item: Pager) => {
|
||||
const statusLabel = item.status === 'active' ? 'فعال' : 'غیرفعال'
|
||||
const statusVariant = item.status === 'active' ? 'success' : 'error'
|
||||
return (
|
||||
<Status variant={statusVariant} label={statusLabel} />
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'createdAt',
|
||||
title: 'تاریخ ایجاد',
|
||||
render: (item: Pager) => {
|
||||
return formatOptionalDate(item.createdAt, {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'updatedAt',
|
||||
title: 'آخرین بروزرسانی',
|
||||
render: (item: Pager) => {
|
||||
return formatOptionalDate(item.updatedAt, {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
# 🚀 useSocket Hook - راهنمای استفاده
|
||||
|
||||
یک Hook قدرتمند و ساده برای مدیریت Socket.IO در React
|
||||
|
||||
## ✨ ویژگیها
|
||||
|
||||
- ✅ دریافت URL و Token مستقیم از کاربر
|
||||
- ✅ اتصال خودکار
|
||||
- ✅ Reconnection هوشمند
|
||||
- ✅ مدیریت Event Listeners
|
||||
- ✅ TypeScript Support
|
||||
- ✅ مدیریت خطاها
|
||||
- ✅ Clean up خودکار
|
||||
|
||||
## 📦 نصب
|
||||
|
||||
```bash
|
||||
npm install socket.io-client
|
||||
```
|
||||
|
||||
## 📖 استفاده
|
||||
|
||||
### سادهترین روش (بدون token)
|
||||
|
||||
```tsx
|
||||
import { useSocket } from '@/pages/pager/hooks/useSocket';
|
||||
|
||||
function MyComponent() {
|
||||
const { isConnected, emit, on } = useSocket('http://localhost:3000');
|
||||
|
||||
useEffect(() => {
|
||||
on('message', (data) => {
|
||||
console.log('پیام:', data);
|
||||
});
|
||||
}, [on]);
|
||||
|
||||
return <div>{isConnected ? 'متصل' : 'قطع'}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
### با Token
|
||||
|
||||
```tsx
|
||||
function MyComponent() {
|
||||
const token = localStorage.getItem('token') || '';
|
||||
const { isConnected, emit, on } = useSocket('http://localhost:3000', token);
|
||||
|
||||
return <div>{isConnected ? 'متصل' : 'قطع'}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
### با تنظیمات کامل
|
||||
|
||||
```tsx
|
||||
function MyComponent() {
|
||||
const url = 'http://localhost:3000';
|
||||
const token = 'your-jwt-token';
|
||||
|
||||
const { isConnected, emit, on, error } = useSocket(url, token, {
|
||||
reconnectionAttempts: 10,
|
||||
reconnectionDelay: 2000,
|
||||
query: {
|
||||
userId: '123',
|
||||
room: 'general',
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
{error && <p>خطا: {error.message}</p>}
|
||||
<p>{isConnected ? 'متصل' : 'قطع'}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 🎯 API
|
||||
|
||||
### پارامترها
|
||||
|
||||
```tsx
|
||||
useSocket(url: string, token?: string, options?: UseSocketOptions)
|
||||
```
|
||||
|
||||
| نام | نوع | الزامی | توضیح |
|
||||
|-----|-----|--------|-------|
|
||||
| `url` | string | ✅ | آدرس سرور سوکت |
|
||||
| `token` | string | ❌ | توکن احراز هویت |
|
||||
| `options` | object | ❌ | تنظیمات اضافی |
|
||||
|
||||
### Options
|
||||
|
||||
| نام | نوع | پیشفرض | توضیح |
|
||||
|-----|-----|---------|-------|
|
||||
| `reconnection` | boolean | true | تلاش مجدد برای اتصال |
|
||||
| `reconnectionAttempts` | number | 5 | تعداد تلاش مجدد |
|
||||
| `reconnectionDelay` | number | 1000 | تاخیر بین تلاشها (ms) |
|
||||
| `query` | object | - | پارامترهای query string |
|
||||
|
||||
### Return Values
|
||||
|
||||
| نام | نوع | توضیح |
|
||||
|-----|-----|-------|
|
||||
| `socket` | Socket \| null | instance سوکت |
|
||||
| `isConnected` | boolean | وضعیت اتصال |
|
||||
| `error` | Error \| null | خطای اتصال |
|
||||
| `emit` | function | ارسال event |
|
||||
| `on` | function | گوش دادن به event |
|
||||
| `off` | function | حذف listener |
|
||||
| `connect` | function | اتصال مجدد |
|
||||
| `disconnect` | function | قطع اتصال |
|
||||
|
||||
## 💡 مثالهای کاربردی
|
||||
|
||||
### 1️⃣ صفحه چت
|
||||
|
||||
```tsx
|
||||
function ChatPage() {
|
||||
const [messages, setMessages] = useState([]);
|
||||
const token = localStorage.getItem('token') || '';
|
||||
|
||||
const { isConnected, emit, on } = useSocket(
|
||||
'http://localhost:3000',
|
||||
token
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
on('chat:message', (message) => {
|
||||
setMessages(prev => [...prev, message]);
|
||||
});
|
||||
}, [on]);
|
||||
|
||||
const sendMessage = (text: string) => {
|
||||
emit('chat:send', { text, timestamp: Date.now() });
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{messages.map((msg, i) => (
|
||||
<div key={i}>{msg.text}</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 2️⃣ دریافت اعلانها
|
||||
|
||||
```tsx
|
||||
function Notifications() {
|
||||
const [notifications, setNotifications] = useState([]);
|
||||
const token = localStorage.getItem('authToken') || '';
|
||||
|
||||
const { on } = useSocket('http://localhost:4000', token);
|
||||
|
||||
useEffect(() => {
|
||||
on('notification:new', (notif) => {
|
||||
setNotifications(prev => [notif, ...prev]);
|
||||
});
|
||||
}, [on]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{notifications.map((n, i) => (
|
||||
<div key={i}>{n.message}</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 3️⃣ صفحه Pager
|
||||
|
||||
```tsx
|
||||
function PagerList() {
|
||||
const socketUrl = import.meta.env.VITE_SOCKET_URL;
|
||||
const token = localStorage.getItem('token') || '';
|
||||
|
||||
const { isConnected, emit, on } = useSocket(socketUrl, token);
|
||||
|
||||
useEffect(() => {
|
||||
on('page:new', (page) => {
|
||||
console.log('پیج جدید:', page);
|
||||
});
|
||||
|
||||
on('page:status', (status) => {
|
||||
console.log('وضعیت:', status);
|
||||
});
|
||||
}, [on]);
|
||||
|
||||
const callPage = (number: string) => {
|
||||
emit('page:call', { number });
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={isConnected ? 'text-green-500' : 'text-red-500'}>
|
||||
{isConnected ? 'آنلاین' : 'آفلاین'}
|
||||
</div>
|
||||
<button onClick={() => callPage('101')}>
|
||||
فراخوانی
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 4️⃣ استفاده با useAuth
|
||||
|
||||
```tsx
|
||||
function MyComponent() {
|
||||
const { token } = useAuth(); // hook شما برای احراز هویت
|
||||
const { isConnected, emit } = useSocket('http://localhost:3000', token);
|
||||
|
||||
return <div>{isConnected ? 'متصل' : 'قطع'}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
### 5️⃣ Custom Hook برای استفاده راحتتر
|
||||
|
||||
```tsx
|
||||
// hooks/useAppSocket.ts
|
||||
export const useAppSocket = () => {
|
||||
const token = localStorage.getItem('token') || '';
|
||||
const url = import.meta.env.VITE_SOCKET_URL || 'http://localhost:3000';
|
||||
|
||||
return useSocket(url, token, {
|
||||
reconnectionAttempts: 3,
|
||||
reconnectionDelay: 1500,
|
||||
});
|
||||
};
|
||||
|
||||
// استفاده در کامپوننت
|
||||
function MyComponent() {
|
||||
const { isConnected, emit, on } = useAppSocket();
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## 🎨 نمایش وضعیت اتصال
|
||||
|
||||
```tsx
|
||||
function ConnectionStatus() {
|
||||
const { isConnected, error } = useSocket('http://localhost:3000');
|
||||
|
||||
return (
|
||||
<div className={`
|
||||
px-3 py-1 rounded-full text-sm font-medium
|
||||
${isConnected
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-red-100 text-red-800'}
|
||||
`}>
|
||||
{isConnected ? '🟢 متصل' : '🔴 قطع'}
|
||||
{error && ` - خطا: ${error.message}`}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 🔥 نکات مهم
|
||||
|
||||
1. **Token Security**: توکن رو توی localStorage نگه دار
|
||||
2. **URL از .env**: آدرس سوکت رو توی `.env` بذار
|
||||
3. **Clean Up خودکار**: نگران پاک کردن listeners نباش
|
||||
4. **یک instance در هر کامپوننت**: هر کامپوننت instance جدید میسازه
|
||||
|
||||
## 🐛 رفع مشکلات
|
||||
|
||||
### خطای اتصال
|
||||
|
||||
```tsx
|
||||
const { error, isConnected } = useSocket(url, token);
|
||||
|
||||
if (error) {
|
||||
console.error('خطا:', error.message);
|
||||
// نمایش پیام به کاربر
|
||||
}
|
||||
```
|
||||
|
||||
### قطع و وصل دستی
|
||||
|
||||
```tsx
|
||||
const { connect, disconnect, isConnected } = useSocket(url, token);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isConnected ? (
|
||||
<button onClick={disconnect}>قطع</button>
|
||||
) : (
|
||||
<button onClick={connect}>اتصال</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
```
|
||||
|
||||
### چند event یکجا
|
||||
|
||||
```tsx
|
||||
useEffect(() => {
|
||||
on('event1', handler1);
|
||||
on('event2', handler2);
|
||||
on('event3', handler3);
|
||||
}, [on]);
|
||||
```
|
||||
|
||||
## ⚙️ تنظیمات محیط (.env)
|
||||
|
||||
```env
|
||||
VITE_SOCKET_URL=http://localhost:3000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**ساخته شده با ❤️ برای Danak**
|
||||
|
||||
موفق باشی! 🚀
|
||||
@@ -0,0 +1,70 @@
|
||||
import { useState, useMemo } from "react";
|
||||
import type { FilterValues } from "@/components/Filters";
|
||||
|
||||
const DEFAULT_PAGE = 1;
|
||||
const DEFAULT_LIMIT = 10;
|
||||
|
||||
export const usePagerFilters = () => {
|
||||
const [filters, setFilters] = useState<FilterValues>({});
|
||||
const [currentPage, setCurrentPage] = useState(DEFAULT_PAGE);
|
||||
const [limit] = useState(DEFAULT_LIMIT);
|
||||
|
||||
const handleFiltersChange = (newFilters: FilterValues) => {
|
||||
setFilters(newFilters);
|
||||
setCurrentPage(DEFAULT_PAGE);
|
||||
};
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
return (data: unknown[]) => {
|
||||
let filtered = [...data];
|
||||
|
||||
if (filters.search) {
|
||||
const searchTerm = (filters.search as string).toLowerCase();
|
||||
filtered = filtered.filter((item: unknown) => {
|
||||
const pager = item as { tableNumber?: string; message?: string };
|
||||
return (
|
||||
pager.tableNumber?.toLowerCase().includes(searchTerm) ||
|
||||
pager.message?.toLowerCase().includes(searchTerm)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (filters.status && filters.status !== "") {
|
||||
filtered = filtered.filter((item: unknown) => {
|
||||
const pager = item as { status?: string };
|
||||
return pager.status === filters.status;
|
||||
});
|
||||
}
|
||||
|
||||
return filtered;
|
||||
};
|
||||
}, [filters]);
|
||||
|
||||
const paginatedData = useMemo(() => {
|
||||
return (data: unknown[]) => {
|
||||
const startIndex = (currentPage - 1) * limit;
|
||||
return data.slice(startIndex, startIndex + limit);
|
||||
};
|
||||
}, [currentPage, limit]);
|
||||
|
||||
const totalPages = useMemo(() => {
|
||||
return (dataLength: number) => {
|
||||
return Math.ceil(dataLength / limit) || 1;
|
||||
};
|
||||
}, [limit]);
|
||||
|
||||
return {
|
||||
filters,
|
||||
currentPage,
|
||||
handleFiltersChange,
|
||||
handlePageChange,
|
||||
limit,
|
||||
filteredData,
|
||||
paginatedData,
|
||||
totalPages,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,189 @@
|
||||
import { useEffect, useRef, useState, useCallback } from "react";
|
||||
import { io, Socket } from "socket.io-client";
|
||||
|
||||
interface UseSocketOptions {
|
||||
reconnection?: boolean;
|
||||
reconnectionAttempts?: number;
|
||||
reconnectionDelay?: number;
|
||||
query?: Record<string, string>;
|
||||
}
|
||||
|
||||
interface UseSocketReturn {
|
||||
socket: Socket | null;
|
||||
isConnected: boolean;
|
||||
error: Error | null;
|
||||
emit: (event: string, data?: unknown) => void;
|
||||
on: (event: string, callback: (...args: unknown[]) => void) => void;
|
||||
off: (event: string, callback?: (...args: unknown[]) => void) => void;
|
||||
connect: () => void;
|
||||
disconnect: () => void;
|
||||
}
|
||||
|
||||
export const useSocket = (
|
||||
url: string,
|
||||
token?: string,
|
||||
options: UseSocketOptions = {}
|
||||
): UseSocketReturn => {
|
||||
const socketRef = useRef<Socket | null>(null);
|
||||
const coreListenersAttachedRef = useRef(false);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const listenersRef = useRef<Map<string, Set<(...args: unknown[]) => void>>>(
|
||||
new Map()
|
||||
);
|
||||
|
||||
const {
|
||||
reconnection = true,
|
||||
reconnectionAttempts = 5,
|
||||
reconnectionDelay = 1000,
|
||||
query,
|
||||
} = options;
|
||||
|
||||
const handleConnect = useCallback(() => {
|
||||
setIsConnected(true);
|
||||
setError(null);
|
||||
}, []);
|
||||
|
||||
const handleDisconnect = useCallback(() => {
|
||||
setIsConnected(false);
|
||||
}, []);
|
||||
|
||||
const handleConnectError = useCallback((err: Error) => {
|
||||
setError(err);
|
||||
setIsConnected(false);
|
||||
}, []);
|
||||
|
||||
const attachCoreListeners = useCallback(
|
||||
(socket: Socket) => {
|
||||
if (coreListenersAttachedRef.current) return;
|
||||
socket.on("connect", handleConnect);
|
||||
socket.on("disconnect", handleDisconnect);
|
||||
socket.on("connect_error", handleConnectError);
|
||||
coreListenersAttachedRef.current = true;
|
||||
},
|
||||
[handleConnect, handleDisconnect, handleConnectError]
|
||||
);
|
||||
|
||||
const connect = useCallback(() => {
|
||||
if (socketRef.current?.connected) return;
|
||||
|
||||
try {
|
||||
if (!socketRef.current) {
|
||||
const socketOptions: {
|
||||
autoConnect: boolean;
|
||||
reconnection: boolean;
|
||||
reconnectionAttempts: number;
|
||||
reconnectionDelay: number;
|
||||
auth?: { token: string };
|
||||
query?: Record<string, string>;
|
||||
} = {
|
||||
autoConnect: false,
|
||||
reconnection,
|
||||
reconnectionAttempts,
|
||||
reconnectionDelay,
|
||||
};
|
||||
|
||||
if (token) {
|
||||
socketOptions.auth = { token };
|
||||
}
|
||||
|
||||
if (query) {
|
||||
socketOptions.query = query;
|
||||
}
|
||||
|
||||
socketRef.current = io(url, socketOptions);
|
||||
attachCoreListeners(socketRef.current);
|
||||
} else {
|
||||
attachCoreListeners(socketRef.current);
|
||||
}
|
||||
|
||||
socketRef.current.connect();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error("خطا در اتصال به سوکت"));
|
||||
}
|
||||
}, [
|
||||
url,
|
||||
token,
|
||||
reconnection,
|
||||
reconnectionAttempts,
|
||||
reconnectionDelay,
|
||||
query,
|
||||
attachCoreListeners,
|
||||
]);
|
||||
|
||||
const disconnect = useCallback(() => {
|
||||
if (socketRef.current) {
|
||||
socketRef.current.disconnect();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const emit = useCallback((event: string, data?: unknown) => {
|
||||
if (socketRef.current?.connected) {
|
||||
socketRef.current.emit(event, data);
|
||||
} else {
|
||||
console.warn("سوکت متصل نیست. امکان ارسال داده وجود ندارد.");
|
||||
}
|
||||
}, []);
|
||||
|
||||
const on = useCallback(
|
||||
(event: string, callback: (...args: unknown[]) => void) => {
|
||||
if (!socketRef.current) return;
|
||||
|
||||
socketRef.current.on(event, callback);
|
||||
|
||||
if (!listenersRef.current.has(event)) {
|
||||
listenersRef.current.set(event, new Set());
|
||||
}
|
||||
listenersRef.current.get(event)!.add(callback);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const off = useCallback(
|
||||
(event: string, callback?: (...args: unknown[]) => void) => {
|
||||
if (!socketRef.current) return;
|
||||
|
||||
if (callback) {
|
||||
socketRef.current.off(event, callback);
|
||||
listenersRef.current.get(event)?.delete(callback);
|
||||
} else {
|
||||
socketRef.current.off(event);
|
||||
listenersRef.current.delete(event);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
connect();
|
||||
|
||||
return () => {
|
||||
listenersRef.current.forEach((callbacks, event) => {
|
||||
callbacks.forEach((callback) => {
|
||||
socketRef.current?.off(event, callback);
|
||||
});
|
||||
});
|
||||
listenersRef.current.clear();
|
||||
|
||||
if (socketRef.current) {
|
||||
socketRef.current.off("connect", handleConnect);
|
||||
socketRef.current.off("disconnect", handleDisconnect);
|
||||
socketRef.current.off("connect_error", handleConnectError);
|
||||
coreListenersAttachedRef.current = false;
|
||||
socketRef.current.disconnect();
|
||||
socketRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [connect, handleConnect, handleDisconnect, handleConnectError]);
|
||||
|
||||
return {
|
||||
socket: socketRef.current,
|
||||
isConnected,
|
||||
error,
|
||||
emit,
|
||||
on,
|
||||
off,
|
||||
connect,
|
||||
disconnect,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
export type Pager = {
|
||||
id: string;
|
||||
tableNumber: string;
|
||||
message: string;
|
||||
status: string;
|
||||
cookieId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type Pagers = {
|
||||
pagers: Pager[];
|
||||
};
|
||||
|
||||
export type PagersType = Pagers;
|
||||
@@ -38,6 +38,7 @@ import CreateCoupon from '@/pages/coupon/Create'
|
||||
import UpdateCoupon from '@/pages/coupon/Update'
|
||||
import ReviewsList from '@/pages/review/List'
|
||||
import DetailReview from '@/pages/review/Detail'
|
||||
import PagersList from '@/pages/pager/List'
|
||||
const MainRouter: FC = () => {
|
||||
|
||||
const { hasSubMenu } = useSharedStore()
|
||||
@@ -92,6 +93,8 @@ const MainRouter: FC = () => {
|
||||
|
||||
<Route path={Pages.reviews.list} element={<ReviewsList />} />
|
||||
<Route path={Pages.reviews.detail + ':id'} element={<DetailReview />} />
|
||||
|
||||
<Route path={Pages.pagers.list} element={<PagersList />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+10
-1
@@ -2,7 +2,7 @@ import { type FC, useEffect } from 'react'
|
||||
import LogoImage from '../assets/images/logo.svg'
|
||||
import LogoSmall from '../assets/images/logo-small.svg'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { DocumentText, Home2, Logout, Message, NotificationStatus, People, Setting2, TicketDiscount, Chart, Calendar, Security, Card, SecurityUser, TruckFast, Element3, Star1 } from 'iconsax-react'
|
||||
import { DocumentText, Home2, Logout, Message, NotificationStatus, People, Setting2, TicketDiscount, Chart, Calendar, Security, Card, SecurityUser, TruckFast, Element3, Star1, NotificationBing } from 'iconsax-react'
|
||||
import SideBarItem from './SideBarItem'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { Pages } from '../config/Pages'
|
||||
@@ -129,6 +129,15 @@ const SideBar: FC = () => {
|
||||
activeName='reviews'
|
||||
/>
|
||||
|
||||
<SideBarItem
|
||||
icon={<NotificationBing variant={isActive('pagers') ? 'Bold' : 'Outline'} color={isActive('pagers') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.pagers')}
|
||||
isActive={isActive('pagers')}
|
||||
link={Pages.pagers.list}
|
||||
name='pagers'
|
||||
activeName='pagers'
|
||||
/>
|
||||
|
||||
<SideBarItem
|
||||
icon={<SecurityUser variant={isActive('admins') ? 'Bold' : 'Outline'} color={isActive('admins') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.admins')}
|
||||
|
||||
Reference in New Issue
Block a user