Ran the official @tailwindcss/upgrade tool: - tailwind.config.ts → @theme directive in globals.css - @tailwind base/components/utilities → @import 'tailwindcss' - postcss.config switched from tailwindcss + autoprefixer to @tailwindcss/postcss (autoprefixer baked in) - focus-visible:outline-none → focus-visible:outline-hidden (the v3 utility was a footgun — outline still showed in forced-colors mode) Reverted the migration tool's over-zealous variant="outline" → variant="outline-solid" rename on CVA prop values; that rename was meant for the Tailwind `outline:` utility, not our Button/Badge component variants. Swapped tailwindcss-animate (v3-style JS plugin) for tw-animate-css (v4-native @import). Same utility surface (animate-spin, animate-in, etc.), one fewer JS plugin in the bundle. Fixed the upgrade tool's malformed dark variant (@custom-variant dark (&:is(class *)) — `class` was being parsed as a tag) to canonical &:where(.dark, .dark *). Verified: tsc 0 errors, eslint 0 errors (16 pre-existing warnings), vitest 1315/1315, next build clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
68 lines
1.8 KiB
TypeScript
68 lines
1.8 KiB
TypeScript
'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) {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
router.push(notification.link as any);
|
|
}
|
|
};
|
|
|
|
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 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>
|
|
);
|
|
}
|