ironie-nextjs/src/app/components/NotificationCenter.tsx
2025-05-28 00:41:23 +02:00

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>
)
}