mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
feat(NotificationsPage): implement tabbed navigation for notifications and improve loading state handling
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Badge,
|
||||
@@ -7,6 +8,7 @@ import {
|
||||
Container,
|
||||
Group,
|
||||
Loader,
|
||||
SegmentedControl,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
@@ -15,9 +17,18 @@ import { IconBellOff, IconCheck } from '@tabler/icons-react';
|
||||
import {
|
||||
localized,
|
||||
useGetNotificationsQuery,
|
||||
useGetUnseenNotificationsQuery,
|
||||
useMarkNotificationReadMutation,
|
||||
} from '@ema-platform/api';
|
||||
|
||||
type Tab = 'all' | 'unseen' | 'seen';
|
||||
|
||||
const EMPTY_COPY: Record<Tab, string> = {
|
||||
all: 'No notifications yet.',
|
||||
unseen: 'Nothing unread.',
|
||||
seen: 'No read notifications.',
|
||||
};
|
||||
|
||||
/**
|
||||
* The applicant's notification inbox.
|
||||
*
|
||||
@@ -26,19 +37,15 @@ import {
|
||||
*/
|
||||
export function NotificationsPage() {
|
||||
const navigate = useNavigate();
|
||||
const { data, isLoading } = useGetNotificationsQuery();
|
||||
const [tab, setTab] = useState<Tab>('all');
|
||||
const all = useGetNotificationsQuery(undefined, { skip: tab === 'unseen' });
|
||||
const unseen = useGetUnseenNotificationsQuery(undefined, { skip: tab !== 'unseen' });
|
||||
const [markRead] = useMarkNotificationReadMutation();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Center h={300}>
|
||||
<Loader />
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
const items = data?.items ?? [];
|
||||
const unread = items.filter((n) => !n.isSeen).length;
|
||||
const { data, isLoading } = tab === 'unseen' ? unseen : all;
|
||||
const items =
|
||||
tab === 'seen' ? (data?.items ?? []).filter((n) => n.isSeen) : (data?.items ?? []);
|
||||
const unread = all.data?.items.filter((n) => !n.isSeen).length ?? 0;
|
||||
|
||||
async function open(id: string, seen: boolean, link?: string) {
|
||||
if (!seen) await markRead(id);
|
||||
@@ -52,11 +59,27 @@ export function NotificationsPage() {
|
||||
{unread > 0 ? `${unread} unread` : 'You are all caught up'}
|
||||
</Text>
|
||||
|
||||
{items.length === 0 ? (
|
||||
<SegmentedControl
|
||||
size="xs"
|
||||
mb="md"
|
||||
value={tab}
|
||||
onChange={(v) => setTab(v as Tab)}
|
||||
data={[
|
||||
{ label: 'All', value: 'all' },
|
||||
{ label: 'Unseen', value: 'unseen' },
|
||||
{ label: 'Seen', value: 'seen' },
|
||||
]}
|
||||
/>
|
||||
|
||||
{isLoading ? (
|
||||
<Center h={300}>
|
||||
<Loader />
|
||||
</Center>
|
||||
) : items.length === 0 ? (
|
||||
<Card withBorder padding="xl">
|
||||
<Stack align="center" gap="xs">
|
||||
<IconBellOff size={32} stroke={1.4} color="var(--mantine-color-gray-5)" />
|
||||
<Text c="dimmed">No notifications yet.</Text>
|
||||
<Text c="dimmed">{EMPTY_COPY[tab]}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
You will be notified as your applications progress.
|
||||
</Text>
|
||||
|
||||
@@ -16,14 +16,18 @@ import {
|
||||
IconUserCircle,
|
||||
} from "@tabler/icons-react";
|
||||
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { notify, AppHeader, AppSidebar } from "@ema-platform/ui";
|
||||
import type { NavItem } from "@ema-platform/ui";
|
||||
import { BrandMark, logout } from "@ema-platform/auth";
|
||||
import { useGetUnseenNotificationsQuery } from "@ema-platform/api";
|
||||
import { SUPPORTED_LANGUAGES } from "../i18n/config";
|
||||
import { useAppSelector } from "../store/hooks";
|
||||
|
||||
const BADGE_POLL_MS = 60_000;
|
||||
|
||||
type PortalNavItem = NavItem & { i18nKey: string };
|
||||
|
||||
/**
|
||||
@@ -170,6 +174,24 @@ export function PortalLayout() {
|
||||
const user = useAppSelector((state) => state.auth.user);
|
||||
const [navOpened, { toggle: toggleNav, close: closeNav }] = useDisclosure();
|
||||
const [sidebarCollapsed, { toggle: toggleSidebar }] = useDisclosure(false);
|
||||
const { data: unseen } = useGetUnseenNotificationsQuery(undefined, {
|
||||
pollingInterval: BADGE_POLL_MS,
|
||||
refetchOnMountOrArgChange: false,
|
||||
});
|
||||
|
||||
const sections = useMemo(
|
||||
() =>
|
||||
NAV_SECTIONS.map((section) => ({
|
||||
label: section.label,
|
||||
items: section.items.map(({ i18nKey, ...rest }) => ({
|
||||
...rest,
|
||||
label: t(i18nKey),
|
||||
badge:
|
||||
rest.to === "/notifications" && unseen?.count ? unseen.count : undefined,
|
||||
})),
|
||||
})),
|
||||
[t, unseen?.count],
|
||||
);
|
||||
|
||||
// Breadcrumb trail
|
||||
const segments = location.pathname.split("/").filter(Boolean);
|
||||
@@ -246,13 +268,7 @@ export function PortalLayout() {
|
||||
}}
|
||||
>
|
||||
<AppSidebar
|
||||
navItems={NAV_SECTIONS.map((section) => ({
|
||||
label: section.label,
|
||||
items: section.items.map(({ i18nKey, ...rest }) => ({
|
||||
...rest,
|
||||
label: t(i18nKey),
|
||||
})),
|
||||
}))}
|
||||
navItems={sections}
|
||||
collapsed={sidebarCollapsed}
|
||||
activePath={location.pathname}
|
||||
onToggleCollapse={toggleSidebar}
|
||||
|
||||
Reference in New Issue
Block a user