frontend phase 14
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
'use client';
|
||||
import { FunctionComponent, useState } from 'react';
|
||||
import Skeleton from '@mui/material/Skeleton';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { useLocale, useTranslations } from 'next-intl';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import AppButton from '@/components/common/AppButton';
|
||||
import { AppIcon } from '@/components/common';
|
||||
import { NOTIFICATIONS_PAGE_SIZE } from '@/services/notifications/constants';
|
||||
import {
|
||||
notificationDeepLink,
|
||||
useMarkAllRead,
|
||||
useMarkNotificationRead,
|
||||
useNotifications,
|
||||
} from '@/services/notifications';
|
||||
import type { AppNotification } from '@/services/notifications/types';
|
||||
import { formatShamsiDateTime } from '@/utils';
|
||||
import NotificationRow from './NotificationRow';
|
||||
|
||||
export interface NotificationCenterProps {
|
||||
role: 'customer' | 'nurse';
|
||||
}
|
||||
|
||||
/**
|
||||
* The notification center — a paged, **unread-first** list. Each row **marks itself read on open** (optimistic)
|
||||
* and **deep-links via `notificationDeepLink`** (role-aware) when its `data` points somewhere. A "mark all read"
|
||||
* action clears the badge at once. Empty / loading-skeleton / error→retry states. Shared by the customer and
|
||||
* nurse notification pages (role decides only the deep-link shell).
|
||||
* @component NotificationCenter
|
||||
*/
|
||||
const NotificationCenter: FunctionComponent<NotificationCenterProps> = ({ role }) => {
|
||||
const t = useTranslations('notifications');
|
||||
const locale = useLocale();
|
||||
const router = useRouter();
|
||||
const [limit, setLimit] = useState(NOTIFICATIONS_PAGE_SIZE);
|
||||
|
||||
const { data, isLoading, isError, refetch, isFetching } = useNotifications(limit);
|
||||
const markRead = useMarkNotificationRead();
|
||||
const markAll = useMarkAllRead();
|
||||
|
||||
const items = data?.items ?? [];
|
||||
const total = data?.total ?? 0;
|
||||
const hasUnread = items.some((n) => !n.isRead);
|
||||
|
||||
const openNotification = (notification: AppNotification) => {
|
||||
if (!notification.isRead) markRead.mutate(notification.id);
|
||||
const target = notificationDeepLink(notification, role);
|
||||
if (target) router.push(`/${locale}${target}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack sx={{ gap: 2, maxWidth: 640, mx: 'auto', width: '100%' }}>
|
||||
<Stack direction="row" sx={{ alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 800 }}>
|
||||
{t('title')}
|
||||
</Typography>
|
||||
{hasUnread ? (
|
||||
<AppButton
|
||||
variant="text"
|
||||
color="primary"
|
||||
onClick={() => markAll.mutate()}
|
||||
disabled={markAll.isPending}
|
||||
sx={{ m: 0 }}
|
||||
>
|
||||
{t('mark_all_read')}
|
||||
</AppButton>
|
||||
) : null}
|
||||
</Stack>
|
||||
|
||||
{isLoading ? (
|
||||
<Stack sx={{ gap: 1 }}>
|
||||
{[0, 1, 2, 3].map((i) => (
|
||||
<Skeleton key={i} variant="rounded" height={72} />
|
||||
))}
|
||||
</Stack>
|
||||
) : isError ? (
|
||||
<Stack sx={{ gap: 1.5, alignItems: 'center', py: 4 }}>
|
||||
<AppIcon icon="error" size={32} color="var(--bal-error)" />
|
||||
<Typography variant="body2" sx={{ color: 'var(--bal-text-secondary)' }}>
|
||||
{t('error_body')}
|
||||
</Typography>
|
||||
<AppButton variant="outlined" color="primary" onClick={() => refetch()}>
|
||||
{t('retry')}
|
||||
</AppButton>
|
||||
</Stack>
|
||||
) : items.length === 0 ? (
|
||||
<Stack sx={{ gap: 1, alignItems: 'center', py: 6 }}>
|
||||
<AppIcon icon="verified" size={36} color="var(--bal-success)" />
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}>
|
||||
{t('empty_title')}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: 'var(--bal-text-secondary)' }}>
|
||||
{t('empty_body')}
|
||||
</Typography>
|
||||
</Stack>
|
||||
) : (
|
||||
<Stack sx={{ gap: 1 }}>
|
||||
{items.map((notification) => (
|
||||
<NotificationRow
|
||||
key={notification.id}
|
||||
notification={notification}
|
||||
timeLabel={formatShamsiDateTime(notification.createdAt, locale)}
|
||||
onOpen={() => openNotification(notification)}
|
||||
/>
|
||||
))}
|
||||
{total > items.length ? (
|
||||
<AppButton
|
||||
variant="text"
|
||||
color="primary"
|
||||
onClick={() => setLimit((current) => current + NOTIFICATIONS_PAGE_SIZE)}
|
||||
disabled={isFetching}
|
||||
sx={{ alignSelf: 'center' }}
|
||||
>
|
||||
{t('load_more')}
|
||||
</AppButton>
|
||||
) : null}
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotificationCenter;
|
||||
Reference in New Issue
Block a user