refactor notifications popover to use new hooks and improve functionality
```
This commit is contained in:
louiscklaw
2025-05-12 11:32:53 +08:00
parent 99ee2f9fc3
commit c7f1f544ec
4 changed files with 242 additions and 169 deletions

View File

@@ -1,37 +1,35 @@
'use client'; 'use client';
import * as React from 'react'; import * as React from 'react';
import Avatar from '@mui/material/Avatar'; import { getNotificationsByUserId } from '@/db/Notifications/GetNotificationByUserId';
import type { Notification } from '@/db/Notifications/type';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
import Link from '@mui/material/Link';
import List from '@mui/material/List'; import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import Popover from '@mui/material/Popover'; import Popover from '@mui/material/Popover';
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
import Tooltip from '@mui/material/Tooltip'; import Tooltip from '@mui/material/Tooltip';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import { ChatText as ChatTextIcon } from '@phosphor-icons/react/dist/ssr/ChatText';
import { EnvelopeSimple as EnvelopeSimpleIcon } from '@phosphor-icons/react/dist/ssr/EnvelopeSimple'; import { EnvelopeSimple as EnvelopeSimpleIcon } from '@phosphor-icons/react/dist/ssr/EnvelopeSimple';
import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User';
import { X as XIcon } from '@phosphor-icons/react/dist/ssr/X';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { dayjs } from '@/lib/dayjs'; import { User } from '@/types/user';
import { useHelloworld } from '@/hooks/use-helloworld';
import { useUser } from '@/hooks/use-user';
import { NotificationItem } from './notification-item';
// import type { Notification } from './type.d.tsx.del'; // import type { Notification } from './type.d.tsx.del';
import { SampleNotifications } from './sample-notifications'; import { SampleNotifications } from './sample-notifications';
import { useHelloworld } from '@/hooks/use-helloworld'; import getImageUrlFromFile from '@/lib/get-image-url-from-file.ts';
import { getAllNotifications } from '@/db/Notifications/GetAll'; import { getUnreadNotificationsByUserId } from '@/db/Notifications/GetUnreadNotificationsByUserId';
import { ListResult, RecordModel } from 'pocketbase'; import { logger } from '@/lib/default-logger';
import { defaultNotification } from '@/db/Notifications/constants'; import { toast } from '@/components/core/toaster';
import { getNotificationsByUserId } from '@/db/Notifications/GetNotificationByUserId';
import { Notification } from '@/db/Notifications/type';
export interface NotificationsPopoverProps { export interface NotificationsPopoverProps {
anchorEl: null | Element; anchorEl: null | Element;
onClose?: () => void; onClose?: () => void;
onMarkAllAsRead?: () => void; onMarkAllAsRead?: () => void;
onRemoveOne?: (id: string) => void; onRemoveOne?: (id: string, reload: () => Promise<void>) => void;
open?: boolean; open?: boolean;
} }
@@ -44,24 +42,36 @@ export function NotificationsPopover({
}: NotificationsPopoverProps): React.JSX.Element { }: NotificationsPopoverProps): React.JSX.Element {
const { t } = useTranslation(); const { t } = useTranslation();
const [notiList, setNotiList] = React.useState<Notification[]>([]); const [notiList, setNotiList] = React.useState<Notification[]>([]);
const { user } = useUser();
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState<string | null>(null); const [showError, setShowError] = React.useState<boolean>(false);
const { data, handleClose, handleOpen, open: testOpen } = useHelloworld<string>(); async function loadUnreadNotifications(): Promise<void> {
React.useEffect(() => {
setLoading(true); setLoading(true);
async function LoadAllNotifications() { try {
const notiList: Notification[] = await getNotificationsByUserId('1'); if (user?.id) {
setNotiList(notiList); const tempNotiList: Notification[] = await getUnreadNotificationsByUserId(user.id);
setNotiList(tempNotiList);
}
} catch (loadNotiError) {
logger.error(loadNotiError);
toast.error('error during loading noti list');
} }
setLoading(false);
void LoadAllNotifications();
}, []);
if (loading) return <>Loading</>; setLoading(false);
if (error) return <>Error</>; }
if (notiList.length == 0)
React.useEffect(() => {
if (user?.id) {
void loadUnreadNotifications();
}
}, [user]);
// if (loading) return <>Loading</>;
// if (showError) return <>Error</>;
if (notiList.length === 0)
return ( return (
<Popover <Popover
anchorEl={anchorEl} anchorEl={anchorEl}
@@ -71,7 +81,7 @@ export function NotificationsPopover({
slotProps={{ paper: { sx: { width: '380px' } } }} slotProps={{ paper: { sx: { width: '380px' } } }}
transformOrigin={{ horizontal: 'right', vertical: 'top' }} transformOrigin={{ horizontal: 'right', vertical: 'top' }}
> >
list is empty {t('list-is-empty')}
</Popover> </Popover>
); );
@@ -80,7 +90,8 @@ export function NotificationsPopover({
anchorEl={anchorEl} anchorEl={anchorEl}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }} anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
onClose={onClose} onClose={onClose}
open={open} // todo: should not use 'true', fallback to 'open'
open
slotProps={{ paper: { sx: { width: '380px' } } }} slotProps={{ paper: { sx: { width: '380px' } } }}
transformOrigin={{ horizontal: 'right', vertical: 'top' }} transformOrigin={{ horizontal: 'right', vertical: 'top' }}
> >
@@ -89,7 +100,24 @@ export function NotificationsPopover({
spacing={2} spacing={2}
sx={{ alignItems: 'center', justifyContent: 'space-between', px: 3, py: 2 }} sx={{ alignItems: 'center', justifyContent: 'space-between', px: 3, py: 2 }}
> >
<Typography variant="h6">{t('Notifications')}</Typography> <Stack
direction="row"
spacing={2}
sx={{ alignItems: 'left' }}
>
<Typography variant="h6">{t('Notifications')}</Typography>
{loading ? (
<Typography
color="gray"
variant="subtitle2"
>
({t('loading')})
</Typography>
) : (
<></>
)}
</Stack>
<Tooltip title={t('Mark all as read')}> <Tooltip title={t('Mark all as read')}>
<IconButton <IconButton
edge="end" edge="end"
@@ -99,6 +127,7 @@ export function NotificationsPopover({
</IconButton> </IconButton>
</Tooltip> </Tooltip>
</Stack> </Stack>
{notiList.length === 0 ? ( {notiList.length === 0 ? (
<Box sx={{ p: 2 }}> <Box sx={{ p: 2 }}>
<Typography variant="subtitle2">{t('There are no notifications')}</Typography> <Typography variant="subtitle2">{t('There are no notifications')}</Typography>
@@ -108,11 +137,11 @@ export function NotificationsPopover({
<List disablePadding> <List disablePadding>
{notiList.map((notification, index) => ( {notiList.map((notification, index) => (
<NotificationItem <NotificationItem
divider={index < SampleNotifications.length - 1} divider={index < notiList.length - 1}
key={notification.id} key={notification.id}
notification={notification} notification={notification}
onRemove={() => { onRemove={() => {
onRemoveOne?.(notification.id); onRemoveOne?.(notification.id, () => loadUnreadNotifications());
}} }}
/> />
))} ))}
@@ -122,136 +151,3 @@ export function NotificationsPopover({
</Popover> </Popover>
); );
} }
interface NotificationItemProps {
divider?: boolean;
notification: Notification;
onRemove?: () => void;
}
function NotificationItem({ divider, notification, onRemove }: NotificationItemProps): React.JSX.Element {
return (
<ListItem
divider={divider}
sx={{ alignItems: 'flex-start', justifyContent: 'space-between' }}
>
<NotificationContent notification={notification} />
<Tooltip title="Remove">
<IconButton
edge="end"
onClick={onRemove}
size="small"
>
<XIcon />
</IconButton>
</Tooltip>
</ListItem>
);
}
interface NotificationContentProps {
notification: Notification;
}
function NotificationContent({ notification }: NotificationContentProps): React.JSX.Element {
if (notification.type === 'new_feature') {
return (
<Stack
direction="row"
spacing={2}
sx={{ alignItems: 'flex-start' }}
>
<Avatar>
<ChatTextIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
<div>
<Typography variant="subtitle2">New feature!</Typography>
<Typography variant="body2">{notification.description}</Typography>
<Typography
color="text.secondary"
variant="caption"
>
{dayjs(notification.createdAt).format('MMM D, hh:mm A')}
</Typography>
</div>
</Stack>
);
}
if (notification.type === 'new_company') {
return (
<Stack
direction="row"
spacing={2}
sx={{ alignItems: 'flex-start' }}
>
<Avatar src={notification.author.avatar}>
<UserIcon />
</Avatar>
<div>
<Typography variant="body2">
<Typography
component="span"
variant="subtitle2"
>
{notification.author.name}
</Typography>{' '}
created{' '}
<Link
underline="always"
variant="body2"
>
{notification.company.name}
</Link>{' '}
company
</Typography>
<Typography
color="text.secondary"
variant="caption"
>
{dayjs(notification.createdAt).format('MMM D, hh:mm A')}
</Typography>
</div>
</Stack>
);
}
if (notification.type === 'new_job') {
return (
<Stack
direction="row"
spacing={2}
sx={{ alignItems: 'flex-start' }}
>
<Avatar src={notification.author.avatar}>
<UserIcon />
</Avatar>
<div>
<Typography variant="body2">
<Typography
component="span"
variant="subtitle2"
>
{notification.author.name}
</Typography>{' '}
added a new job{' '}
<Link
underline="always"
variant="body2"
>
{notification.job.title}
</Link>
</Typography>
<Typography
color="text.secondary"
variant="caption"
>
{dayjs(notification.createdAt).format('MMM D, hh:mm A')}
</Typography>
</div>
</Stack>
);
}
return <div />;
}

View File

@@ -0,0 +1,139 @@
'use client';
import * as React from 'react';
import type { Notification } from '@/db/Notifications/type';
import Avatar from '@mui/material/Avatar';
import Link from '@mui/material/Link';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { ChatText as ChatTextIcon } from '@phosphor-icons/react/dist/ssr/ChatText';
import { User as UserIcon } from '@phosphor-icons/react/dist/ssr/User';
import { dayjs } from '@/lib/dayjs';
import getImageUrlFromFile from '@/lib/get-image-url-from-file.ts';
import { useRouter } from 'next/navigation';
import { toast } from '@/components/core/toaster';
interface NotificationContentProps {
notification: Notification;
}
export function NotificationContent({ notification }: NotificationContentProps): React.JSX.Element {
const router = useRouter();
if (notification.type === 'new_feature') {
return (
<Stack
direction="row"
spacing={2}
sx={{ alignItems: 'flex-start' }}
>
<Avatar>
<ChatTextIcon fontSize="var(--Icon-fontSize)" />
</Avatar>
<div>
<Typography variant="subtitle2">New feature!</Typography>
<Typography variant="body2">{notification.description}</Typography>
<Typography
color="text.secondary"
variant="caption"
>
{dayjs(notification.createdAt).format('MMM D, hh:mm A')}
</Typography>
</div>
</Stack>
);
}
if (notification.type === 'new_company') {
return (
<Stack
direction="row"
spacing={2}
sx={{ alignItems: 'flex-start' }}
>
<Avatar src={notification?.author?.avatar || ''}>
<UserIcon size={24} />
</Avatar>
<div>
<Typography variant="body2">
<Typography
component="span"
variant="subtitle2"
>
{notification?.author?.name}
</Typography>{' '}
created{' '}
<Link
underline="always"
variant="body2"
>
{notification?.company?.name}
</Link>{' '}
company
</Typography>
<Typography
color="text.secondary"
variant="caption"
>
{dayjs(notification.created).format('MMM D, hh:mm A')}
</Typography>
</div>
</Stack>
);
}
if (notification.type === 'new_job') {
const handleClick = (): void => {
try {
if (notification.link) {
router.push(notification.link);
}
} catch (error) {
toast.error((error as { message: string }).message);
}
};
return (
<Stack
direction="row"
spacing={2}
sx={{
alignItems: 'flex-start',
cursor: notification.link ? 'pointer' : '',
}}
//
onClick={handleClick}
>
<Avatar src={notification?.author?.avatar}>
<UserIcon />
</Avatar>
<div>
<Typography variant="body2">
<Typography
component="span"
variant="subtitle2"
>
{notification?.author?.name}
</Typography>{' '}
added a new job{' '}
<Link
underline="always"
variant="body2"
>
{notification?.job?.title}
</Link>
</Typography>
<Typography
color="text.secondary"
variant="caption"
>
{dayjs(notification.created).format('MMM D, hh:mm A')}
</Typography>
</div>
</Stack>
);
}
return <div />;
}

View File

@@ -0,0 +1,36 @@
'use client';
import * as React from 'react';
import type { Notification } from '@/db/Notifications/type';
import IconButton from '@mui/material/IconButton';
import ListItem from '@mui/material/ListItem';
import Tooltip from '@mui/material/Tooltip';
import { X as XIcon } from '@phosphor-icons/react/dist/ssr/X';
import { NotificationContent } from './notification-content';
interface NotificationItemProps {
divider?: boolean;
notification: Notification;
onRemove?: () => void;
}
export function NotificationItem({ divider, notification, onRemove }: NotificationItemProps): React.JSX.Element {
return (
<ListItem
divider={divider}
sx={{ alignItems: 'flex-start', justifyContent: 'space-between' }}
>
<NotificationContent notification={notification} />
<Tooltip title="Remove">
<IconButton
edge="end"
onClick={onRemove}
size="small"
>
<XIcon />
</IconButton>
</Tooltip>
</ListItem>
);
}

View File

@@ -1,6 +1,7 @@
'use client'; 'use client';
import { Notification } from '@/db/Notifications/type';
import { dayjs } from '@/lib/dayjs'; import { dayjs } from '@/lib/dayjs';
import type { Notification } from './type.d.tsx.del'; // import type { Notification } from './type.d.tsx';
export const SampleNotifications = [ export const SampleNotifications = [
{ {
@@ -8,7 +9,7 @@ export const SampleNotifications = [
createdAt: dayjs().subtract(7, 'minute').subtract(5, 'hour').subtract(1, 'day').toDate(), createdAt: dayjs().subtract(7, 'minute').subtract(5, 'hour').subtract(1, 'day').toDate(),
read: false, read: false,
type: 'new_job', type: 'new_job',
author: { name: 'Jie Yan', avatar: '/assets/avatar-8.png' }, author: { id: '0001', collectionId: '0001', name: 'Jie Yan', avatar: '/assets/avatar-8.png' },
job: { title: 'Remote React / React Native Developer' }, job: { title: 'Remote React / React Native Developer' },
}, },
{ {
@@ -16,7 +17,7 @@ export const SampleNotifications = [
createdAt: dayjs().subtract(18, 'minute').subtract(3, 'hour').subtract(5, 'day').toDate(), createdAt: dayjs().subtract(18, 'minute').subtract(3, 'hour').subtract(5, 'day').toDate(),
read: true, read: true,
type: 'new_job', type: 'new_job',
author: { name: 'Fran Perez', avatar: '/assets/avatar-5.png' }, author: { id: '0001', collectionId: '0001', name: 'Fran Perez', avatar: '/assets/avatar-5.png' },
job: { title: 'Senior Golang Backend Engineer' }, job: { title: 'Senior Golang Backend Engineer' },
}, },
{ {
@@ -24,6 +25,7 @@ export const SampleNotifications = [
createdAt: dayjs().subtract(4, 'minute').subtract(5, 'hour').subtract(7, 'day').toDate(), createdAt: dayjs().subtract(4, 'minute').subtract(5, 'hour').subtract(7, 'day').toDate(),
read: true, read: true,
type: 'new_feature', type: 'new_feature',
author: { id: '0001', collectionId: '0001', name: 'Fran Perez', avatar: '/assets/avatar-5.png' },
description: 'Logistics management is now available', description: 'Logistics management is now available',
}, },
{ {
@@ -31,7 +33,7 @@ export const SampleNotifications = [
createdAt: dayjs().subtract(7, 'minute').subtract(8, 'hour').subtract(7, 'day').toDate(), createdAt: dayjs().subtract(7, 'minute').subtract(8, 'hour').subtract(7, 'day').toDate(),
read: true, read: true,
type: 'new_company', type: 'new_company',
author: { name: 'Jie Yan', avatar: '/assets/avatar-8.png' }, author: { id: '0001', collectionId: '002', name: 'Jie Yan', avatar: '/assets/avatar-8.png' },
company: { name: 'Stripe' }, company: { name: 'Stripe' },
}, },
] satisfies Notification[]; ] satisfies Notification[];