From c7f1f544ec12bd5ff093726cd4578b8b5526851b Mon Sep 17 00:00:00 2001 From: louiscklaw Date: Mon, 12 May 2025 11:32:53 +0800 Subject: [PATCH] ``` refactor notifications popover to use new hooks and improve functionality ``` --- .../layout/notifications-popover/index.tsx | 226 +++++------------- .../notification-content.tsx | 139 +++++++++++ .../notification-item.tsx | 36 +++ .../sample-notifications.tsx | 10 +- 4 files changed, 242 insertions(+), 169 deletions(-) create mode 100644 002_source/cms/src/components/dashboard/layout/notifications-popover/notification-content.tsx create mode 100644 002_source/cms/src/components/dashboard/layout/notifications-popover/notification-item.tsx diff --git a/002_source/cms/src/components/dashboard/layout/notifications-popover/index.tsx b/002_source/cms/src/components/dashboard/layout/notifications-popover/index.tsx index 0f35fdc..617f472 100644 --- a/002_source/cms/src/components/dashboard/layout/notifications-popover/index.tsx +++ b/002_source/cms/src/components/dashboard/layout/notifications-popover/index.tsx @@ -1,37 +1,35 @@ 'use client'; 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 IconButton from '@mui/material/IconButton'; -import Link from '@mui/material/Link'; import List from '@mui/material/List'; -import ListItem from '@mui/material/ListItem'; import Popover from '@mui/material/Popover'; import Stack from '@mui/material/Stack'; import Tooltip from '@mui/material/Tooltip'; 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 { 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 { 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 { SampleNotifications } from './sample-notifications'; -import { useHelloworld } from '@/hooks/use-helloworld'; -import { getAllNotifications } from '@/db/Notifications/GetAll'; -import { ListResult, RecordModel } from 'pocketbase'; -import { defaultNotification } from '@/db/Notifications/constants'; -import { getNotificationsByUserId } from '@/db/Notifications/GetNotificationByUserId'; -import { Notification } from '@/db/Notifications/type'; +import getImageUrlFromFile from '@/lib/get-image-url-from-file.ts'; +import { getUnreadNotificationsByUserId } from '@/db/Notifications/GetUnreadNotificationsByUserId'; +import { logger } from '@/lib/default-logger'; +import { toast } from '@/components/core/toaster'; export interface NotificationsPopoverProps { anchorEl: null | Element; onClose?: () => void; onMarkAllAsRead?: () => void; - onRemoveOne?: (id: string) => void; + onRemoveOne?: (id: string, reload: () => Promise) => void; open?: boolean; } @@ -44,24 +42,36 @@ export function NotificationsPopover({ }: NotificationsPopoverProps): React.JSX.Element { const { t } = useTranslation(); const [notiList, setNotiList] = React.useState([]); + const { user } = useUser(); - const [loading, setLoading] = React.useState(false); - const [error, setError] = React.useState(null); + const [loading, setLoading] = React.useState(true); + const [showError, setShowError] = React.useState(false); - const { data, handleClose, handleOpen, open: testOpen } = useHelloworld(); - React.useEffect(() => { + async function loadUnreadNotifications(): Promise { setLoading(true); - async function LoadAllNotifications() { - const notiList: Notification[] = await getNotificationsByUserId('1'); - setNotiList(notiList); + try { + if (user?.id) { + 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; - if (error) return <>Error; - if (notiList.length == 0) + setLoading(false); + } + + React.useEffect(() => { + if (user?.id) { + void loadUnreadNotifications(); + } + }, [user]); + + // if (loading) return <>Loading; + // if (showError) return <>Error; + + if (notiList.length === 0) return ( - list is empty + {t('list-is-empty')} ); @@ -80,7 +90,8 @@ export function NotificationsPopover({ anchorEl={anchorEl} anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }} onClose={onClose} - open={open} + // todo: should not use 'true', fallback to 'open' + open slotProps={{ paper: { sx: { width: '380px' } } }} transformOrigin={{ horizontal: 'right', vertical: 'top' }} > @@ -89,7 +100,24 @@ export function NotificationsPopover({ spacing={2} sx={{ alignItems: 'center', justifyContent: 'space-between', px: 3, py: 2 }} > - {t('Notifications')} + + {t('Notifications')} + + {loading ? ( + + ({t('loading')}) + + ) : ( + <> + )} + + {notiList.length === 0 ? ( {t('There are no notifications')} @@ -108,11 +137,11 @@ export function NotificationsPopover({ {notiList.map((notification, index) => ( { - onRemoveOne?.(notification.id); + onRemoveOne?.(notification.id, () => loadUnreadNotifications()); }} /> ))} @@ -122,136 +151,3 @@ export function NotificationsPopover({ ); } - -interface NotificationItemProps { - divider?: boolean; - notification: Notification; - onRemove?: () => void; -} - -function NotificationItem({ divider, notification, onRemove }: NotificationItemProps): React.JSX.Element { - return ( - - - - - - - - - ); -} - -interface NotificationContentProps { - notification: Notification; -} - -function NotificationContent({ notification }: NotificationContentProps): React.JSX.Element { - if (notification.type === 'new_feature') { - return ( - - - - -
- New feature! - {notification.description} - - {dayjs(notification.createdAt).format('MMM D, hh:mm A')} - -
-
- ); - } - - if (notification.type === 'new_company') { - return ( - - - - -
- - - {notification.author.name} - {' '} - created{' '} - - {notification.company.name} - {' '} - company - - - {dayjs(notification.createdAt).format('MMM D, hh:mm A')} - -
-
- ); - } - - if (notification.type === 'new_job') { - return ( - - - - -
- - - {notification.author.name} - {' '} - added a new job{' '} - - {notification.job.title} - - - - {dayjs(notification.createdAt).format('MMM D, hh:mm A')} - -
-
- ); - } - - return
; -} diff --git a/002_source/cms/src/components/dashboard/layout/notifications-popover/notification-content.tsx b/002_source/cms/src/components/dashboard/layout/notifications-popover/notification-content.tsx new file mode 100644 index 0000000..697c49d --- /dev/null +++ b/002_source/cms/src/components/dashboard/layout/notifications-popover/notification-content.tsx @@ -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 ( + + + + +
+ New feature! + {notification.description} + + {dayjs(notification.createdAt).format('MMM D, hh:mm A')} + +
+
+ ); + } + + if (notification.type === 'new_company') { + return ( + + + + +
+ + + {notification?.author?.name} + {' '} + created{' '} + + {notification?.company?.name} + {' '} + company + + + {dayjs(notification.created).format('MMM D, hh:mm A')} + +
+
+ ); + } + + 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 ( + + + + +
+ + + {notification?.author?.name} + {' '} + added a new job{' '} + + {notification?.job?.title} + + + + {dayjs(notification.created).format('MMM D, hh:mm A')} + +
+
+ ); + } + + return
; +} diff --git a/002_source/cms/src/components/dashboard/layout/notifications-popover/notification-item.tsx b/002_source/cms/src/components/dashboard/layout/notifications-popover/notification-item.tsx new file mode 100644 index 0000000..77338bf --- /dev/null +++ b/002_source/cms/src/components/dashboard/layout/notifications-popover/notification-item.tsx @@ -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 ( + + + + + + + + + ); +} diff --git a/002_source/cms/src/components/dashboard/layout/notifications-popover/sample-notifications.tsx b/002_source/cms/src/components/dashboard/layout/notifications-popover/sample-notifications.tsx index 1259e17..f2b9615 100644 --- a/002_source/cms/src/components/dashboard/layout/notifications-popover/sample-notifications.tsx +++ b/002_source/cms/src/components/dashboard/layout/notifications-popover/sample-notifications.tsx @@ -1,6 +1,7 @@ 'use client'; +import { Notification } from '@/db/Notifications/type'; import { dayjs } from '@/lib/dayjs'; -import type { Notification } from './type.d.tsx.del'; +// import type { Notification } from './type.d.tsx'; export const SampleNotifications = [ { @@ -8,7 +9,7 @@ export const SampleNotifications = [ createdAt: dayjs().subtract(7, 'minute').subtract(5, 'hour').subtract(1, 'day').toDate(), read: false, 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' }, }, { @@ -16,7 +17,7 @@ export const SampleNotifications = [ createdAt: dayjs().subtract(18, 'minute').subtract(3, 'hour').subtract(5, 'day').toDate(), read: true, 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' }, }, { @@ -24,6 +25,7 @@ export const SampleNotifications = [ createdAt: dayjs().subtract(4, 'minute').subtract(5, 'hour').subtract(7, 'day').toDate(), read: true, type: 'new_feature', + author: { id: '0001', collectionId: '0001', name: 'Fran Perez', avatar: '/assets/avatar-5.png' }, 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(), read: true, 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' }, }, ] satisfies Notification[];