diff --git a/src/lib/api/notificationsApi.ts b/src/lib/api/notificationsApi.ts new file mode 100644 index 0000000..6416220 --- /dev/null +++ b/src/lib/api/notificationsApi.ts @@ -0,0 +1,91 @@ +/** + * API service layer for notifications. + * Handles all HTTP communication with the backend. + */ + +import type { Notification } from '$lib/types/notification'; + +export type Fetcher = typeof fetch; + +export interface NotificationsApiDeps { + fetcher: Fetcher; +} + +export interface NotificationsPaginatedResponse { + data: Notification[]; + pagination: { + total: number; + hasMore: boolean; + }; +} + +export interface UnreadCountResponse { + count: number; +} + +export function createNotificationsApi({ fetcher }: NotificationsApiDeps) { + const fetchUnreadCount = async (): Promise => { + const res = await fetcher('/api/notifications/unread-count'); + + if (!res.ok) { + throw new Error('Failed to fetch unread count'); + } + + return (await res.json()) as UnreadCountResponse; + }; + + const fetchNotifications = async ( + page: number, + limit: number = 10 + ): Promise => { + const res = await fetcher(`/api/notifications?page=${page}&limit=${limit}`); + + if (!res.ok) { + throw new Error('Failed to fetch notifications'); + } + + return (await res.json()) as NotificationsPaginatedResponse; + }; + + const markAsRead = async (notificationId: string): Promise => { + const res = await fetcher(`/api/notifications/${notificationId}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ read: true }) + }); + + if (!res.ok) { + throw new Error('Failed to mark notification as read'); + } + }; + + const markAllAsRead = async (): Promise => { + const res = await fetcher('/api/notifications/mark-all-read', { + method: 'PATCH' + }); + + if (!res.ok) { + throw new Error('Failed to mark all notifications as read'); + } + }; + + const dismiss = async (notificationId: string): Promise => { + const res = await fetcher(`/api/notifications/${notificationId}`, { + method: 'DELETE' + }); + + if (!res.ok) { + throw new Error('Failed to dismiss notification'); + } + }; + + return { + fetchUnreadCount, + fetchNotifications, + markAsRead, + markAllAsRead, + dismiss + }; +} + +export type NotificationsApi = ReturnType; diff --git a/src/lib/components/Header.svelte b/src/lib/components/Header.svelte index 2c7a606..7e9565f 100644 --- a/src/lib/components/Header.svelte +++ b/src/lib/components/Header.svelte @@ -9,6 +9,7 @@ import { LogOutIcon, MenuIcon, XIcon } from '@lucide/svelte'; import { Avatar } from '@skeletonlabs/skeleton-svelte'; import { avatarUrl } from '$lib/stores/avatar'; + import { notificationsPresenter } from '$lib/presenters/notifications'; import NotificationBell from './NotificationBell.svelte'; let { @@ -70,6 +71,9 @@ sessionStorage.removeItem(STORAGE_KEYS.SETTINGS_CACHE); sessionStorage.removeItem(STORAGE_KEYS.QUOTE_CACHE); + // Reset notifications presenter state + notificationsPresenter.reset(); + await signOut(); location.href = routes.login; } catch (e) { diff --git a/src/lib/components/NotificationBell.svelte b/src/lib/components/NotificationBell.svelte index 41d0b95..79ac5c8 100644 --- a/src/lib/components/NotificationBell.svelte +++ b/src/lib/components/NotificationBell.svelte @@ -2,19 +2,22 @@ import { onMount } from 'svelte'; import { Popover, Portal } from '@skeletonlabs/skeleton-svelte'; import { Bell, CheckCheck, Loader2 } from '@lucide/svelte'; - import { notificationStore } from '$lib/stores/notifications.svelte'; + import { notificationsPresenter } from '$lib/presenters/notifications'; import NotificationItem from './NotificationItem.svelte'; let isOpen = $state(false); let isClosing = $state(false); + // Subscribe to presenter state + const presenterState = notificationsPresenter.state; + onMount(() => { - notificationStore.fetchUnreadCount(); + notificationsPresenter.fetchUnreadCount(); }); async function handleOpen() { - if (!notificationStore.initialized) { - await notificationStore.fetchNotifications(true); + if (!$presenterState.initialized) { + await notificationsPresenter.fetchNotifications(true); } } @@ -35,19 +38,19 @@ } function handleMarkRead(id: string) { - notificationStore.markAsRead(id); + notificationsPresenter.markAsRead(id); } function handleDismiss(id: string) { - notificationStore.dismiss(id); + notificationsPresenter.dismiss(id); } function handleMarkAllRead() { - notificationStore.markAllAsRead(); + notificationsPresenter.markAllAsRead(); } function handleLoadMore() { - notificationStore.loadMore(); + notificationsPresenter.loadMore(); } @@ -61,11 +64,11 @@ aria-label="Notifications" > - {#if notificationStore.unreadCount > 0} + {#if $presenterState.unreadCount > 0} - {notificationStore.unreadCount > 99 ? '99+' : notificationStore.unreadCount} + {$presenterState.unreadCount > 99 ? '99+' : $presenterState.unreadCount} {/if} @@ -86,7 +89,7 @@ > Notifications - {#if notificationStore.unreadCount > 0} + {#if $presenterState.unreadCount > 0}