update notifications,

This commit is contained in:
louiscklaw
2025-04-24 20:02:56 +08:00
parent b8e8968866
commit 2dcc765072
17 changed files with 348 additions and 57 deletions

View File

@@ -18,46 +18,14 @@ 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 { dayjs } from '@/lib/dayjs';
// import type { Notification } from './type.d.tsx.del';
export type Notification = { id: string; createdAt: Date; read: boolean } & ( import { SampleNotifications } from './sample-notifications';
| { type: 'new_feature'; description: string } import { useHelloworld } from '@/hooks/use-helloworld';
| { type: 'new_company'; author: { name: string; avatar?: string }; company: { name: string } } import { getAllNotifications } from '@/db/Notifications/GetAll';
| { type: 'new_job'; author: { name: string; avatar?: string }; job: { title: string } } import { ListResult, RecordModel } from 'pocketbase';
); import { defaultNotification } from '@/db/Notifications/constants';
import { getNotificationsByUserId } from '@/db/Notifications/GetNotificationByUserId';
const notifications = [ import { Notification } from '@/db/Notifications/type';
{
id: 'EV-004',
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' },
job: { title: 'Remote React / React Native Developer' },
},
{
id: 'EV-003',
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' },
job: { title: 'Senior Golang Backend Engineer' },
},
{
id: 'EV-002',
createdAt: dayjs().subtract(4, 'minute').subtract(5, 'hour').subtract(7, 'day').toDate(),
read: true,
type: 'new_feature',
description: 'Logistics management is now available',
},
{
id: 'EV-001',
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' },
company: { name: 'Stripe' },
},
] satisfies Notification[];
export interface NotificationsPopoverProps { export interface NotificationsPopoverProps {
anchorEl: null | Element; anchorEl: null | Element;
@@ -75,6 +43,25 @@ export function NotificationsPopover({
open = false, open = false,
}: NotificationsPopoverProps): React.JSX.Element { }: NotificationsPopoverProps): React.JSX.Element {
const { t } = useTranslation(); const { t } = useTranslation();
const [notiList, setNotiList] = React.useState<Notification[]>([]);
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState<string | null>(null);
const { data, handleClose, handleOpen, open: testOpen } = useHelloworld<string>();
React.useEffect(() => {
setLoading(true);
async function LoadAllNotifications() {
const notiList: Notification[] = await getNotificationsByUserId('1');
setNotiList(notiList);
}
setLoading(false);
void LoadAllNotifications();
}, []);
if (loading) return <>Loading</>;
if (error) return <>Error</>;
if (notiList.length == 0)
return ( return (
<Popover <Popover
anchorEl={anchorEl} anchorEl={anchorEl}
@@ -84,24 +71,44 @@ export function NotificationsPopover({
slotProps={{ paper: { sx: { width: '380px' } } }} slotProps={{ paper: { sx: { width: '380px' } } }}
transformOrigin={{ horizontal: 'right', vertical: 'top' }} transformOrigin={{ horizontal: 'right', vertical: 'top' }}
> >
<Stack direction="row" spacing={2} sx={{ alignItems: 'center', justifyContent: 'space-between', px: 3, py: 2 }}> list is empty
</Popover>
);
return (
<Popover
anchorEl={anchorEl}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
onClose={onClose}
open={open}
slotProps={{ paper: { sx: { width: '380px' } } }}
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
>
<Stack
direction="row"
spacing={2}
sx={{ alignItems: 'center', justifyContent: 'space-between', px: 3, py: 2 }}
>
<Typography variant="h6">{t('Notifications')}</Typography> <Typography variant="h6">{t('Notifications')}</Typography>
<Tooltip title={t('Mark all as read')}> <Tooltip title={t('Mark all as read')}>
<IconButton edge="end" onClick={onMarkAllAsRead}> <IconButton
edge="end"
onClick={onMarkAllAsRead}
>
<EnvelopeSimpleIcon /> <EnvelopeSimpleIcon />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
</Stack> </Stack>
{notifications.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>
</Box> </Box>
) : ( ) : (
<Box sx={{ maxHeight: '270px', overflowY: 'auto' }}> <Box sx={{ maxHeight: '270px', overflowY: 'auto' }}>
<List disablePadding> <List disablePadding>
{notifications.map((notification, index) => ( {notiList.map((notification, index) => (
<NotificationItem <NotificationItem
divider={index < notifications.length - 1} divider={index < SampleNotifications.length - 1}
key={notification.id} key={notification.id}
notification={notification} notification={notification}
onRemove={() => { onRemove={() => {
@@ -124,10 +131,17 @@ interface NotificationItemProps {
function NotificationItem({ divider, notification, onRemove }: NotificationItemProps): React.JSX.Element { function NotificationItem({ divider, notification, onRemove }: NotificationItemProps): React.JSX.Element {
return ( return (
<ListItem divider={divider} sx={{ alignItems: 'flex-start', justifyContent: 'space-between' }}> <ListItem
divider={divider}
sx={{ alignItems: 'flex-start', justifyContent: 'space-between' }}
>
<NotificationContent notification={notification} /> <NotificationContent notification={notification} />
<Tooltip title="Remove"> <Tooltip title="Remove">
<IconButton edge="end" onClick={onRemove} size="small"> <IconButton
edge="end"
onClick={onRemove}
size="small"
>
<XIcon /> <XIcon />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
@@ -142,14 +156,21 @@ interface NotificationContentProps {
function NotificationContent({ notification }: NotificationContentProps): React.JSX.Element { function NotificationContent({ notification }: NotificationContentProps): React.JSX.Element {
if (notification.type === 'new_feature') { if (notification.type === 'new_feature') {
return ( return (
<Stack direction="row" spacing={2} sx={{ alignItems: 'flex-start' }}> <Stack
direction="row"
spacing={2}
sx={{ alignItems: 'flex-start' }}
>
<Avatar> <Avatar>
<ChatTextIcon fontSize="var(--Icon-fontSize)" /> <ChatTextIcon fontSize="var(--Icon-fontSize)" />
</Avatar> </Avatar>
<div> <div>
<Typography variant="subtitle2">New feature!</Typography> <Typography variant="subtitle2">New feature!</Typography>
<Typography variant="body2">{notification.description}</Typography> <Typography variant="body2">{notification.description}</Typography>
<Typography color="text.secondary" variant="caption"> <Typography
color="text.secondary"
variant="caption"
>
{dayjs(notification.createdAt).format('MMM D, hh:mm A')} {dayjs(notification.createdAt).format('MMM D, hh:mm A')}
</Typography> </Typography>
</div> </div>
@@ -159,22 +180,35 @@ function NotificationContent({ notification }: NotificationContentProps): React.
if (notification.type === 'new_company') { if (notification.type === 'new_company') {
return ( return (
<Stack direction="row" spacing={2} sx={{ alignItems: 'flex-start' }}> <Stack
direction="row"
spacing={2}
sx={{ alignItems: 'flex-start' }}
>
<Avatar src={notification.author.avatar}> <Avatar src={notification.author.avatar}>
<UserIcon /> <UserIcon />
</Avatar> </Avatar>
<div> <div>
<Typography variant="body2"> <Typography variant="body2">
<Typography component="span" variant="subtitle2"> <Typography
component="span"
variant="subtitle2"
>
{notification.author.name} {notification.author.name}
</Typography>{' '} </Typography>{' '}
created{' '} created{' '}
<Link underline="always" variant="body2"> <Link
underline="always"
variant="body2"
>
{notification.company.name} {notification.company.name}
</Link>{' '} </Link>{' '}
company company
</Typography> </Typography>
<Typography color="text.secondary" variant="caption"> <Typography
color="text.secondary"
variant="caption"
>
{dayjs(notification.createdAt).format('MMM D, hh:mm A')} {dayjs(notification.createdAt).format('MMM D, hh:mm A')}
</Typography> </Typography>
</div> </div>
@@ -184,21 +218,34 @@ function NotificationContent({ notification }: NotificationContentProps): React.
if (notification.type === 'new_job') { if (notification.type === 'new_job') {
return ( return (
<Stack direction="row" spacing={2} sx={{ alignItems: 'flex-start' }}> <Stack
direction="row"
spacing={2}
sx={{ alignItems: 'flex-start' }}
>
<Avatar src={notification.author.avatar}> <Avatar src={notification.author.avatar}>
<UserIcon /> <UserIcon />
</Avatar> </Avatar>
<div> <div>
<Typography variant="body2"> <Typography variant="body2">
<Typography component="span" variant="subtitle2"> <Typography
component="span"
variant="subtitle2"
>
{notification.author.name} {notification.author.name}
</Typography>{' '} </Typography>{' '}
added a new job{' '} added a new job{' '}
<Link underline="always" variant="body2"> <Link
underline="always"
variant="body2"
>
{notification.job.title} {notification.job.title}
</Link> </Link>
</Typography> </Typography>
<Typography color="text.secondary" variant="caption"> <Typography
color="text.secondary"
variant="caption"
>
{dayjs(notification.createdAt).format('MMM D, hh:mm A')} {dayjs(notification.createdAt).format('MMM D, hh:mm A')}
</Typography> </Typography>
</div> </div>

View File

@@ -0,0 +1,37 @@
'use client';
import { dayjs } from '@/lib/dayjs';
import type { Notification } from './type.d.tsx.del';
export const SampleNotifications = [
{
id: 'EV-004',
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' },
job: { title: 'Remote React / React Native Developer' },
},
{
id: 'EV-003',
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' },
job: { title: 'Senior Golang Backend Engineer' },
},
{
id: 'EV-002',
createdAt: dayjs().subtract(4, 'minute').subtract(5, 'hour').subtract(7, 'day').toDate(),
read: true,
type: 'new_feature',
description: 'Logistics management is now available',
},
{
id: 'EV-001',
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' },
company: { name: 'Stripe' },
},
] satisfies Notification[];

View File

@@ -0,0 +1,12 @@
export interface NotificationFormProps {
id?: string;
title: string;
message: string;
isRead: boolean;
createdAt: Date;
updatedAt: Date;
}
export interface NotificationItem extends NotificationFormProps {
id: string;
}

View File

@@ -0,0 +1,11 @@
// api method for create notification record
// RULES:
// TBA
import { pb } from '@/lib/pb';
import { COL_NOTIFICATIONS } from '@/constants';
import type { NotificationFormProps } from '@/components/dashboard/notification/type.d';
import type { RecordModel } from 'pocketbase';
export async function createNotification(data: NotificationFormProps): Promise<RecordModel> {
return pb.collection(COL_NOTIFICATIONS).create(data);
}

View File

@@ -0,0 +1,9 @@
// api method for delete notification record
// RULES:
// TBA
import { pb } from '@/lib/pb';
import { COL_NOTIFICATIONS } from '@/constants';
export async function deleteNotification(id: string): Promise<boolean> {
return pb.collection(COL_NOTIFICATIONS).delete(id);
}

View File

@@ -0,0 +1,9 @@
import { COL_CUSTOMERS } from '@/constants';
import { pb } from '@/lib/pb';
export default async function GetActiveCount(): Promise<number> {
const { totalItems: count } = await pb.collection(COL_CUSTOMERS).getList(1, 1, {
filter: 'status = "active"',
});
return count;
}

View File

@@ -0,0 +1,10 @@
// api method for get all notification records
// RULES:
// TBA
import { pb } from '@/lib/pb';
import { COL_NOTIFICATIONS } from '@/constants';
import { RecordModel } from 'pocketbase';
export async function getAllNotifications(options = {}): Promise<RecordModel[]> {
return pb.collection(COL_NOTIFICATIONS).getFullList(options);
}

View File

@@ -0,0 +1,7 @@
import { pb } from '@/lib/pb';
import { COL_CUSTOMERS } from '@/constants';
export async function getAllCustomersCount(): Promise<number> {
const result = await pb.collection(COL_CUSTOMERS).getList(1, 1);
return result.totalItems;
}

View File

@@ -0,0 +1,9 @@
import { COL_CUSTOMERS } from '@/constants';
import { pb } from '@/lib/pb';
export default async function GetBlockedCount(): Promise<number> {
const { totalItems: count } = await pb.collection(COL_CUSTOMERS).getList(1, 1, {
filter: 'status = "blocked"',
});
return count;
}

View File

@@ -0,0 +1,10 @@
// api method for get notification by id
// RULES:
// TBA
import { pb } from '@/lib/pb';
import { COL_NOTIFICATIONS } from '@/constants';
import { RecordModel } from 'pocketbase';
export async function getNotificationById(id: string): Promise<RecordModel> {
return pb.collection(COL_NOTIFICATIONS).getOne(id);
}

View File

@@ -0,0 +1,12 @@
// api method for get notifications by user id
import { pb } from '@/lib/pb';
import { COL_NOTIFICATIONS } from '@/constants';
import type { Notification } from './type.d';
export async function getNotificationsByUserId(userId: string): Promise<Notification[]> {
const records = await pb.collection(COL_NOTIFICATIONS).getFullList({
filter: `author.id = "000000000000001"`,
sort: '-created',
});
return records as unknown as Notification[];
}

View File

@@ -0,0 +1,9 @@
import { COL_CUSTOMERS } from '@/constants';
import { pb } from '@/lib/pb';
export default async function GetPendingCount(): Promise<number> {
const { totalItems: count } = await pb.collection(COL_CUSTOMERS).getList(1, 1, {
filter: 'status = "pending"',
});
return count;
}

View File

@@ -0,0 +1,3 @@
export function helloCustomer() {
return 'Hello from Customers module!';
}

View File

@@ -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 updateNotification(id: string, data: Partial<NotificationFormProps>): Promise<RecordModel> {
return pb.collection(COL_NOTIFICATIONS).update(id, data);
}

View File

@@ -0,0 +1,31 @@
# GUIDELINES
This folder contains drivers for `Customer`/`Customers` records using PocketBase:
- create (Create.tsx)
- read (GetById.tsx)
- write (Update.tsx)
- count (GetAllCount.tsx, GetActiveCount.tsx, GetBlockedCount.tsx, GetPendingCount.tsx)
- misc (Helloworld.tsx)
- delete (Delete.tsx)
- list (GetAll.tsx)
the `@` sign refer to `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src`
## Assumption and Requirements
- assume `pb` is located in `@/lib/pb`
- no need to handle error in this function, i'll handle it in the caller
- type information defined in `/home/logic/_wsl_workspace/001_github_ws/lettersoup-online-ws/lettersoup-online/project/002_source/cms/src/db/Customers/type.d.tsx`
simple template:
```typescript
import { pb } from '@/lib/pb';
import { COL_CUSTOMERS } from '@/constants';
export async function createCustomer(data: CreateFormProps) {
// ...content
// use direct return of pb.collection (e.g. return pb.collection(xxx))
}
```

View File

@@ -0,0 +1,19 @@
// Default and empty values for Notification
import type { Notification } from './type';
export const defaultNotification: Notification = {
id: '',
read: false,
type: '',
author: {},
job: {},
description: '',
NOTI_ID: '',
created: '',
updated: '',
};
export const emptyNotification: Notification = {
...defaultNotification,
};

View File

@@ -0,0 +1,45 @@
'use client';
export type SortDir = 'asc' | 'desc';
export interface Notification {
id: string;
read: boolean;
type: string;
author: Record<string, unknown>;
job: Record<string, unknown>;
description: string;
NOTI_ID: string;
created: string;
updated: string;
}
export interface CreateFormProps {
read?: boolean;
type: string;
author: Record<string, unknown>;
job: Record<string, unknown>;
description: string;
NOTI_ID: string;
}
export interface EditFormProps {
read?: boolean;
type?: string;
author?: Record<string, unknown>;
job?: Record<string, unknown>;
description?: string;
NOTI_ID?: string;
}
export interface NotificationsFiltersProps {
filters?: Filters;
sortDir?: SortDir;
fullData: Notification[];
}
export interface Filters {
type?: string;
read?: boolean;
NOTI_ID?: string;
}