From cf70e2af21cd9621207e6b5189d41ec1126d72e5 Mon Sep 17 00:00:00 2001 From: louiscklaw Date: Mon, 12 May 2025 13:10:19 +0800 Subject: [PATCH] ``` refactor notifications popover to include unread count, mark all as read button, and loading state ``` --- .../layout/horizontal/main-nav/index.tsx | 10 +- .../layout/notifications-popover/index.tsx | 103 ++++++++++++++++-- .../mark-all-as-read-button.tsx | 29 +++++ .../main-nav/notifications-button.tsx | 50 ++++++++- .../Notifications/GetNotificationByUserId.tsx | 8 +- .../GetUnreadNotificationsByUserId.tsx | 20 ++++ .../src/db/Notifications/mark-one-as-read.tsx | 11 ++ 002_source/cms/src/db/Notifications/type.d.ts | 16 ++- 8 files changed, 224 insertions(+), 23 deletions(-) create mode 100644 002_source/cms/src/components/dashboard/layout/notifications-popover/mark-all-as-read-button.tsx create mode 100644 002_source/cms/src/db/Notifications/GetUnreadNotificationsByUserId.tsx create mode 100644 002_source/cms/src/db/Notifications/mark-one-as-read.tsx diff --git a/002_source/cms/src/components/dashboard/layout/horizontal/main-nav/index.tsx b/002_source/cms/src/components/dashboard/layout/horizontal/main-nav/index.tsx index 4435eb6..e297dd5 100644 --- a/002_source/cms/src/components/dashboard/layout/horizontal/main-nav/index.tsx +++ b/002_source/cms/src/components/dashboard/layout/horizontal/main-nav/index.tsx @@ -189,7 +189,15 @@ function NotificationsButton(): React.JSX.Element { void; onMarkAllAsRead?: () => void; onRemoveOne?: (id: string, reload: () => Promise) => void; open?: boolean; + setListLength: React.Dispatch>; } export function NotificationsPopover({ @@ -39,6 +45,7 @@ export function NotificationsPopover({ onMarkAllAsRead, onRemoveOne, open = false, + setListLength, }: NotificationsPopoverProps): React.JSX.Element { const { t } = useTranslation(); const [notiList, setNotiList] = React.useState([]); @@ -53,6 +60,7 @@ export function NotificationsPopover({ if (user?.id) { const tempNotiList: Notification[] = await getUnreadNotificationsByUserId(user.id); setNotiList(tempNotiList); + setListLength(tempNotiList.length); } } catch (loadNotiError) { logger.error(loadNotiError); @@ -78,10 +86,60 @@ export function NotificationsPopover({ anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }} onClose={onClose} open={open} - slotProps={{ paper: { sx: { width: '380px' } } }} + slotProps={{ paper: { sx: { width: 'unset' } } }} transformOrigin={{ horizontal: 'right', vertical: 'top' }} > - {t('list-is-empty')} + + + + {t('Notifications')} ({notiList.length}) + + + {loading ? ( + + ({t('loading')}) + + ) : ( + <> + )} + + {/* MarkAllAsReadButton(onMarkAllAsRead, notiList.length <= 0) */} + + + + + + + + {t('list-is-empty')} + + + + ); @@ -91,7 +149,7 @@ export function NotificationsPopover({ anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }} onClose={onClose} // todo: should not use 'true', fallback to 'open' - open + open={open} slotProps={{ paper: { sx: { width: '380px' } } }} transformOrigin={{ horizontal: 'right', vertical: 'top' }} > @@ -105,7 +163,9 @@ export function NotificationsPopover({ spacing={2} sx={{ alignItems: 'left' }} > - {t('Notifications')} + + {t('Notifications')} ({notiList.length}) + {loading ? ( )} - - - - - + {notiList.length === 0 ? ( @@ -151,3 +207,26 @@ export function NotificationsPopover({ ); } + +// TODO: remove me +// function MarkAllAsReadButton({ +// onMarkAllAsRead, +// disabled, +// }: { +// onMarkAllAsRead: (() => void) | undefined; +// disabled: boolean; +// }): React.JSX.Element { +// const { t } = useTranslation(); + +// return ( +// +// +// +// +// +// ); +// } diff --git a/002_source/cms/src/components/dashboard/layout/notifications-popover/mark-all-as-read-button.tsx b/002_source/cms/src/components/dashboard/layout/notifications-popover/mark-all-as-read-button.tsx new file mode 100644 index 0000000..5f7a528 --- /dev/null +++ b/002_source/cms/src/components/dashboard/layout/notifications-popover/mark-all-as-read-button.tsx @@ -0,0 +1,29 @@ +'use client'; + +import * as React from 'react'; +import IconButton from '@mui/material/IconButton'; +import Tooltip from '@mui/material/Tooltip'; +import { EnvelopeSimple as EnvelopeSimpleIcon } from '@phosphor-icons/react/dist/ssr/EnvelopeSimple'; +import { useTranslation } from 'react-i18next'; + +export function MarkAllAsReadButton({ + onMarkAllAsRead, + disabled, +}: { + onMarkAllAsRead: (() => void) | undefined; + disabled: boolean; +}): React.JSX.Element { + const { t } = useTranslation(); + + return ( + + + + + + ); +} diff --git a/002_source/cms/src/components/dashboard/layout/vertical/main-nav/notifications-button.tsx b/002_source/cms/src/components/dashboard/layout/vertical/main-nav/notifications-button.tsx index 1548966..ef87378 100644 --- a/002_source/cms/src/components/dashboard/layout/vertical/main-nav/notifications-button.tsx +++ b/002_source/cms/src/components/dashboard/layout/vertical/main-nav/notifications-button.tsx @@ -9,18 +9,60 @@ import { Bell as BellIcon } from '@phosphor-icons/react/dist/ssr/Bell'; import { usePopover } from '@/hooks/use-popover'; import { NotificationsPopover } from '../../notifications-popover'; +import { logger } from '@/lib/default-logger'; // import { NotificationsButton } from './notifications-button'; +import { toast } from '@/components/core/toaster'; +import { MarkOneAsRead } from '@/db/Notifications/mark-one-as-read'; +import { getUnreadNotificationsByUserId } from '@/db/Notifications/GetUnreadNotificationsByUserId'; +import { Notification } from '@/db/Notifications/type'; +import { useUser } from '@/hooks/use-user'; export function NotificationsButton(): React.JSX.Element { const popover = usePopover(); + const { user } = useUser(); + + const [loading, setLoading] = React.useState(true); + const [showError, setShowError] = React.useState(false); + const [listLength, setListLength] = React.useState(0); + + const [notiList, setNotiList] = React.useState([]); + + function handleMarkAllAsRead(): void { + // try { + // await MarkOneAsRead(id); + // toast.success('Notification marked as read'); + // } catch (error) { + // logger.debug(error); + // toast.error('Something went wrong'); + // } + } + + function handleRemoveOne(id: string, cb: () => void): void { + MarkOneAsRead(id) + .then(() => { + toast.success('Notification marked as read'); + cb(); + }) + .catch((error) => { + logger.debug(error); + toast.error('Something went wrong'); + }); + } return ( + {/* */} ); diff --git a/002_source/cms/src/db/Notifications/GetNotificationByUserId.tsx b/002_source/cms/src/db/Notifications/GetNotificationByUserId.tsx index 3271212..a867abe 100644 --- a/002_source/cms/src/db/Notifications/GetNotificationByUserId.tsx +++ b/002_source/cms/src/db/Notifications/GetNotificationByUserId.tsx @@ -1,14 +1,18 @@ // // RULES: // api method for get notifications by user id -import { pb } from '@/lib/pb'; import { COL_NOTIFICATIONS } from '@/constants'; + +import { pb } from '@/lib/pb'; + import type { Notification } from './type.d'; export async function getNotificationsByUserId(userId: string): Promise { const records = await pb.collection(COL_NOTIFICATIONS).getFullList({ - filter: `author.id = "000000000000001"`, + expand: 'author, to_user_id', + filter: `to_user_id.id = "${userId}"`, sort: '-created', }); + return records as unknown as Notification[]; } diff --git a/002_source/cms/src/db/Notifications/GetUnreadNotificationsByUserId.tsx b/002_source/cms/src/db/Notifications/GetUnreadNotificationsByUserId.tsx new file mode 100644 index 0000000..cb9647f --- /dev/null +++ b/002_source/cms/src/db/Notifications/GetUnreadNotificationsByUserId.tsx @@ -0,0 +1,20 @@ +// +// RULES: +// api method for get notifications by user id +import { COL_NOTIFICATIONS } from '@/constants'; + +import { pb } from '@/lib/pb'; + +import type { Notification } from './type.d'; + +export async function getUnreadNotificationsByUserId(userId: string): Promise { + const records = await pb.collection(COL_NOTIFICATIONS).getFullList({ + expand: 'author, to_user_id', + filter: `to_user_id.id = "${userId}" && read = false`, + sort: '-created', + cache: 'no-cache', + requestKey: null, + }); + + return records as unknown as Notification[]; +} diff --git a/002_source/cms/src/db/Notifications/mark-one-as-read.tsx b/002_source/cms/src/db/Notifications/mark-one-as-read.tsx new file mode 100644 index 0000000..828f6f3 --- /dev/null +++ b/002_source/cms/src/db/Notifications/mark-one-as-read.tsx @@ -0,0 +1,11 @@ +// api method for update notification record +// RULES: +// TBA +import { pb } from '@/lib/pb'; +import { COL_NOTIFICATIONS } from '@/constants'; +import type { RecordModel } from 'pocketbase'; +import type { NotificationFormProps } from '@/components/dashboard/notification/type.d'; + +export async function MarkOneAsRead(id: string): Promise { + return pb.collection(COL_NOTIFICATIONS).update(id, { read: true }); +} diff --git a/002_source/cms/src/db/Notifications/type.d.ts b/002_source/cms/src/db/Notifications/type.d.ts index ae7910e..0666f42 100644 --- a/002_source/cms/src/db/Notifications/type.d.ts +++ b/002_source/cms/src/db/Notifications/type.d.ts @@ -1,17 +1,21 @@ 'use client'; +import type { User } from '@/types/user'; + export type SortDir = 'asc' | 'desc'; export interface Notification { id: string; + created: string; + createdAt: Date; read: boolean; type: string; - author: Record; - job: Record; - description: string; - NOTI_ID: string; - created: string; - updated: string; + author?: User; + job?: { title: string }; + description?: string; + company?: { name: string }; + to_user_id?: User; + link: string; } export interface CreateFormProps {