Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import { formatDistanceToNow } from 'date-fns';
|
|
|
|
|
import { useRouter } from 'next/navigation';
|
|
|
|
|
|
|
|
|
|
interface NotificationItemProps {
|
|
|
|
|
notification: {
|
|
|
|
|
id: string;
|
|
|
|
|
type: string;
|
|
|
|
|
title: string;
|
|
|
|
|
description: string | null;
|
|
|
|
|
link: string | null;
|
|
|
|
|
isRead: boolean;
|
|
|
|
|
createdAt: Date;
|
|
|
|
|
};
|
|
|
|
|
onMarkRead: (id: string) => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function NotificationItem({ notification, onMarkRead }: NotificationItemProps) {
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
|
|
|
|
|
const handleClick = () => {
|
|
|
|
|
if (!notification.isRead) {
|
|
|
|
|
onMarkRead(notification.id);
|
|
|
|
|
}
|
|
|
|
|
if (notification.link) {
|
2026-03-26 12:06:18 +01:00
|
|
|
router.push(notification.link);
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={handleClick}
|
|
|
|
|
className="w-full text-left flex items-start gap-3 px-4 py-3 hover:bg-muted/50 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
{/* Unread indicator */}
|
|
|
|
|
<div className="mt-1.5 flex-shrink-0">
|
|
|
|
|
{!notification.isRead ? (
|
|
|
|
|
<span className="block h-2 w-2 rounded-full bg-blue-500" />
|
|
|
|
|
) : (
|
|
|
|
|
<span className="block h-2 w-2 rounded-full bg-transparent" />
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Content */}
|
|
|
|
|
<div className="flex-1 min-w-0">
|
|
|
|
|
<p
|
|
|
|
|
className={`text-sm leading-snug truncate ${
|
|
|
|
|
notification.isRead ? 'text-muted-foreground' : 'text-foreground font-medium'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
{notification.title}
|
|
|
|
|
</p>
|
|
|
|
|
{notification.description && (
|
|
|
|
|
<p className="text-xs text-muted-foreground mt-0.5 line-clamp-2">
|
|
|
|
|
{notification.description}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
|
|
|
{formatDistanceToNow(new Date(notification.createdAt), { addSuffix: true })}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
}
|