notification with sound

This commit is contained in:
hamid zarghami
2025-12-13 10:58:13 +03:30
parent 90f04f8c82
commit c2934e4b81
4 changed files with 169 additions and 10 deletions
Binary file not shown.
+67 -10
View File
@@ -1,36 +1,78 @@
import { getToken } from '@/config/func'
import { useSocket } from '@/pages/pager/hooks/useSocket'
import { type FC, useEffect, useCallback } from 'react'
import { type FC, useEffect, useCallback, useState } from 'react'
import type { NotificationPayload } from '@/types/notification.types'
import NotificationItem from '@/components/NotificationItem'
interface NotificationItemData extends NotificationPayload {
id: string
timestamp: number
}
const Notification: FC = () => {
const [notifications, setNotifications] = useState<NotificationItemData[]>([])
const [audioEnabled, setAudioEnabled] = useState(false)
const token = getToken()
const socketUrl = `${import.meta.env.VITE_SOCKET_URL}/notifications`
const { connect, socket, error } = useSocket(socketUrl, token!)
// لاگ کردن خطاها
useEffect(() => {
if (error) {
console.error('❌ Notification: خطا در اتصال:', error)
}
}, [error])
useEffect(() => {
connect()
}, [connect])
const handleGetNotification = useCallback((data: NotificationPayload) => {
useEffect(() => {
const enableAudio = async () => {
try {
const audio = new Audio('/mixkit-correct-answer-tone-2870.wav')
audio.volume = 0
await audio.play()
audio.pause()
audio.currentTime = 0
setAudioEnabled(true)
} catch {
console.warn('صدا هنوز فعال نشده است')
}
}
console.log('✅ Notification: دریافت نوتیفیکیشن‌ها:', data)
// alert('دریافت نوتیفیکیشن‌ها: ' + JSON.stringify(data))
const handleUserInteraction = () => {
if (!audioEnabled) {
enableAudio()
}
}
window.addEventListener('click', handleUserInteraction, { once: true })
window.addEventListener('touchstart', handleUserInteraction, { once: true })
window.addEventListener('keydown', handleUserInteraction, { once: true })
return () => {
window.removeEventListener('click', handleUserInteraction)
window.removeEventListener('touchstart', handleUserInteraction)
window.removeEventListener('keydown', handleUserInteraction)
}
}, [audioEnabled])
const handleGetNotification = useCallback((data: NotificationPayload) => {
const newNotification: NotificationItemData = {
...data,
id: `${Date.now()}-${Math.random()}`,
timestamp: Date.now(),
}
setNotifications((prev) => [...prev, newNotification])
}, [])
const handleRemoveNotification = useCallback((id: string) => {
setNotifications((prev) => prev.filter((notif) => notif.id !== id))
}, [])
useEffect(() => {
if (socket) {
socket.on('joined', () => null)
socket.on('notifications', handleGetNotification)
@@ -40,9 +82,24 @@ const Notification: FC = () => {
}
}, [socket, handleGetNotification])
if (notifications.length === 0) {
return null
}
return null
return (
<div className="fixed right-4 top-4 z-50 flex flex-col gap-2 max-w-[320px] w-full">
{notifications.map((notification) => (
<NotificationItem
key={notification.id}
id={notification.id}
subject={notification.subject}
body={notification.body}
onRemove={handleRemoveNotification}
audioEnabled={audioEnabled}
/>
))}
</div>
)
}
export default Notification
export default Notification
+97
View File
@@ -0,0 +1,97 @@
import { type FC, useEffect } from 'react'
import { CloseCircle, NotificationBing, ShoppingBag, MessageText1 } from 'iconsax-react'
import Button from '@/components/Button'
import { NotificationSubject } from '@/types/notification.types'
interface NotificationItemProps {
id: string
subject: string
body: string
onRemove: (id: string) => void
audioEnabled: boolean
}
const NotificationItem: FC<NotificationItemProps> = ({ id, subject, body, onRemove, audioEnabled }) => {
useEffect(() => {
if (!audioEnabled) {
return
}
const playNotificationSound = async () => {
try {
const audio = new Audio('/mixkit-correct-answer-tone-2870.wav')
audio.volume = 0.5
audio.addEventListener('error', (err) => {
console.error('❌ خطا در لود فایل صوتی:', err)
}, { once: true })
await audio.play()
} catch (err) {
console.warn('خطا در پخش صدا:', err)
}
}
playNotificationSound()
}, [audioEnabled])
const getNotificationConfig = (subjectType: string) => {
switch (subjectType) {
case NotificationSubject.PAGER_CREATED:
return {
icon: <MessageText1 color="white" size={16} variant="Bold" />,
title: 'پیجر جدید',
}
case NotificationSubject.ORDER_CREATED:
return {
icon: <ShoppingBag color="white" size={16} variant="Bold" />,
title: 'سفارش جدید',
}
default:
return {
icon: <NotificationBing color="white" size={16} variant="Bold" />,
title: 'اطلاعیه',
}
}
}
const config = getNotificationConfig(subject)
return (
<div className="bg-white border border-gray-200 rounded-lg p-3 shadow-sm transition-all duration-200 hover:shadow-md">
<div className="flex items-start gap-2.5">
<div className="bg-black p-1.5 rounded flex-shrink-0">
<div className="text-white">
{config.icon}
</div>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-1">
<h4 className="text-xs font-semibold text-gray-900">
{config.title}
</h4>
<button
onClick={() => onRemove(id)}
className="flex-shrink-0 text-gray-400 hover:text-gray-600 transition-colors p-0.5 rounded hover:bg-gray-100"
aria-label="بستن"
>
<CloseCircle color="gray" size={18} />
</button>
</div>
<p className="text-xs text-gray-700 leading-relaxed mb-2">
{body || 'نوتیفیکیشن جدید'}
</p>
<div className="flex justify-end">
<Button
onClick={() => onRemove(id)}
className="w-fit px-4 h-7 text-xs"
>
دیدم
</Button>
</div>
</div>
</div>
</div>
)
}
export default NotificationItem
+5
View File
@@ -2,3 +2,8 @@ export interface NotificationPayload {
subject: string;
body: string;
}
export const enum NotificationSubject {
PAGER_CREATED = "pager.created",
ORDER_CREATED = "order.created",
}