prvi kod za live uporabu
Some checks failed
ERP CI Pipeline / test (push) Has been cancelled

This commit is contained in:
mariomitte
2026-07-09 21:21:17 +02:00
parent 857bb65d52
commit 5d39e048ba
239 changed files with 25151 additions and 0 deletions

View File

@@ -0,0 +1,135 @@
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>
);
}