183 lines
5.8 KiB
TypeScript
183 lines
5.8 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useState } from 'react'
|
|
import NotificationDropdown from './NotificationDropdown'
|
|
import { useWS } from '@/app/lib/wsStore'
|
|
import { useSession } from 'next-auth/react'
|
|
import { useTeamManager } from '../hooks/useTeamManager'
|
|
import { useRouter } from 'next/navigation'
|
|
|
|
type Notification = {
|
|
id: string
|
|
text: string
|
|
read: boolean
|
|
actionType?: string
|
|
actionData?: string
|
|
createdAt?: string
|
|
}
|
|
|
|
export default function NotificationCenter() {
|
|
const { data: session } = useSession()
|
|
const [notifications, setNotifications] = useState<Notification[]>([])
|
|
const [open, setOpen] = useState(false)
|
|
const { socket, connect } = useWS()
|
|
const { markAllAsRead, markOneAsRead, handleInviteAction } = useTeamManager({}, null)
|
|
const router = useRouter()
|
|
const [previewText, setPreviewText] = useState<string | null>(null)
|
|
const [showPreview, setShowPreview] = useState(false)
|
|
const [animateBell, setAnimateBell] = useState(false)
|
|
|
|
useEffect(() => {
|
|
const steamId = session?.user?.steamId
|
|
if (!steamId) return
|
|
|
|
const loadNotifications = async () => {
|
|
try {
|
|
const res = await fetch('/api/notifications/user')
|
|
if (!res.ok) throw new Error('Fehler beim Laden')
|
|
const data = await res.json()
|
|
const loaded = data.notifications.map((n: any) => ({
|
|
id: n.id,
|
|
text: n.message,
|
|
read: n.read,
|
|
actionType: n.actionType,
|
|
actionData: n.actionData,
|
|
createdAt: n.createdAt,
|
|
}))
|
|
setNotifications(loaded)
|
|
} catch (err) {
|
|
console.error('[NotificationCenter] Fehler beim Laden:', err)
|
|
}
|
|
}
|
|
|
|
loadNotifications()
|
|
connect(steamId)
|
|
}, [session?.user?.steamId, connect])
|
|
|
|
useEffect(() => {
|
|
if (!socket) return
|
|
|
|
const handleMessage = (event: MessageEvent) => {
|
|
const data = JSON.parse(event.data)
|
|
if (data.type === 'heartbeat') return
|
|
|
|
const isNotificationType = [
|
|
'notification',
|
|
'invitation',
|
|
'team-invite',
|
|
'team-joined',
|
|
'team-member-joined',
|
|
'team-kick',
|
|
'team-kick-other',
|
|
'team-left',
|
|
'team-member-left',
|
|
'team-leader-changed',
|
|
'team-join-request'
|
|
].includes(data.type)
|
|
|
|
if (!isNotificationType) return
|
|
|
|
const newNotification: Notification = {
|
|
id: data.id,
|
|
text: data.message || 'Neue Benachrichtigung',
|
|
read: false,
|
|
actionType: data.actionType,
|
|
actionData: data.actionData,
|
|
createdAt: data.createdAt,
|
|
}
|
|
|
|
setNotifications(prev => [newNotification, ...prev])
|
|
setPreviewText(newNotification.text)
|
|
setShowPreview(true)
|
|
setAnimateBell(true)
|
|
|
|
setTimeout(() => {
|
|
setShowPreview(false)
|
|
setTimeout(() => {
|
|
setPreviewText(null)
|
|
}, 300)
|
|
setAnimateBell(false)
|
|
}, 3000)
|
|
|
|
|
|
}
|
|
|
|
socket.addEventListener('message', handleMessage)
|
|
return () => socket.removeEventListener('message', handleMessage)
|
|
}, [socket])
|
|
|
|
return (
|
|
<div className="fixed bottom-6 right-6 z-50">
|
|
<button
|
|
type="button"
|
|
onClick={() => setOpen(prev => !prev)}
|
|
className={`relative flex items-center transition-all duration-300 ease-in-out
|
|
${showPreview ? 'w-[320px] pl-4 pr-11' : 'w-[44px] justify-center'}
|
|
h-11 rounded-lg border border-gray-200 bg-white text-gray-800 shadow-2xs
|
|
dark:bg-neutral-900 dark:border-neutral-700 dark:text-white`}
|
|
>
|
|
{/* Vorschautext */}
|
|
{previewText && (
|
|
<span className="truncate text-sm text-gray-800 dark:text-white">
|
|
{previewText}
|
|
</span>
|
|
)}
|
|
|
|
{/* Notification Bell (absolut rechts innerhalb des Buttons) */}
|
|
<div className="absolute right-0 top-0 bottom-0 w-11 flex items-center justify-center">
|
|
<svg
|
|
className={`shrink-0 size-6 transition-transform ${animateBell ? 'animate-shake' : ''}`}
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14V11a6.002 6.002 0 00-4-5.659V4a2 2 0 00-4 0v1.341C7.67 6.165 6 8.388 6 11v3c0 .828-.672 1.5-1.5 1.5H4v1h5m6 0v1a2 2 0 11-4 0v-1h4z"
|
|
/>
|
|
</svg>
|
|
|
|
{notifications.some(n => !n.read) && (
|
|
<span className="flex absolute top-0 end-0 -mt-1 -me-1">
|
|
<span className="animate-ping absolute inline-flex size-5 rounded-full bg-red-400 opacity-75 dark:bg-red-600"></span>
|
|
<span className="relative inline-flex items-center justify-center size-5 rounded-full text-xs font-bold bg-red-500 text-white">
|
|
{notifications.filter(n => !n.read).length}
|
|
</span>
|
|
</span>
|
|
)}
|
|
</div>
|
|
</button>
|
|
|
|
{/* Dropdown */}
|
|
{open && (
|
|
<NotificationDropdown
|
|
notifications={notifications}
|
|
markAllAsRead={async () => {
|
|
await markAllAsRead()
|
|
setNotifications(prev => prev.map(n => ({ ...n, read: true })))
|
|
}}
|
|
onSingleRead={async (id) => {
|
|
await markOneAsRead(id)
|
|
setNotifications(prev => prev.map(n => (n.id === id ? { ...n, read: true } : n)))
|
|
}}
|
|
onClose={() => setOpen(false)}
|
|
onAction={async (action, id) => {
|
|
await handleInviteAction(action, id)
|
|
setNotifications(prev =>
|
|
prev.map(n =>
|
|
n.actionData === id
|
|
? { ...n, read: true, actionType: undefined, actionData: undefined }
|
|
: n
|
|
)
|
|
)
|
|
if (action === 'accept') router.refresh()
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|