notifications

This commit is contained in:
alma 2025-05-04 12:18:34 +02:00
parent 9b62063962
commit 3275b8b6c9
5 changed files with 70 additions and 3 deletions

View File

@ -74,6 +74,38 @@
}
}
/* Notification message styles */
.notification-message {
max-width: 100%;
word-wrap: break-word;
line-height: 1.4;
}
.notification-message p {
margin: 0.25rem 0;
}
.notification-message img {
max-width: 16px;
height: 16px;
display: inline-block;
vertical-align: middle;
border-radius: 50%;
margin-right: 4px;
}
.notification-message a.userMention {
font-weight: 500;
color: #3b82f6;
text-decoration: none;
display: inline-flex;
align-items: center;
}
.notification-message a:hover {
text-decoration: underline;
}
/* Email display styles */
.email-content-display {
max-width: 100%;

View File

@ -13,6 +13,7 @@ import {
DropdownMenuSeparator,
} from '@/components/ui/dropdown-menu';
import { formatDistanceToNow } from 'date-fns';
import { SafeHTML } from '@/components/safe-html';
interface NotificationBadgeProps {
className?: string;
@ -168,7 +169,7 @@ export const NotificationBadge = memo(function NotificationBadge({ className }:
<DropdownMenuItem key={notification.id} className="px-4 py-3 cursor-default">
<div className="w-full">
<div className="flex items-start justify-between">
<div>
<div className="max-w-[90%]">
<p className="text-sm font-medium">
{notification.title}
{!notification.isRead && (
@ -179,7 +180,7 @@ export const NotificationBadge = memo(function NotificationBadge({ className }:
{formatDistanceToNow(new Date(notification.timestamp), { addSuffix: true })}
</p>
</div>
<div className="flex space-x-1">
<div className="flex space-x-1 ml-2">
{!notification.isRead && (
<Button
variant="ghost"
@ -201,7 +202,10 @@ export const NotificationBadge = memo(function NotificationBadge({ className }:
)}
</div>
</div>
<p className="text-xs mt-1">{notification.message}</p>
<SafeHTML
html={notification.message}
className="text-xs mt-1 notification-message"
/>
</div>
</DropdownMenuItem>
))}

29
components/safe-html.tsx Normal file
View File

@ -0,0 +1,29 @@
import React from 'react';
import DOMPurify from 'dompurify';
interface SafeHTMLProps {
html: string;
className?: string;
}
export function SafeHTML({ html, className }: SafeHTMLProps) {
const sanitizedHTML = DOMPurify.sanitize(html, {
USE_PROFILES: { html: true },
ALLOWED_TAGS: [
'a', 'p', 'br', 'b', 'i', 'em', 'strong', 'span', 'div',
'img', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'code', 'pre', 'blockquote'
],
ALLOWED_ATTR: [
'href', 'target', 'class', 'id', 'style', 'src', 'alt',
'data-tagged-user-id', 'data-mention'
]
});
return (
<div
className={className}
dangerouslySetInnerHTML={{ __html: sanitizedHTML }}
/>
);
}

1
package-lock.json generated
View File

@ -57,6 +57,7 @@
"cookies-next": "^5.1.0",
"crypto-js": "^4.2.0",
"date-fns": "^3.6.0",
"dompurify": "^3.2.5",
"dotenv": "^16.5.0",
"embla-carousel-react": "8.5.1",
"fullcalendar": "^6.1.15",

View File

@ -58,6 +58,7 @@
"cookies-next": "^5.1.0",
"crypto-js": "^4.2.0",
"date-fns": "^3.6.0",
"dompurify": "^3.2.5",
"dotenv": "^16.5.0",
"embla-carousel-react": "8.5.1",
"fullcalendar": "^6.1.15",