ironie-nextjs/src/app/components/NotificationDropdown.tsx
2025-08-10 23:51:46 +02:00

182 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import { useEffect, useRef } from 'react'
import Button from './Button'
import { formatDistanceToNow } from 'date-fns'
import { de } from 'date-fns/locale'
type Notification = {
id: string
text: string
read: boolean
actionType?: string // z.B. "team-invite" | "team-join-request"
actionData?: string // invitationId oder teamId
createdAt?: string
}
type Props = {
notifications: Notification[]
markAllAsRead: () => void
onSingleRead: (id: string) => void
onClose: () => void
onAction: (action: 'accept' | 'reject', invitationId: string) => void
onClickNotification?: (notification: Notification) => void
}
export default function NotificationDropdown({
notifications,
markAllAsRead,
onSingleRead,
onClose,
onAction,
onClickNotification
}: Props) {
const dropdownRef = useRef<HTMLDivElement>(null)
/* --- Klick außerhalb schließt Dropdown ------------------------- */
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
onClose()
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [onClose])
/* --- Render ----------------------------------------------------- */
return (
<div
ref={dropdownRef}
className="absolute bottom-20 right-0 w-80 bg-white dark:bg-neutral-800 border border-gray-200 dark:border-neutral-700 rounded-lg shadow-xl overflow-hidden z-50"
>
{/* Kopfzeile */}
<div className="p-2 flex justify-between items-center border-b border-gray-200 dark:border-neutral-700">
<span className="font-semibold text-gray-800 dark:text-white">
Benachrichtigungen
</span>
<Button
title="Alle als gelesen markieren"
onClick={markAllAsRead}
variant="solid"
color="blue"
size="sm"
className="p-2"
>
{/* Icon */}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" className="w-4 h-4" fill="currentColor">
<path d="M255.4 48.2c.2-.1.4-.2.6-.2s.4.1.6.2L460.6 194c2.1 1.5 3.4 3.9 3.4 6.5v13.6L291.5 355.7c-20.7 17-50.4 17-71.1 0L48 214.1v-13.6c0-2.6 1.2-5 3.4-6.5L255.4 48.2zM48 276.2L190 392.8c38.4 31.5 93.7 31.5 132 0L464 276.2V456c0 4.4-3.6 8-8 8H56c-4.4 0-8-3.6-8-8V276.2zM256 0c-10.2 0-20.2 3.2-28.5 9.1L23.5 154.9C8.7 165.4 0 182.4 0 200.5V456c0 30.9 25.1 56 56 56h400c30.9 0 56-25.1 56-56V200.5c0-18.1-8.7-35.1-23.4-45.6L284.5 9.1C276.2 3.2 266.2 0 256 0z" />
</svg>
</Button>
</div>
{/* Liste */}
<div className="max-h-60 overflow-y-auto">
{notifications.length === 0 ? (
<div className="p-4 text-center text-gray-500 dark:text-neutral-400">
Keine Benachrichtigungen
</div>
) : (
notifications.map((n) => {
const needsAction =
!n.read &&
(n.actionType === 'team-invite' ||
n.actionType === 'team-join-request')
return (
<div
key={n.id}
className="grid grid-cols-[auto_1fr_auto] items-center gap-2 py-3 px-2 border-b border-gray-200 dark:border-neutral-700 text-sm hover:bg-gray-50 dark:hover:bg-neutral-700 cursor-pointer"
onClick={() => {
onClickNotification?.(n) // ⬅️ Navigation / Weiterverarbeitung
if (!n.read) onSingleRead(n.id) // ⬅️ erst danach als gelesen markieren
}}
>
{/* roter Punkt */}
<div className="flex items-center justify-center h-full">
<span
className={`inline-block w-2 h-2 rounded-full ${
n.read ? 'bg-transparent' : 'bg-red-500'
}`}
/>
</div>
{/* Text + Timestamp */}
<div className="flex flex-col gap-1">
<span
className={
n.read
? 'text-gray-600 dark:text-neutral-400'
: 'font-semibold text-gray-900 dark:text-white'
}
>
{n.text}
</span>
<span className="text-xs text-gray-400 dark:text-neutral-500">
{n.createdAt &&
formatDistanceToNow(new Date(n.createdAt), {
addSuffix: true,
locale: de,
})}
</span>
</div>
{/* Aktionen */}
<div className="flex items-center gap-1">
{needsAction ? (
<>
<Button
onClick={(e) => {
e.stopPropagation()
onAction('accept', n.actionData ?? n.id)
onSingleRead(n.id)
}}
className="px-2 py-1 text-xs font-medium rounded bg-green-600 text-white hover:bg-green-700"
color="green"
size="sm"
>
</Button>
<Button
onClick={(e) => {
e.stopPropagation()
onAction('reject', n.actionData ?? n.id)
onSingleRead(n.id)
}}
className="px-2 py-1 text-xs font-medium rounded bg-red-600 text-white hover:bg-red-700"
color="red"
size="sm"
>
</Button>
</>
) : (
!n.read && (
<Button
onClick={() => onSingleRead(n.id)}
title="Als gelesen markieren"
className="p-1 text-gray-400 hover:text-gray-700 dark:hover:text-white"
color="gray"
size="sm"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" className="w-4 h-4" fill="currentColor">
<path d="M255.4 48.2c.2-.1.4-.2.6-.2s.4.1.6.2L460.6 194c2.1 1.5 3.4 3.9 3.4 6.5v13.6L291.5 355.7c-20.7 17-50.4 17-71.1 0L48 214.1v-13.6c0-2.6 1.2-5 3.4-6.5L255.4 48.2zM48 276.2L190 392.8c38.4 31.5 93.7 31.5 132 0L464 276.2V456c0 4.4-3.6 8-8 8H56c-4.4 0-8-3.6-8-8V276.2zM256 0c-10.2 0-20.2 3.2-28.5 9.1L23.5 154.9C8.7 165.4 0 182.4 0 200.5V456c0 30.9 25.1 56 56 56h400c30.9 0 56-25.1 56-56V200.5c0-18.1-8.7-35.1-23.4-45.6L284.5 9.1C276.2 3.2 266.2 0 256 0z" />
</svg>
</Button>
)
)}
</div>
</div>
)
})
)}
</div>
</div>
)
}