use socket

This commit is contained in:
hamid zarghami
2025-12-10 12:34:55 +03:30
parent afc295860e
commit abae41914f
14 changed files with 941 additions and 4 deletions
+2
View File
@@ -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'
+137 -1
View File
@@ -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",
+1
View File
@@ -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",
+3
View File
@@ -75,4 +75,7 @@ export const Pages = {
list: "/reviews/list",
detail: "/reviews/detail/",
},
pagers: {
list: "/pagers/list",
},
};
+2 -1
View File
@@ -65,7 +65,8 @@
"payment_methods": "روش های پرداخت",
"admins": "مدیران",
"shipment_methods": "روش های ارسال",
"reviews": "نظرات"
"reviews": "نظرات",
"pagers": "پیجرها"
},
"shipment": {
"dineIn": "سرو در محل",
+109
View File
@@ -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',
})
}
},
]
}
+317
View File
@@ -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**
موفق باشی! 🚀
+70
View File
@@ -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,
};
};
+189
View File
@@ -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,
};
};
+15
View File
@@ -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;
+3
View File
@@ -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
View File
@@ -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')}