datastake-daf 0.6.501 → 0.6.503
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/layouts/index.css +1 -0
- package/dist/layouts/index.js +7137 -0
- package/dist/utils/index.js +287 -0
- package/package.json +1 -1
- package/rollup.config.js +25 -0
- package/src/@daf/hooks/usePermissions.js +20 -0
- package/src/@daf/layouts/AppLayout/AppLayout.stories.js +532 -0
- package/src/@daf/layouts/AppLayout/components/LoginPopup/index.js +116 -0
- package/src/@daf/layouts/AppLayout/components/LoginPopup/style.js +26 -0
- package/src/@daf/layouts/AppLayout/components/MobileDrawer/index.js +208 -0
- package/src/@daf/layouts/AppLayout/components/MobileDrawer/style.js +87 -0
- package/src/@daf/layouts/AppLayout/components/Notifications/Notification/index.js +31 -0
- package/src/@daf/layouts/AppLayout/components/Notifications/NotificationHistory/index.js +22 -0
- package/src/@daf/layouts/AppLayout/components/Notifications/context/index.js +79 -0
- package/src/@daf/layouts/AppLayout/components/Notifications/index.js +106 -0
- package/src/@daf/layouts/AppLayout/components/Notifications/style.js +61 -0
- package/src/@daf/layouts/AppLayout/components/Notifications/useNotifications.js +105 -0
- package/src/@daf/layouts/AppLayout/components/Sidenav/index.js +296 -0
- package/src/@daf/layouts/AppLayout/components/UserDropdown/UserIcon.js +96 -0
- package/src/@daf/layouts/AppLayout/components/UserDropdown/index.js +112 -0
- package/src/@daf/layouts/AppLayout/index.jsx +384 -0
- package/src/@daf/layouts/AppLayout/index.scss +5 -0
- package/src/@daf/layouts/AppLayout/styles/header.scss +257 -0
- package/src/@daf/layouts/AppLayout/styles/layout.scss +76 -0
- package/src/@daf/layouts/AppLayout/styles/responsive.scss +52 -0
- package/src/@daf/layouts/AppLayout/styles/sidebar.scss +79 -0
- package/src/@daf/layouts/AppLayout/styles/variables.scss +22 -0
- package/src/helpers/theme.js +304 -0
- package/src/helpers/user.js +4 -0
- package/src/layouts.js +1 -0
- package/src/utils.js +4 -2
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/* eslint-disable react/prop-types */
|
|
2
|
+
import React, { useCallback, useMemo } from "react";
|
|
3
|
+
import SidenavMenu from "../../../../core/components/Sidenav/Menu.jsx";
|
|
4
|
+
import { formatClassname } from '../../../../../helpers/ClassesHelper.js';
|
|
5
|
+
import { renderModule } from "../Sidenav";
|
|
6
|
+
import { Style } from "./style";
|
|
7
|
+
|
|
8
|
+
const isCollapsed = false;
|
|
9
|
+
const selectedKeys = [];
|
|
10
|
+
|
|
11
|
+
export default function MobileDrawer({
|
|
12
|
+
mod = 'pme',
|
|
13
|
+
toggle = () => { },
|
|
14
|
+
drawerOpened = false,
|
|
15
|
+
isUserDropdown = false,
|
|
16
|
+
user,
|
|
17
|
+
sidenavConfig = {},
|
|
18
|
+
navigate,
|
|
19
|
+
t = (key) => key,
|
|
20
|
+
checkPermission = () => false,
|
|
21
|
+
logOut,
|
|
22
|
+
changeNotificationState,
|
|
23
|
+
matchPath,
|
|
24
|
+
appName = 'app',
|
|
25
|
+
userHelpers = {},
|
|
26
|
+
isDev = false,
|
|
27
|
+
selectedProject,
|
|
28
|
+
}) {
|
|
29
|
+
const items = useMemo(() => sidenavConfig[mod] || [], [sidenavConfig, mod]);
|
|
30
|
+
|
|
31
|
+
const canViewPartners = useMemo(() => mod === 'tazama', [mod]);
|
|
32
|
+
const canViewProjects = checkPermission({
|
|
33
|
+
permission: 'projects.canView',
|
|
34
|
+
permissions: user?.role?.permissions
|
|
35
|
+
});
|
|
36
|
+
const canViewUsers = checkPermission({
|
|
37
|
+
permission: 'users.canView',
|
|
38
|
+
permissions: user?.role?.permissions
|
|
39
|
+
});
|
|
40
|
+
const canViewSettings = checkPermission({
|
|
41
|
+
permission: 'settings.canView',
|
|
42
|
+
permissions: user?.role?.permissions
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const hasMultipleApps = useMemo(() =>
|
|
46
|
+
Object.keys(user?.modules || {}).filter((k) => user.modules[k].status === 'approved').length > 1,
|
|
47
|
+
[user]
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const handleLogOut = useCallback(() => {
|
|
51
|
+
const hasPrevious = (localStorage.getItem('previous'));
|
|
52
|
+
sessionStorage.removeItem('notifications');
|
|
53
|
+
if (hasPrevious) {
|
|
54
|
+
logOut?.({ isImpersonation: true });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
logOut?.();
|
|
58
|
+
}, [logOut]);
|
|
59
|
+
|
|
60
|
+
const goTo = (...props) => {
|
|
61
|
+
navigate?.(...props);
|
|
62
|
+
toggle();
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const filteredItems = useMemo(() => {
|
|
66
|
+
const mapItems = (item) => {
|
|
67
|
+
const isDisabled = typeof item.isDisabled === 'function' ?
|
|
68
|
+
item.isDisabled(user, selectedProject, selectedProject)
|
|
69
|
+
: item.isDisabled || false;
|
|
70
|
+
|
|
71
|
+
if (item.items) {
|
|
72
|
+
return {
|
|
73
|
+
...item,
|
|
74
|
+
isDisabled,
|
|
75
|
+
items: item.items.filter((item) => {
|
|
76
|
+
const res = typeof item.visible === 'function' ?
|
|
77
|
+
item.visible(user, selectedProject, selectedProject) : true;
|
|
78
|
+
return res;
|
|
79
|
+
}).map(mapItems)
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { ...item, isDisabled };
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
if (isUserDropdown) {
|
|
87
|
+
return [
|
|
88
|
+
{
|
|
89
|
+
type: 'link',
|
|
90
|
+
name: t('Projects'),
|
|
91
|
+
path: `/app/${mod}/projects`,
|
|
92
|
+
isDashboard: true,
|
|
93
|
+
visible: canViewProjects
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
type: 'link',
|
|
97
|
+
name: t('Partners'),
|
|
98
|
+
path: `/app/${mod}/partners`,
|
|
99
|
+
isDashboard: true,
|
|
100
|
+
visible: canViewPartners
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
type: 'link',
|
|
104
|
+
name: t('Users'),
|
|
105
|
+
path: `/app/users`,
|
|
106
|
+
isDashboard: true,
|
|
107
|
+
visible: canViewUsers
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
type: 'link',
|
|
111
|
+
name: t('Settings'),
|
|
112
|
+
path: `/app/${mod}/view/settings`,
|
|
113
|
+
isDashboard: true,
|
|
114
|
+
visible: canViewSettings
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
type: 'link',
|
|
118
|
+
name: t('Log out'),
|
|
119
|
+
onClick: () => handleLogOut(),
|
|
120
|
+
isDashboard: true,
|
|
121
|
+
visible: true
|
|
122
|
+
},
|
|
123
|
+
]
|
|
124
|
+
.filter((v) => v.visible)
|
|
125
|
+
.map(mapItems);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return items
|
|
129
|
+
.filter(item => {
|
|
130
|
+
const res = typeof item.visible === 'function' ?
|
|
131
|
+
item.visible(user, selectedProject, selectedProject) : true;
|
|
132
|
+
return res;
|
|
133
|
+
})
|
|
134
|
+
.map(mapItems);
|
|
135
|
+
}, [
|
|
136
|
+
items,
|
|
137
|
+
user,
|
|
138
|
+
selectedProject,
|
|
139
|
+
t,
|
|
140
|
+
isUserDropdown,
|
|
141
|
+
mod,
|
|
142
|
+
handleLogOut,
|
|
143
|
+
hasMultipleApps,
|
|
144
|
+
canViewPartners,
|
|
145
|
+
canViewUsers,
|
|
146
|
+
canViewSettings,
|
|
147
|
+
canViewProjects
|
|
148
|
+
]);
|
|
149
|
+
|
|
150
|
+
const checkOnClick = ({ event }) => {
|
|
151
|
+
changeNotificationState({
|
|
152
|
+
onYes: () => {
|
|
153
|
+
event();
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<Style className={formatClassname([!drawerOpened && 'closed', isUserDropdown && 'user-dropdown'])}>
|
|
160
|
+
<div className="drawer">
|
|
161
|
+
<div
|
|
162
|
+
className={formatClassname([
|
|
163
|
+
isCollapsed ? 'sidenav-sider-collapsed sidenav-sider flex-1' : 'sidenav-sider sidenav-sider-opened flex-1',
|
|
164
|
+
appName
|
|
165
|
+
])}
|
|
166
|
+
style={{ width: isCollapsed ? '70px' : '250px', minWidth: isCollapsed ? 'auto' : '250px' }}
|
|
167
|
+
>
|
|
168
|
+
<div className="flex">
|
|
169
|
+
<div className="flex-1">
|
|
170
|
+
{mod === 'app' || !user ? null : renderModule({
|
|
171
|
+
isCollapsed,
|
|
172
|
+
mod: mod,
|
|
173
|
+
module: mod,
|
|
174
|
+
user,
|
|
175
|
+
userHelpers
|
|
176
|
+
})}
|
|
177
|
+
</div>
|
|
178
|
+
<div className="cursor-pointer close-icon" onClick={toggle}>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
<div className="sidenav-cont">
|
|
182
|
+
<div className={formatClassname(['drawer', 'menus-cont', 'not-collapsed'])}>
|
|
183
|
+
<SidenavMenu
|
|
184
|
+
module={mod}
|
|
185
|
+
menuMode="inline"
|
|
186
|
+
selectedKeys={selectedKeys}
|
|
187
|
+
filteredItems={filteredItems}
|
|
188
|
+
isCollapsed={false}
|
|
189
|
+
setHoverItem={() => { }}
|
|
190
|
+
setTopHover={() => { }}
|
|
191
|
+
setHoverOpen={() => { }}
|
|
192
|
+
onMouseMove={() => { }}
|
|
193
|
+
user={user}
|
|
194
|
+
goTo={goTo}
|
|
195
|
+
isDev={isDev}
|
|
196
|
+
showSettings
|
|
197
|
+
t={t}
|
|
198
|
+
checkOnClick={checkOnClick}
|
|
199
|
+
project={selectedProject}
|
|
200
|
+
matchPath={matchPath}
|
|
201
|
+
/>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
</Style>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import styled from "styled-components";
|
|
2
|
+
|
|
3
|
+
export const Style = styled.div`
|
|
4
|
+
position: fixed;
|
|
5
|
+
background: rgba(0, 0, 0, 0.4);
|
|
6
|
+
width: 100dvw;
|
|
7
|
+
height: calc(100dvh - 64px);
|
|
8
|
+
top: 64px;
|
|
9
|
+
left: 0px;
|
|
10
|
+
z-index: 1001;
|
|
11
|
+
display: flex;
|
|
12
|
+
flex-direction: row;
|
|
13
|
+
animation: .4s fadeIn forwards;
|
|
14
|
+
|
|
15
|
+
&.user-dropdown {
|
|
16
|
+
justify-content: flex-end;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
div {
|
|
20
|
+
line-height: 0px;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.sidenav-sider {
|
|
24
|
+
display: flex;
|
|
25
|
+
flex-direction: column;
|
|
26
|
+
|
|
27
|
+
.anticon {
|
|
28
|
+
line-height: 0px !important;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
span[role="img"] {
|
|
32
|
+
padding: 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.close-icon {
|
|
36
|
+
margin-top: 20px;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.mod-name {
|
|
40
|
+
margin-bottom: 20px;
|
|
41
|
+
margin-top: 20px;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.drawer {
|
|
46
|
+
width: 250px;
|
|
47
|
+
transition: .4s margin-right;
|
|
48
|
+
height: 100%;
|
|
49
|
+
display: flex;
|
|
50
|
+
flex-direction: column;
|
|
51
|
+
|
|
52
|
+
.sidenav-cont {
|
|
53
|
+
svg {
|
|
54
|
+
margin-right: 0px;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
span {
|
|
58
|
+
padding-left: 0px;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
height: unset;
|
|
62
|
+
flex: 1;
|
|
63
|
+
overflow-y: auto;
|
|
64
|
+
max-height: calc(100dvh - 125px);
|
|
65
|
+
|
|
66
|
+
.log-out {
|
|
67
|
+
padding-top: var(--size-lg);
|
|
68
|
+
margin: 0px var(--size-lg);
|
|
69
|
+
cursor: pointer;
|
|
70
|
+
|
|
71
|
+
span {
|
|
72
|
+
padding-left: 10px;
|
|
73
|
+
margin-left: 10px;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@keyframes fadeIn {
|
|
80
|
+
from { opacity: 0; }
|
|
81
|
+
to { opacity: 1; }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
&.closed {
|
|
85
|
+
display: none;
|
|
86
|
+
}
|
|
87
|
+
`;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/* eslint-disable react/prop-types */
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { formatClassname } from "../../../../../../helpers/ClassesHelper.js";
|
|
4
|
+
import { useNotificationsContext } from "../context";
|
|
5
|
+
|
|
6
|
+
export const Notification = ({
|
|
7
|
+
n,
|
|
8
|
+
toggle = () => {},
|
|
9
|
+
t = (key) => key,
|
|
10
|
+
navigate,
|
|
11
|
+
}) => {
|
|
12
|
+
const { removeNotification } = useNotificationsContext();
|
|
13
|
+
|
|
14
|
+
const handleClick = () => {
|
|
15
|
+
if (n.data?.link) {
|
|
16
|
+
removeNotification(n.id);
|
|
17
|
+
toggle();
|
|
18
|
+
navigate?.(n.data.link);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div
|
|
24
|
+
className={formatClassname(['noti', n.data?.link && 'clickable'])}
|
|
25
|
+
onClick={handleClick}
|
|
26
|
+
>
|
|
27
|
+
<strong>{n.title || t('Notification')}</strong>
|
|
28
|
+
<div>{n.body}</div>
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/* eslint-disable react/prop-types */
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { Modal } from "antd";
|
|
4
|
+
|
|
5
|
+
export default function NotificationHistory({
|
|
6
|
+
visible,
|
|
7
|
+
setHistoryVisible,
|
|
8
|
+
t = (key) => key,
|
|
9
|
+
}) {
|
|
10
|
+
return (
|
|
11
|
+
<Modal
|
|
12
|
+
open={visible}
|
|
13
|
+
onCancel={() => setHistoryVisible(false)}
|
|
14
|
+
footer={null}
|
|
15
|
+
title={t('Notification History')}
|
|
16
|
+
>
|
|
17
|
+
<div style={{ padding: '20px' }}>
|
|
18
|
+
{t('Notification history content will appear here')}
|
|
19
|
+
</div>
|
|
20
|
+
</Modal>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/* eslint-disable react/prop-types */
|
|
2
|
+
import moment from "moment";
|
|
3
|
+
import React, { createContext, useContext, useCallback } from "react";
|
|
4
|
+
import useNotifications from "../useNotifications.js";
|
|
5
|
+
|
|
6
|
+
const status = 'unread';
|
|
7
|
+
|
|
8
|
+
export const NotificationsContext = createContext({
|
|
9
|
+
loading: false,
|
|
10
|
+
_notifications: [],
|
|
11
|
+
_fetch: () => {},
|
|
12
|
+
total: 0,
|
|
13
|
+
clearAll: () => { },
|
|
14
|
+
removeNotification: () => { },
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const NotificationsProvider = ({
|
|
18
|
+
children,
|
|
19
|
+
user,
|
|
20
|
+
notificationHandlers = {},
|
|
21
|
+
NotificationsHistoryProvider,
|
|
22
|
+
firebaseEnabled = true,
|
|
23
|
+
useFirebaseHook,
|
|
24
|
+
}) => {
|
|
25
|
+
const {
|
|
26
|
+
loading,
|
|
27
|
+
_notifications,
|
|
28
|
+
_fetch,
|
|
29
|
+
total,
|
|
30
|
+
addNotification,
|
|
31
|
+
clearAll,
|
|
32
|
+
removeNotification,
|
|
33
|
+
} = useNotifications({ status, user, ...notificationHandlers });
|
|
34
|
+
|
|
35
|
+
const value = {
|
|
36
|
+
loading,
|
|
37
|
+
_notifications,
|
|
38
|
+
_fetch,
|
|
39
|
+
total,
|
|
40
|
+
clearAll,
|
|
41
|
+
removeNotification,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const onMessage = useCallback((notification) => {
|
|
45
|
+
let data = {};
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
data = JSON.parse(notification.data?.data);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
console.log(err);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
addNotification({
|
|
54
|
+
id: notification.messageId,
|
|
55
|
+
type: notification.data?.type,
|
|
56
|
+
title: notification.notification?.title,
|
|
57
|
+
body: notification.notification?.body,
|
|
58
|
+
createdAt: moment().toString(),
|
|
59
|
+
data: data,
|
|
60
|
+
read: false,
|
|
61
|
+
});
|
|
62
|
+
}, [addNotification]);
|
|
63
|
+
|
|
64
|
+
// Only use Firebase if enabled and hook provided
|
|
65
|
+
if (firebaseEnabled && useFirebaseHook) {
|
|
66
|
+
useFirebaseHook(onMessage);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<NotificationsContext.Provider value={value}>
|
|
71
|
+
{children}
|
|
72
|
+
</NotificationsContext.Provider>
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const useNotificationsContext = () => {
|
|
77
|
+
const value = useContext(NotificationsContext);
|
|
78
|
+
return value;
|
|
79
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/* eslint-disable react/prop-types */
|
|
2
|
+
import { Dropdown } from "antd";
|
|
3
|
+
import { LoadingOutlined } from "@ant-design/icons";
|
|
4
|
+
import React, { useState } from "react";
|
|
5
|
+
import CustomIcon from '../../../../core/components/Icon/CustomIcon.jsx';
|
|
6
|
+
import { NotificationsStyle } from './style';
|
|
7
|
+
import { useNotificationsContext } from "./context";
|
|
8
|
+
import { Notification } from "./Notification";
|
|
9
|
+
import NotificationHistory from "./NotificationHistory";
|
|
10
|
+
import { formatClassname } from "../../../../../helpers/ClassesHelper.js";
|
|
11
|
+
|
|
12
|
+
export default function Notifications({
|
|
13
|
+
mod = 'pme',
|
|
14
|
+
toggle = () => { },
|
|
15
|
+
t = (key) => key,
|
|
16
|
+
navigate,
|
|
17
|
+
appName = 'app',
|
|
18
|
+
}) {
|
|
19
|
+
const [historyVisible, setHistoryVisible] = useState(false);
|
|
20
|
+
const { loading, _notifications, _fetch, total, clearAll } = useNotificationsContext();
|
|
21
|
+
|
|
22
|
+
const items = [
|
|
23
|
+
{
|
|
24
|
+
key: "details",
|
|
25
|
+
icon: <CustomIcon name="CheckDone" width={13} height={13} />,
|
|
26
|
+
disabled: !total,
|
|
27
|
+
onClick: () => {
|
|
28
|
+
clearAll();
|
|
29
|
+
toggle();
|
|
30
|
+
},
|
|
31
|
+
label: t('Clear All'),
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
key: "publish",
|
|
35
|
+
icon: <CustomIcon name="NotificationText" width={13} height={13} />,
|
|
36
|
+
onClick: () => {
|
|
37
|
+
toggle();
|
|
38
|
+
setHistoryVisible(true);
|
|
39
|
+
},
|
|
40
|
+
label: t('View All'),
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
key: "dashboard",
|
|
44
|
+
icon: <CustomIcon name="Settings" width={13} height={13} />,
|
|
45
|
+
onClick: () => {
|
|
46
|
+
toggle();
|
|
47
|
+
navigate?.(`/app/${mod}/view/settings?activeForm=notifications`);
|
|
48
|
+
},
|
|
49
|
+
label: t('Settings'),
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<NotificationsStyle className={appName}>
|
|
55
|
+
<div className="notis-title">
|
|
56
|
+
<h3>{t('sbg::notification')}</h3>
|
|
57
|
+
<Dropdown
|
|
58
|
+
menu={{ items }}
|
|
59
|
+
trigger="click"
|
|
60
|
+
rootClassName={formatClassname(['dark-menu', appName])}
|
|
61
|
+
>
|
|
62
|
+
<div className="cursor-pointer">
|
|
63
|
+
<CustomIcon name="MoreCustom" width={3} height={13} />
|
|
64
|
+
</div>
|
|
65
|
+
</Dropdown>
|
|
66
|
+
</div>
|
|
67
|
+
<div
|
|
68
|
+
className="notis-list"
|
|
69
|
+
onScroll={(e) => {
|
|
70
|
+
const _t = e.target;
|
|
71
|
+
const canFetch = (_t.scrollTop + _t.clientHeight) >= _t.scrollHeight - 200;
|
|
72
|
+
|
|
73
|
+
if (canFetch) {
|
|
74
|
+
_fetch();
|
|
75
|
+
}
|
|
76
|
+
}}
|
|
77
|
+
>
|
|
78
|
+
{!loading && !total ? (
|
|
79
|
+
<div className="no-notis">
|
|
80
|
+
{t('No new notifications')}
|
|
81
|
+
</div>
|
|
82
|
+
) : null}
|
|
83
|
+
{_notifications.map((n, i) => (
|
|
84
|
+
<Notification
|
|
85
|
+
toggle={toggle}
|
|
86
|
+
key={`notifications-${i + 1}`}
|
|
87
|
+
n={n}
|
|
88
|
+
t={t}
|
|
89
|
+
navigate={navigate}
|
|
90
|
+
/>
|
|
91
|
+
))}
|
|
92
|
+
{loading && (
|
|
93
|
+
<div className="loading-cont">
|
|
94
|
+
<LoadingOutlined />
|
|
95
|
+
</div>
|
|
96
|
+
)}
|
|
97
|
+
</div>
|
|
98
|
+
<NotificationHistory
|
|
99
|
+
visible={historyVisible}
|
|
100
|
+
setHistoryVisible={setHistoryVisible}
|
|
101
|
+
t={t}
|
|
102
|
+
navigate={navigate}
|
|
103
|
+
/>
|
|
104
|
+
</NotificationsStyle>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import styled from 'styled-components';
|
|
2
|
+
|
|
3
|
+
export const NotificationsStyle = styled.div`
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
|
|
7
|
+
.notis-list {
|
|
8
|
+
max-height: 40vh;
|
|
9
|
+
overflow-y: auto;
|
|
10
|
+
padding: 0px var(--size-lg) var(--size-lg) var(--size-lg);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.notis-title {
|
|
14
|
+
display: flex;
|
|
15
|
+
color: white;
|
|
16
|
+
padding: var(--size-lg) var(--size-lg) 0 var(--size-lg);
|
|
17
|
+
margin-bottom: 8px;
|
|
18
|
+
|
|
19
|
+
h3 {
|
|
20
|
+
color: white;
|
|
21
|
+
font-size: 18px;
|
|
22
|
+
flex: 1;
|
|
23
|
+
margin-bottom: 0px;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.noti {
|
|
28
|
+
white-space: wrap;
|
|
29
|
+
padding: var(--size) 0px;
|
|
30
|
+
transition: 0.4s background;
|
|
31
|
+
user-select: none;
|
|
32
|
+
color: white;
|
|
33
|
+
border-bottom: 1px solid #384250;
|
|
34
|
+
|
|
35
|
+
&:last-of-type {
|
|
36
|
+
padding-bottom: 0px;
|
|
37
|
+
border-bottom: 0px;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
strong {
|
|
41
|
+
color: white;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
&.clickable {
|
|
45
|
+
cursor: pointer;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.no-notis {
|
|
50
|
+
color: white;
|
|
51
|
+
text-align: center;
|
|
52
|
+
padding: var(--size-lg) 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.loading-cont {
|
|
56
|
+
display: flex;
|
|
57
|
+
justify-content: center;
|
|
58
|
+
padding: var(--size-lg) 0;
|
|
59
|
+
color: white;
|
|
60
|
+
}
|
|
61
|
+
`;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
const useNotifications = ({
|
|
4
|
+
status = 'unread',
|
|
5
|
+
user,
|
|
6
|
+
fetchNotifications,
|
|
7
|
+
markAsRead,
|
|
8
|
+
markAllAsRead,
|
|
9
|
+
}) => {
|
|
10
|
+
const [loading, setLoading] = useState(false);
|
|
11
|
+
const [notifications, setNotifications] = useState([]);
|
|
12
|
+
const [total, setTotal] = useState(0);
|
|
13
|
+
const [page, setPage] = useState(0);
|
|
14
|
+
const [hasMore, setHasMore] = useState(true);
|
|
15
|
+
const isFetching = useRef(false);
|
|
16
|
+
|
|
17
|
+
const _notifications = useMemo(() => {
|
|
18
|
+
return notifications.filter((n) => !n.read);
|
|
19
|
+
}, [notifications]);
|
|
20
|
+
|
|
21
|
+
const _fetch = useCallback(async () => {
|
|
22
|
+
if (!fetchNotifications || isFetching.current || !hasMore || loading) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
isFetching.current = true;
|
|
28
|
+
setLoading(true);
|
|
29
|
+
|
|
30
|
+
const response = await fetchNotifications({
|
|
31
|
+
page: page + 1,
|
|
32
|
+
status,
|
|
33
|
+
limit: 20,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const newNotifications = response.data || [];
|
|
37
|
+
|
|
38
|
+
if (newNotifications.length === 0) {
|
|
39
|
+
setHasMore(false);
|
|
40
|
+
} else {
|
|
41
|
+
setNotifications((prev) => [...prev, ...newNotifications]);
|
|
42
|
+
setPage((p) => p + 1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
setTotal(response.total || 0);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
console.error('Error fetching notifications:', err);
|
|
48
|
+
} finally {
|
|
49
|
+
setLoading(false);
|
|
50
|
+
isFetching.current = false;
|
|
51
|
+
}
|
|
52
|
+
}, [fetchNotifications, page, status, hasMore, loading]);
|
|
53
|
+
|
|
54
|
+
const removeNotification = useCallback(async (id) => {
|
|
55
|
+
try {
|
|
56
|
+
if (markAsRead) {
|
|
57
|
+
await markAsRead(id);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
setNotifications((prev) =>
|
|
61
|
+
prev.map((n) => n.id === id ? { ...n, read: true } : n)
|
|
62
|
+
);
|
|
63
|
+
setTotal((t) => Math.max(0, t - 1));
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.error('Error removing notification:', err);
|
|
66
|
+
}
|
|
67
|
+
}, [markAsRead]);
|
|
68
|
+
|
|
69
|
+
const clearAll = useCallback(async () => {
|
|
70
|
+
try {
|
|
71
|
+
if (markAllAsRead) {
|
|
72
|
+
await markAllAsRead();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
setNotifications((prev) => prev.map((n) => ({ ...n, read: true })));
|
|
76
|
+
setTotal(0);
|
|
77
|
+
} catch (err) {
|
|
78
|
+
console.error('Error clearing notifications:', err);
|
|
79
|
+
}
|
|
80
|
+
}, [markAllAsRead]);
|
|
81
|
+
|
|
82
|
+
const addNotification = useCallback((notification) => {
|
|
83
|
+
setNotifications((prev) => [notification, ...prev]);
|
|
84
|
+
setTotal((t) => t + 1);
|
|
85
|
+
}, []);
|
|
86
|
+
|
|
87
|
+
// Initial fetch
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
if (user) {
|
|
90
|
+
_fetch();
|
|
91
|
+
}
|
|
92
|
+
}, [user]);
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
loading,
|
|
96
|
+
_notifications,
|
|
97
|
+
_fetch,
|
|
98
|
+
total,
|
|
99
|
+
clearAll,
|
|
100
|
+
removeNotification,
|
|
101
|
+
addNotification,
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export default useNotifications;
|