update notifications,
This commit is contained in:
@@ -18,46 +18,14 @@ import { X as XIcon } from '@phosphor-icons/react/dist/ssr/X';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { dayjs } from '@/lib/dayjs';
|
||||
|
||||
export type Notification = { id: string; createdAt: Date; read: boolean } & (
|
||||
| { type: 'new_feature'; description: string }
|
||||
| { type: 'new_company'; author: { name: string; avatar?: string }; company: { name: string } }
|
||||
| { type: 'new_job'; author: { name: string; avatar?: string }; job: { title: string } }
|
||||
);
|
||||
|
||||
const notifications = [
|
||||
{
|
||||
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[];
|
||||
// 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';
|
||||
|
||||
export interface NotificationsPopoverProps {
|
||||
anchorEl: null | Element;
|
||||
@@ -75,6 +43,38 @@ export function NotificationsPopover({
|
||||
open = false,
|
||||
}: NotificationsPopoverProps): React.JSX.Element {
|
||||
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 (
|
||||
<Popover
|
||||
anchorEl={anchorEl}
|
||||
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
|
||||
onClose={onClose}
|
||||
open={open}
|
||||
slotProps={{ paper: { sx: { width: '380px' } } }}
|
||||
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
|
||||
>
|
||||
list is empty
|
||||
</Popover>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
anchorEl={anchorEl}
|
||||
@@ -84,24 +84,31 @@ export function NotificationsPopover({
|
||||
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 }}>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={2}
|
||||
sx={{ alignItems: 'center', justifyContent: 'space-between', px: 3, py: 2 }}
|
||||
>
|
||||
<Typography variant="h6">{t('Notifications')}</Typography>
|
||||
<Tooltip title={t('Mark all as read')}>
|
||||
<IconButton edge="end" onClick={onMarkAllAsRead}>
|
||||
<IconButton
|
||||
edge="end"
|
||||
onClick={onMarkAllAsRead}
|
||||
>
|
||||
<EnvelopeSimpleIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
{notifications.length === 0 ? (
|
||||
{notiList.length === 0 ? (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="subtitle2">{t('There are no notifications')}</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ maxHeight: '270px', overflowY: 'auto' }}>
|
||||
<List disablePadding>
|
||||
{notifications.map((notification, index) => (
|
||||
{notiList.map((notification, index) => (
|
||||
<NotificationItem
|
||||
divider={index < notifications.length - 1}
|
||||
divider={index < SampleNotifications.length - 1}
|
||||
key={notification.id}
|
||||
notification={notification}
|
||||
onRemove={() => {
|
||||
@@ -124,10 +131,17 @@ interface NotificationItemProps {
|
||||
|
||||
function NotificationItem({ divider, notification, onRemove }: NotificationItemProps): React.JSX.Element {
|
||||
return (
|
||||
<ListItem divider={divider} sx={{ alignItems: 'flex-start', justifyContent: 'space-between' }}>
|
||||
<ListItem
|
||||
divider={divider}
|
||||
sx={{ alignItems: 'flex-start', justifyContent: 'space-between' }}
|
||||
>
|
||||
<NotificationContent notification={notification} />
|
||||
<Tooltip title="Remove">
|
||||
<IconButton edge="end" onClick={onRemove} size="small">
|
||||
<IconButton
|
||||
edge="end"
|
||||
onClick={onRemove}
|
||||
size="small"
|
||||
>
|
||||
<XIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
@@ -142,14 +156,21 @@ interface NotificationContentProps {
|
||||
function NotificationContent({ notification }: NotificationContentProps): React.JSX.Element {
|
||||
if (notification.type === 'new_feature') {
|
||||
return (
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'flex-start' }}>
|
||||
<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">
|
||||
<Typography
|
||||
color="text.secondary"
|
||||
variant="caption"
|
||||
>
|
||||
{dayjs(notification.createdAt).format('MMM D, hh:mm A')}
|
||||
</Typography>
|
||||
</div>
|
||||
@@ -159,22 +180,35 @@ function NotificationContent({ notification }: NotificationContentProps): React.
|
||||
|
||||
if (notification.type === 'new_company') {
|
||||
return (
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'flex-start' }}>
|
||||
<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">
|
||||
<Typography
|
||||
component="span"
|
||||
variant="subtitle2"
|
||||
>
|
||||
{notification.author.name}
|
||||
</Typography>{' '}
|
||||
created{' '}
|
||||
<Link underline="always" variant="body2">
|
||||
<Link
|
||||
underline="always"
|
||||
variant="body2"
|
||||
>
|
||||
{notification.company.name}
|
||||
</Link>{' '}
|
||||
company
|
||||
</Typography>
|
||||
<Typography color="text.secondary" variant="caption">
|
||||
<Typography
|
||||
color="text.secondary"
|
||||
variant="caption"
|
||||
>
|
||||
{dayjs(notification.createdAt).format('MMM D, hh:mm A')}
|
||||
</Typography>
|
||||
</div>
|
||||
@@ -184,21 +218,34 @@ function NotificationContent({ notification }: NotificationContentProps): React.
|
||||
|
||||
if (notification.type === 'new_job') {
|
||||
return (
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'flex-start' }}>
|
||||
<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">
|
||||
<Typography
|
||||
component="span"
|
||||
variant="subtitle2"
|
||||
>
|
||||
{notification.author.name}
|
||||
</Typography>{' '}
|
||||
added a new job{' '}
|
||||
<Link underline="always" variant="body2">
|
||||
<Link
|
||||
underline="always"
|
||||
variant="body2"
|
||||
>
|
||||
{notification.job.title}
|
||||
</Link>
|
||||
</Typography>
|
||||
<Typography color="text.secondary" variant="caption">
|
||||
<Typography
|
||||
color="text.secondary"
|
||||
variant="caption"
|
||||
>
|
||||
{dayjs(notification.createdAt).format('MMM D, hh:mm A')}
|
||||
</Typography>
|
||||
</div>
|
@@ -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[];
|
12
002_source/cms/src/components/dashboard/notification/type.d.ts
vendored
Normal file
12
002_source/cms/src/components/dashboard/notification/type.d.ts
vendored
Normal 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;
|
||||
}
|
11
002_source/cms/src/db/Notifications/Create.tsx
Normal file
11
002_source/cms/src/db/Notifications/Create.tsx
Normal 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);
|
||||
}
|
9
002_source/cms/src/db/Notifications/Delete.tsx
Normal file
9
002_source/cms/src/db/Notifications/Delete.tsx
Normal 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);
|
||||
}
|
9
002_source/cms/src/db/Notifications/GetActiveCount.tsx
Normal file
9
002_source/cms/src/db/Notifications/GetActiveCount.tsx
Normal 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;
|
||||
}
|
10
002_source/cms/src/db/Notifications/GetAll.tsx
Normal file
10
002_source/cms/src/db/Notifications/GetAll.tsx
Normal 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);
|
||||
}
|
7
002_source/cms/src/db/Notifications/GetAllCount.tsx
Normal file
7
002_source/cms/src/db/Notifications/GetAllCount.tsx
Normal 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;
|
||||
}
|
9
002_source/cms/src/db/Notifications/GetBlockedCount.tsx
Normal file
9
002_source/cms/src/db/Notifications/GetBlockedCount.tsx
Normal 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;
|
||||
}
|
10
002_source/cms/src/db/Notifications/GetById.tsx
Normal file
10
002_source/cms/src/db/Notifications/GetById.tsx
Normal 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);
|
||||
}
|
@@ -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[];
|
||||
}
|
9
002_source/cms/src/db/Notifications/GetPendingCount.tsx
Normal file
9
002_source/cms/src/db/Notifications/GetPendingCount.tsx
Normal 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;
|
||||
}
|
3
002_source/cms/src/db/Notifications/Helloworld.tsx
Normal file
3
002_source/cms/src/db/Notifications/Helloworld.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export function helloCustomer() {
|
||||
return 'Hello from Customers module!';
|
||||
}
|
11
002_source/cms/src/db/Notifications/Update.tsx
Normal file
11
002_source/cms/src/db/Notifications/Update.tsx
Normal 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);
|
||||
}
|
31
002_source/cms/src/db/Notifications/_GUIDELINES.md
Normal file
31
002_source/cms/src/db/Notifications/_GUIDELINES.md
Normal 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))
|
||||
}
|
||||
```
|
19
002_source/cms/src/db/Notifications/constants.ts
Normal file
19
002_source/cms/src/db/Notifications/constants.ts
Normal 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,
|
||||
};
|
45
002_source/cms/src/db/Notifications/type.d.ts
vendored
Normal file
45
002_source/cms/src/db/Notifications/type.d.ts
vendored
Normal 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;
|
||||
}
|
Reference in New Issue
Block a user