fe integration for notification system with context, components, and API integration

This commit is contained in:
2026-10-20 16:53:15 +05:30
parent 6b9f27573b
commit 209ee7da2d
11 changed files with 696 additions and 17 deletions
@@ -0,0 +1,133 @@
'use client'
import { useCallback, useEffect, useState } from 'react'
import { Trash2 } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { EmptyState } from '@/components/shared/empty-state'
import { NotificationRow } from '@/components/layout/notification-bell'
import { useNotifications } from '@/contexts/notification-context'
import { getApiError } from '@/lib/api'
import { notifications } from '@/lib/api/notifications'
import { toast } from '@/components/ui/toast'
import type { NotificationResponse } from '@/types/notifications'
const PAGE_SIZE = 20
type Filter = 'all' | 'unread'
export function NotificationsClient() {
const { recent, unreadCount, open, markAllRead, refresh } = useNotifications()
const [filter, setFilter] = useState<Filter>('all')
const [items, setItems] = useState<NotificationResponse[]>([])
const [page, setPage] = useState(1)
const [total, setTotal] = useState(0)
const [loading, setLoading] = useState(true)
// The caller decides whether to show the loading state (it is true on first render).
const load = useCallback(async (targetPage: number, reset: boolean) => {
try {
const result = await notifications.list({
unreadOnly: filter === 'unread',
page: targetPage,
pageSize: PAGE_SIZE,
})
setItems(prev => (reset ? result.items : [...prev, ...result.items]))
setPage(targetPage)
setTotal(result.total)
} catch (error) {
toast.add({ title: 'Could not load notifications', description: getApiError(error), type: 'error' })
} finally {
setLoading(false)
}
}, [filter])
// Reload when the filter changes or a new notification is pushed in.
const latestId = recent[0]?.id
useEffect(() => { void load(1, true) }, [load, latestId])
async function handleOpen(n: NotificationResponse) {
setItems(prev => prev.map(x => (x.id === n.id ? { ...x, isRead: true } : x)))
await open(n)
}
async function handleMarkAllRead() {
await markAllRead()
await load(1, true)
}
async function handleDelete(n: NotificationResponse) {
try {
await notifications.remove(n.id)
setItems(prev => prev.filter(x => x.id !== n.id))
setTotal(t => Math.max(0, t - 1))
void refresh()
} catch (error) {
toast.add({ title: 'Could not delete notification', description: getApiError(error), type: 'error' })
}
}
const hasMore = items.length < total
return (
<div className="mx-auto flex w-full max-w-3xl flex-col gap-4">
<div className="flex flex-wrap items-center justify-between gap-2">
<div>
<h1 className="text-lg font-semibold">Notifications</h1>
<p className="text-sm text-muted-foreground">
{unreadCount > 0 ? `${unreadCount} unread` : 'You\'re all caught up'}
</p>
</div>
<div className="flex items-center gap-2">
<div className="flex rounded-2xl border border-border p-0.5">
{(['all', 'unread'] as const).map(f => (
<Button
key={f}
variant={filter === f ? 'secondary' : 'ghost'}
size="sm"
onClick={() => { setLoading(true); setFilter(f) }}
>
{f === 'all' ? 'All' : 'Unread'}
</Button>
))}
</div>
<Button variant="outline" size="sm" disabled={unreadCount === 0} onClick={() => { void handleMarkAllRead() }}>
Mark all read
</Button>
</div>
</div>
<div className="overflow-hidden rounded-2xl border border-border">
{items.length === 0 && !loading ? (
<EmptyState
title={filter === 'unread' ? 'No unread notifications' : 'No notifications yet'}
description="Alerts about deliveries, stock, claims and orders will show up here."
/>
) : (
<div className="divide-y divide-border">
{items.map(n => (
<div key={n.id} className="group relative">
<NotificationRow notification={n} onOpen={item => { void handleOpen(item) }} />
<Button
variant="ghost"
size="icon-sm"
aria-label="Delete notification"
className="absolute top-2 right-2 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100"
onClick={() => { void handleDelete(n) }}
>
<Trash2 size={14} />
</Button>
</div>
))}
</div>
)}
</div>
{hasMore && (
<Button variant="outline" disabled={loading} onClick={() => { setLoading(true); void load(page + 1, false) }} className="self-center">
{loading ? 'Loading…' : 'Load more'}
</Button>
)}
</div>
)
}
@@ -0,0 +1,5 @@
import { NotificationsClient } from './_components/notifications-client'
export default function NotificationsPage() {
return <NotificationsClient />
}