136 lines
5.7 KiB
JavaScript
136 lines
5.7 KiB
JavaScript
import { useStore } from '@nanostores/preact';
|
|
import { useEffect, useMemo, useState } from 'preact/hooks';
|
|
import {
|
|
$isRealtimeConnected,
|
|
$notifications,
|
|
$unreadNotifications,
|
|
connectNotifications,
|
|
fetchNotifications,
|
|
markAllNotificationsAsRead,
|
|
markNotificationAsRead,
|
|
} from '../stores/notificationStore';
|
|
import { $accessToken, $user } from '../stores/authStore';
|
|
import NotificationDetailModal from './notifications/NotificationDetailModal';
|
|
|
|
function formatTimestamp(value) {
|
|
if (!value) return '';
|
|
const date = new Date(value);
|
|
if (Number.isNaN(date.getTime())) return '';
|
|
return new Intl.DateTimeFormat('hr-HR', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
}).format(date);
|
|
}
|
|
|
|
export default function NotificationBell() {
|
|
const [mounted, setMounted] = useState(false);
|
|
const [open, setOpen] = useState(false);
|
|
const [selectedNotification, setSelectedNotification] = useState(null);
|
|
const notifications = useStore($notifications);
|
|
const unread = useStore($unreadNotifications);
|
|
const isConnected = useStore($isRealtimeConnected);
|
|
const token = useStore($accessToken);
|
|
const user = useStore($user);
|
|
|
|
useEffect(() => {
|
|
setMounted(true);
|
|
}, []);
|
|
|
|
const latestNotifications = useMemo(
|
|
() => notifications.slice(0, 6),
|
|
[notifications]
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (!token || !user?.id) return;
|
|
fetchNotifications();
|
|
connectNotifications();
|
|
}, [token, user?.id]);
|
|
|
|
// Stabilan placeholder za SSR + prvi client render.
|
|
if (!mounted || !token) {
|
|
return <div className="h-10 w-10" aria-hidden="true" />;
|
|
}
|
|
|
|
return (
|
|
<div className="relative">
|
|
<button
|
|
type="button"
|
|
onClick={() => setOpen((current) => !current)}
|
|
className="relative inline-flex h-10 w-10 items-center justify-center rounded-lg border border-border-hairline bg-canvas-base text-text-muted hover:bg-canvas-deep"
|
|
aria-label="Notifikacije"
|
|
>
|
|
<span className="text-lg">🔔</span>
|
|
{unread > 0 && (
|
|
<span className="absolute -right-1 -top-1 inline-flex min-h-5 min-w-5 items-center justify-center rounded-full bg-red-600 px-1 text-xs font-bold text-white">
|
|
{unread > 99 ? '99+' : unread}
|
|
</span>
|
|
)}
|
|
</button>
|
|
|
|
{open && (
|
|
<div className="absolute right-0 z-30 mt-2 w-96 overflow-hidden rounded-xl border border-border-hairline bg-canvas-elevated shadow-lg">
|
|
<div className="flex items-center justify-between border-b border-border-hairline px-4 py-3">
|
|
<div>
|
|
<p className="font-semibold text-text-main">Notifikacije</p>
|
|
<p className="text-xs text-text-muted">
|
|
Realtime: {isConnected ? 'spojen' : 'nije spojen'}
|
|
</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => markAllNotificationsAsRead()}
|
|
className="text-xs font-medium text-indigo-600 hover:text-indigo-800"
|
|
>
|
|
Označi sve
|
|
</button>
|
|
</div>
|
|
|
|
<ul className="max-h-96 overflow-y-auto">
|
|
{latestNotifications.length === 0 && (
|
|
<li className="px-4 py-6 text-sm text-text-muted">Nema notifikacija.</li>
|
|
)}
|
|
{latestNotifications.map((notification) => (
|
|
<li
|
|
key={notification.id}
|
|
className={`cursor-pointer border-b border-border-hairline px-4 py-3 last:border-b-0 ${
|
|
notification.is_read ? 'bg-canvas-elevated' : 'bg-indigo-50/30'
|
|
}`}
|
|
onClick={async () => {
|
|
if (!notification.is_read) {
|
|
try {
|
|
await markNotificationAsRead(notification.id);
|
|
} catch {
|
|
// Error handling already done in store toast.
|
|
}
|
|
}
|
|
setSelectedNotification(notification);
|
|
setOpen(false);
|
|
}}
|
|
>
|
|
<div className="flex items-start justify-between gap-2">
|
|
<p className="text-sm font-semibold text-text-main">
|
|
{notification.title}
|
|
</p>
|
|
<span className="shrink-0 text-[11px] text-text-muted">
|
|
{formatTimestamp(notification.created_at)}
|
|
</span>
|
|
</div>
|
|
<p className="mt-1 text-sm text-text-muted">{notification.message}</p>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
|
|
<NotificationDetailModal
|
|
open={!!selectedNotification}
|
|
notification={selectedNotification}
|
|
onClose={() => setSelectedNotification(null)}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|