147 lines
4.1 KiB
TypeScript
147 lines
4.1 KiB
TypeScript
/**
|
|
* NotificationItem — single notification row inside the dropdown.
|
|
*
|
|
* Props:
|
|
* notification: NotificationItem
|
|
* onMarkRead: (id: string) => void
|
|
*/
|
|
|
|
import React from 'react';
|
|
import clsx from 'clsx';
|
|
import {
|
|
Info,
|
|
Mail,
|
|
CheckSquare,
|
|
Calendar,
|
|
AlertCircle,
|
|
User,
|
|
FileText,
|
|
Bell,
|
|
type LucideIcon,
|
|
} from 'lucide-react';
|
|
import type { NotificationItem as NotificationItemType } from '@/api/notifications';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
// ── helpers ──
|
|
|
|
/**
|
|
* Returns a German relative-time string like "vor 5 Min" or "vor 2 Stunden".
|
|
* Falls back to "gerade eben" for < 1 min and an absolute date for > 7 days.
|
|
*/
|
|
function relativeTime(isoDate: string | null | undefined): string {
|
|
if (!isoDate) return '';
|
|
const now = Date.now();
|
|
const then = new Date(isoDate).getTime();
|
|
if (Number.isNaN(then)) return '';
|
|
const diffMs = now - then;
|
|
if (diffMs < 0) return 'gerade eben';
|
|
const diffMin = Math.floor(diffMs / 60000);
|
|
if (diffMin < 1) return 'gerade eben';
|
|
if (diffMin < 60) return `vor ${diffMin} Min`;
|
|
const diffHrs = Math.floor(diffMin / 60);
|
|
if (diffHrs < 24) return `vor ${diffHrs} Std`;
|
|
const diffDays = Math.floor(diffHrs / 24);
|
|
if (diffDays < 7) return `vor ${diffDays} ${diffDays === 1 ? 'Tag' : 'Tagen'}`;
|
|
// > 7 days: show absolute date
|
|
return new Date(isoDate).toLocaleDateString('de-DE', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric',
|
|
});
|
|
}
|
|
|
|
/** Map notification type → lucide icon. */
|
|
const typeIconMap: Record<string, LucideIcon> = {
|
|
info: Info,
|
|
email: Mail,
|
|
mail: Mail,
|
|
task: CheckSquare,
|
|
calendar: Calendar,
|
|
event: Calendar,
|
|
alert: AlertCircle,
|
|
warning: AlertCircle,
|
|
error: AlertCircle,
|
|
contact: User,
|
|
user: User,
|
|
document: FileText,
|
|
file: FileText,
|
|
};
|
|
|
|
function getIconForType(type: string): LucideIcon {
|
|
return typeIconMap[type] ?? Bell;
|
|
}
|
|
|
|
// ── component ──
|
|
|
|
export interface NotificationItemProps {
|
|
notification: NotificationItemType;
|
|
onMarkRead: (id: string) => void;
|
|
}
|
|
|
|
export function NotificationItem({ notification, onMarkRead }: NotificationItemProps) {
|
|
const { t } = useTranslation();
|
|
const isUnread = notification.read_at == null;
|
|
const Icon = getIconForType(notification.type);
|
|
|
|
const handleClick = () => {
|
|
if (isUnread) {
|
|
onMarkRead(notification.id);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<button
|
|
onClick={handleClick}
|
|
className={clsx(
|
|
'w-full text-left flex items-start gap-3 px-3 py-2.5 hover:bg-secondary-50 transition-colors cursor-pointer',
|
|
'border-b border-secondary-100 last:border-b-0 min-h-touch focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500',
|
|
isUnread && 'bg-primary-50/40',
|
|
)}
|
|
role="menuitem"
|
|
aria-label={notification.title}
|
|
>
|
|
{/* Icon */}
|
|
<span
|
|
className={clsx(
|
|
'flex-shrink-0 mt-0.5 w-8 h-8 rounded-full flex items-center justify-center',
|
|
isUnread ? 'bg-primary-100 text-primary-600' : 'bg-secondary-100 text-secondary-500',
|
|
)}
|
|
aria-hidden="true"
|
|
>
|
|
<Icon className="w-4 h-4" strokeWidth={2} />
|
|
</span>
|
|
|
|
{/* Content */}
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2">
|
|
<p
|
|
className={clsx(
|
|
'text-sm truncate',
|
|
isUnread ? 'font-semibold text-secondary-900' : 'font-medium text-secondary-700',
|
|
)}
|
|
>
|
|
{notification.title}
|
|
</p>
|
|
{isUnread && (
|
|
<span
|
|
className="flex-shrink-0 w-2 h-2 rounded-full bg-primary-500"
|
|
aria-label={t('notificationItem.ungelesen')}
|
|
title={t('notificationItem.ungelesen')}
|
|
/>
|
|
)}
|
|
</div>
|
|
{notification.body && (
|
|
<p className="text-xs text-secondary-500 truncate mt-0.5">
|
|
{notification.body}
|
|
</p>
|
|
)}
|
|
{notification.created_at && (
|
|
<p className="text-xs text-secondary-400 mt-1">
|
|
{relativeTime(notification.created_at)}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</button>
|
|
);
|
|
}
|