strapi-plugin-notifier 1.0.0
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/LICENSE +21 -0
- package/README.md +150 -0
- package/dist/_chunks/Index-CiDBgkGf.js +310 -0
- package/dist/_chunks/Index-DI2t9tlH.mjs +310 -0
- package/dist/_chunks/SettingsPage-BdBxZMqP.js +137 -0
- package/dist/_chunks/SettingsPage-DIKfBfvo.mjs +137 -0
- package/dist/_chunks/index-7WJGsVEY.js +28282 -0
- package/dist/_chunks/index-CNYabBMJ.mjs +28264 -0
- package/dist/admin/index.js +4 -0
- package/dist/admin/index.mjs +5 -0
- package/dist/server/index.js +466 -0
- package/dist/server/index.mjs +467 -0
- package/package.json +86 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 datrine
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# strapi-plugin-notifier
|
|
2
|
+
|
|
3
|
+
A first-class notification system for Strapi v5 admin panels. Adds a live bell icon to the sidebar, a full notification inbox, a runtime Settings panel, and a simple API to send notifications from anywhere in your Strapi application.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Live bell icon** — badge count updated by polling the server (interval configurable)
|
|
8
|
+
- **Notification inbox** — filter by type, mark as read, dismiss, load more, clear all
|
|
9
|
+
- **Targeting** — broadcast to all admins, or target by user ID or role code
|
|
10
|
+
- **Configurable** — poll interval, page size, retention policy, and UI accent colours
|
|
11
|
+
- **Settings panel** — runtime configuration via Settings → Notifier (persisted in Strapi plugin store)
|
|
12
|
+
- **Retention cron** — daily cleanup at 3 AM, respects maxDays and maxPerUser limits
|
|
13
|
+
- **Strapi v5 only**
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install strapi-plugin-notifier
|
|
19
|
+
# or
|
|
20
|
+
yarn add strapi-plugin-notifier
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Enable the plugin in `config/plugins.ts`:
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
export default {
|
|
27
|
+
notifier: {
|
|
28
|
+
enabled: true,
|
|
29
|
+
config: {
|
|
30
|
+
// all fields are optional — see Configuration below
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Configuration
|
|
37
|
+
|
|
38
|
+
All configuration is optional. Built-in defaults apply unless overridden.
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
// config/plugins.ts
|
|
42
|
+
export default {
|
|
43
|
+
notifier: {
|
|
44
|
+
enabled: true,
|
|
45
|
+
config: {
|
|
46
|
+
retention: {
|
|
47
|
+
maxDays: 90, // delete notifications older than N days
|
|
48
|
+
maxPerUser: 500, // cap notifications stored per user
|
|
49
|
+
},
|
|
50
|
+
delivery: {
|
|
51
|
+
pollIntervalMs: 30_000, // how often the Bell polls (ms)
|
|
52
|
+
pageSize: 20, // notifications per page in inbox
|
|
53
|
+
},
|
|
54
|
+
ui: {
|
|
55
|
+
theme: {
|
|
56
|
+
accent: {
|
|
57
|
+
info: '#4945ff',
|
|
58
|
+
success: '#5cb85c',
|
|
59
|
+
warning: '#f0ad4e',
|
|
60
|
+
error: '#ee5e52',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Settings can also be updated at runtime from the Strapi admin panel under **Settings → Notifier**. Runtime settings take precedence over `config/plugins.ts`.
|
|
70
|
+
|
|
71
|
+
## Sending notifications
|
|
72
|
+
|
|
73
|
+
Use the `notifier` service from anywhere in your Strapi code (services, controllers, lifecycles, webhooks):
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
const notifier = strapi.plugin('notifier').service('notifier');
|
|
77
|
+
|
|
78
|
+
// Broadcast to all admin users
|
|
79
|
+
notifier.broadcast({ title: 'Maintenance scheduled', type: 'warning' });
|
|
80
|
+
|
|
81
|
+
// Send to a specific role
|
|
82
|
+
notifier.toRole('strapi-editor', {
|
|
83
|
+
title: 'New content submitted',
|
|
84
|
+
message: 'An article is waiting for review.',
|
|
85
|
+
url: '/content-manager/collection-types/api::article.article',
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Send to a specific admin user (by user ID)
|
|
89
|
+
notifier.toUser(42, {
|
|
90
|
+
title: 'Your export is ready',
|
|
91
|
+
type: 'success',
|
|
92
|
+
url: '/uploads/export-2024.csv',
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Generic send with full control
|
|
96
|
+
notifier.send({
|
|
97
|
+
title: 'Hello',
|
|
98
|
+
message: 'World',
|
|
99
|
+
type: 'info',
|
|
100
|
+
url: 'https://example.com',
|
|
101
|
+
to: { role: 'strapi-super-admin' },
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Notification options
|
|
106
|
+
|
|
107
|
+
| Field | Type | Required | Default |
|
|
108
|
+
|-----------|-------------------------------------------|----------|-------------|
|
|
109
|
+
| `title` | `string` | Yes | — |
|
|
110
|
+
| `message` | `string` | No | — |
|
|
111
|
+
| `type` | `'info' \| 'success' \| 'warning' \| 'error'` | No | `'info'` |
|
|
112
|
+
| `url` | `string` | No | — |
|
|
113
|
+
|
|
114
|
+
## API routes
|
|
115
|
+
|
|
116
|
+
All routes require `admin::isAuthenticatedAdmin`. The plugin mounts under `/notifier/`.
|
|
117
|
+
|
|
118
|
+
| Method | Path | Description |
|
|
119
|
+
|--------|---------------------------------|--------------------------------------|
|
|
120
|
+
| GET | `/notifier/notifications` | List notifications (paginated) |
|
|
121
|
+
| PUT | `/notifier/notifications/read-all` | Mark all as read |
|
|
122
|
+
| DELETE | `/notifier/notifications` | Clear all notifications |
|
|
123
|
+
| PUT | `/notifier/notifications/:id/read` | Mark one as read |
|
|
124
|
+
| DELETE | `/notifier/notifications/:id` | Clear one notification |
|
|
125
|
+
| GET | `/notifier/config` | Fetch UI config (safe for frontend) |
|
|
126
|
+
| GET | `/notifier/settings` | Get full settings (requires permission) |
|
|
127
|
+
| PUT | `/notifier/settings` | Update settings (requires permission) |
|
|
128
|
+
| DELETE | `/notifier/settings` | Reset settings to defaults |
|
|
129
|
+
|
|
130
|
+
Query parameters for `GET /notifier/notifications`:
|
|
131
|
+
|
|
132
|
+
| Param | Default | Description |
|
|
133
|
+
|------------|---------|---------------------|
|
|
134
|
+
| `page` | `1` | Page number |
|
|
135
|
+
| `pageSize` | `20` | Results per page |
|
|
136
|
+
|
|
137
|
+
## Permissions
|
|
138
|
+
|
|
139
|
+
Two permissions are registered under the **Notifier** plugin section in **Settings → Roles**:
|
|
140
|
+
|
|
141
|
+
- `plugin::notifier.settings.read` — view the settings panel
|
|
142
|
+
- `plugin::notifier.settings.update` — save or reset settings
|
|
143
|
+
|
|
144
|
+
## Content type
|
|
145
|
+
|
|
146
|
+
Notifications are stored in `plugin::notifier.notification` (collection: `notifier_notifications`). The content type is hidden from Content Manager and Content-Type Builder by default.
|
|
147
|
+
|
|
148
|
+
## License
|
|
149
|
+
|
|
150
|
+
MIT
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const jsxRuntime = require("react/jsx-runtime");
|
|
4
|
+
const React = require("react");
|
|
5
|
+
const designSystem = require("@strapi/design-system");
|
|
6
|
+
require("react-dom/client");
|
|
7
|
+
const icons = require("@strapi/icons");
|
|
8
|
+
const index = require("./index-7WJGsVEY.js");
|
|
9
|
+
require("@strapi/icons/symbols");
|
|
10
|
+
const toStoreItem = (n) => ({
|
|
11
|
+
id: String(n.id),
|
|
12
|
+
title: n.title,
|
|
13
|
+
message: n.message ?? "",
|
|
14
|
+
type: n.type ?? "info",
|
|
15
|
+
read: n.read,
|
|
16
|
+
createdAt: n.createdAt,
|
|
17
|
+
url: n.url
|
|
18
|
+
});
|
|
19
|
+
const useNotificationActions = () => {
|
|
20
|
+
const { get, put, del } = index.useFetchClient();
|
|
21
|
+
const [isLoading, setIsLoading] = React.useState(false);
|
|
22
|
+
const [pagination, setPagination] = React.useState(null);
|
|
23
|
+
const currentPage = React.useRef(1);
|
|
24
|
+
const fetchPage = React.useCallback(
|
|
25
|
+
async (page, append = false) => {
|
|
26
|
+
const { pageSize } = index.getConfig();
|
|
27
|
+
setIsLoading(true);
|
|
28
|
+
try {
|
|
29
|
+
const { data } = await get(
|
|
30
|
+
`/notifier/notifications?page=${page}&pageSize=${pageSize}`
|
|
31
|
+
);
|
|
32
|
+
if (Array.isArray(data?.data)) {
|
|
33
|
+
const incoming = data.data.map(toStoreItem);
|
|
34
|
+
if (append) {
|
|
35
|
+
index.setNotifications([...index.getNotifications(), ...incoming]);
|
|
36
|
+
} else {
|
|
37
|
+
index.setNotifications(incoming);
|
|
38
|
+
}
|
|
39
|
+
setPagination(data.pagination ?? null);
|
|
40
|
+
currentPage.current = page;
|
|
41
|
+
}
|
|
42
|
+
} finally {
|
|
43
|
+
setIsLoading(false);
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
[get]
|
|
47
|
+
);
|
|
48
|
+
const fetchAll = React.useCallback(() => fetchPage(1, false), [fetchPage]);
|
|
49
|
+
const loadMore = React.useCallback(() => {
|
|
50
|
+
if (!pagination || currentPage.current >= pagination.pageCount) return;
|
|
51
|
+
fetchPage(currentPage.current + 1, true);
|
|
52
|
+
}, [fetchPage, pagination]);
|
|
53
|
+
const handleMarkAsRead = React.useCallback(
|
|
54
|
+
async (id) => {
|
|
55
|
+
index.markAsRead(id);
|
|
56
|
+
try {
|
|
57
|
+
await put(`/notifier/notifications/${id}/read`);
|
|
58
|
+
} catch {
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
[put]
|
|
62
|
+
);
|
|
63
|
+
const handleMarkAllAsRead = React.useCallback(async () => {
|
|
64
|
+
index.markAllAsRead();
|
|
65
|
+
try {
|
|
66
|
+
await put("/notifier/notifications/read-all");
|
|
67
|
+
} catch {
|
|
68
|
+
}
|
|
69
|
+
}, [put]);
|
|
70
|
+
const handleClear = React.useCallback(
|
|
71
|
+
async (id) => {
|
|
72
|
+
index.clearNotification(id);
|
|
73
|
+
try {
|
|
74
|
+
await del(`/notifier/notifications/${id}`);
|
|
75
|
+
} catch {
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
[del]
|
|
79
|
+
);
|
|
80
|
+
const handleClearAll = React.useCallback(async () => {
|
|
81
|
+
index.clearAll();
|
|
82
|
+
try {
|
|
83
|
+
await del("/notifier/notifications");
|
|
84
|
+
} catch {
|
|
85
|
+
}
|
|
86
|
+
}, [del]);
|
|
87
|
+
const hasMore = pagination ? currentPage.current < pagination.pageCount : false;
|
|
88
|
+
return {
|
|
89
|
+
fetchAll,
|
|
90
|
+
loadMore,
|
|
91
|
+
handleMarkAsRead,
|
|
92
|
+
handleMarkAllAsRead,
|
|
93
|
+
handleClear,
|
|
94
|
+
handleClearAll,
|
|
95
|
+
isLoading,
|
|
96
|
+
hasMore
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
const FILTERS = ["all", "unread", "info", "success", "warning", "error"];
|
|
100
|
+
const FILTER_LABELS = {
|
|
101
|
+
all: "All",
|
|
102
|
+
unread: "Unread",
|
|
103
|
+
info: "Info",
|
|
104
|
+
success: "Success",
|
|
105
|
+
warning: "Warning",
|
|
106
|
+
error: "Error"
|
|
107
|
+
};
|
|
108
|
+
function InboxHeader({
|
|
109
|
+
filter,
|
|
110
|
+
onFilterChange,
|
|
111
|
+
onMarkAllAsRead,
|
|
112
|
+
onClearAll
|
|
113
|
+
}) {
|
|
114
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { paddingBottom: 4, children: [
|
|
115
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "center", paddingBottom: 4, children: [
|
|
116
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "alpha", children: "Notifications" }),
|
|
117
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, children: [
|
|
118
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "tertiary", size: "S", onClick: onMarkAllAsRead, children: "Mark all as read" }),
|
|
119
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
120
|
+
designSystem.Button,
|
|
121
|
+
{
|
|
122
|
+
variant: "danger-light",
|
|
123
|
+
size: "S",
|
|
124
|
+
onClick: onClearAll,
|
|
125
|
+
children: "Clear all"
|
|
126
|
+
}
|
|
127
|
+
)
|
|
128
|
+
] })
|
|
129
|
+
] }),
|
|
130
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { gap: 2, wrap: "wrap", children: FILTERS.map((f) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
131
|
+
"button",
|
|
132
|
+
{
|
|
133
|
+
onClick: () => onFilterChange(f),
|
|
134
|
+
style: {
|
|
135
|
+
padding: "4px 12px",
|
|
136
|
+
borderRadius: "16px",
|
|
137
|
+
border: "1px solid",
|
|
138
|
+
borderColor: filter === f ? "#4945ff" : "#dcdce4",
|
|
139
|
+
backgroundColor: filter === f ? "#f0f0ff" : "transparent",
|
|
140
|
+
color: filter === f ? "#4945ff" : "#32324d",
|
|
141
|
+
cursor: "pointer",
|
|
142
|
+
fontSize: "12px",
|
|
143
|
+
fontWeight: filter === f ? 600 : 400,
|
|
144
|
+
transition: "all 0.15s"
|
|
145
|
+
},
|
|
146
|
+
children: FILTER_LABELS[f]
|
|
147
|
+
},
|
|
148
|
+
f
|
|
149
|
+
)) })
|
|
150
|
+
] });
|
|
151
|
+
}
|
|
152
|
+
const useNotifications = () => {
|
|
153
|
+
const [notifications, setNotifications] = React.useState(index.getNotifications);
|
|
154
|
+
React.useEffect(() => index.subscribeToNotifications(setNotifications), []);
|
|
155
|
+
return notifications;
|
|
156
|
+
};
|
|
157
|
+
const UNREAD_BG = {
|
|
158
|
+
info: "#f0f0ff",
|
|
159
|
+
success: "#f0fff4",
|
|
160
|
+
warning: "#fffbf0",
|
|
161
|
+
error: "#fff5f5"
|
|
162
|
+
};
|
|
163
|
+
function NotificationItem({
|
|
164
|
+
notification,
|
|
165
|
+
onMarkAsRead,
|
|
166
|
+
onClear
|
|
167
|
+
}) {
|
|
168
|
+
const config = index.usePluginConfig();
|
|
169
|
+
const accent = config.theme.accent[notification.type];
|
|
170
|
+
const handleClick = () => {
|
|
171
|
+
if (!notification.read) onMarkAsRead(notification.id);
|
|
172
|
+
if (notification.url) {
|
|
173
|
+
if (notification.url.startsWith("http")) {
|
|
174
|
+
window.open(notification.url, "_blank", "noopener,noreferrer");
|
|
175
|
+
} else {
|
|
176
|
+
window.location.href = notification.url;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
const handleDismiss = (e) => {
|
|
181
|
+
e.stopPropagation();
|
|
182
|
+
onClear(notification.id);
|
|
183
|
+
};
|
|
184
|
+
const bg = !notification.read ? UNREAD_BG[notification.type] : "transparent";
|
|
185
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
186
|
+
designSystem.Box,
|
|
187
|
+
{
|
|
188
|
+
onClick: handleClick,
|
|
189
|
+
style: {
|
|
190
|
+
borderLeft: `4px solid ${accent}`,
|
|
191
|
+
backgroundColor: bg,
|
|
192
|
+
cursor: notification.url ? "pointer" : "default",
|
|
193
|
+
transition: "background 0.15s"
|
|
194
|
+
},
|
|
195
|
+
padding: 4,
|
|
196
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "flex-start", gap: 2, children: [
|
|
197
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { style: { flex: 1, minWidth: 0 }, children: [
|
|
198
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 1, alignItems: "center", children: [
|
|
199
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
200
|
+
designSystem.Typography,
|
|
201
|
+
{
|
|
202
|
+
variant: "omega",
|
|
203
|
+
fontWeight: notification.read ? "normal" : "bold",
|
|
204
|
+
style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
|
|
205
|
+
children: notification.title
|
|
206
|
+
}
|
|
207
|
+
),
|
|
208
|
+
notification.url && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: "↗" })
|
|
209
|
+
] }),
|
|
210
|
+
notification.message && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", style: { marginTop: 2 }, children: notification.message }),
|
|
211
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral400", style: { marginTop: 4 }, children: new Date(notification.createdAt).toLocaleString() })
|
|
212
|
+
] }),
|
|
213
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
214
|
+
designSystem.IconButton,
|
|
215
|
+
{
|
|
216
|
+
label: "Dismiss",
|
|
217
|
+
onClick: handleDismiss,
|
|
218
|
+
variant: "ghost",
|
|
219
|
+
size: "S",
|
|
220
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(icons.Cross, {})
|
|
221
|
+
}
|
|
222
|
+
)
|
|
223
|
+
] })
|
|
224
|
+
}
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
const MESSAGES = {
|
|
228
|
+
all: "No notifications yet.",
|
|
229
|
+
unread: "No unread notifications.",
|
|
230
|
+
info: "No info notifications.",
|
|
231
|
+
success: "No success notifications.",
|
|
232
|
+
warning: "No warning notifications.",
|
|
233
|
+
error: "No error notifications."
|
|
234
|
+
};
|
|
235
|
+
function EmptyInbox({ filter = "all" }) {
|
|
236
|
+
return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { padding: 8, style: { textAlign: "center" }, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", textColor: "neutral500", children: MESSAGES[filter] }) });
|
|
237
|
+
}
|
|
238
|
+
const applyFilter = (items, filter) => {
|
|
239
|
+
if (filter === "all") return items;
|
|
240
|
+
if (filter === "unread") return items.filter((n) => !n.read);
|
|
241
|
+
return items.filter((n) => n.type === filter);
|
|
242
|
+
};
|
|
243
|
+
function NotificationList({
|
|
244
|
+
filter,
|
|
245
|
+
onMarkAsRead,
|
|
246
|
+
onClear,
|
|
247
|
+
isLoading,
|
|
248
|
+
hasMore,
|
|
249
|
+
onLoadMore
|
|
250
|
+
}) {
|
|
251
|
+
const notifications = useNotifications();
|
|
252
|
+
const filtered = applyFilter(notifications, filter);
|
|
253
|
+
if (isLoading && notifications.length === 0) {
|
|
254
|
+
return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { small: true, children: "Loading notifications…" }) });
|
|
255
|
+
}
|
|
256
|
+
if (filtered.length === 0) {
|
|
257
|
+
return /* @__PURE__ */ jsxRuntime.jsx(EmptyInbox, { filter });
|
|
258
|
+
}
|
|
259
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { children: [
|
|
260
|
+
filtered.map((n) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
261
|
+
NotificationItem,
|
|
262
|
+
{
|
|
263
|
+
notification: n,
|
|
264
|
+
onMarkAsRead,
|
|
265
|
+
onClear
|
|
266
|
+
},
|
|
267
|
+
n.id
|
|
268
|
+
)),
|
|
269
|
+
hasMore && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "tertiary", onClick: onLoadMore, loading: isLoading, children: isLoading ? "Loading…" : "Load more" }) })
|
|
270
|
+
] });
|
|
271
|
+
}
|
|
272
|
+
function Index() {
|
|
273
|
+
const [filter, setFilter] = React.useState("all");
|
|
274
|
+
const {
|
|
275
|
+
fetchAll,
|
|
276
|
+
loadMore,
|
|
277
|
+
handleMarkAsRead,
|
|
278
|
+
handleMarkAllAsRead,
|
|
279
|
+
handleClear,
|
|
280
|
+
handleClearAll,
|
|
281
|
+
isLoading,
|
|
282
|
+
hasMore
|
|
283
|
+
} = useNotificationActions();
|
|
284
|
+
React.useEffect(() => {
|
|
285
|
+
fetchAll();
|
|
286
|
+
}, []);
|
|
287
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { padding: 8, style: { height: "100%" }, children: [
|
|
288
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
289
|
+
InboxHeader,
|
|
290
|
+
{
|
|
291
|
+
filter,
|
|
292
|
+
onFilterChange: setFilter,
|
|
293
|
+
onMarkAllAsRead: handleMarkAllAsRead,
|
|
294
|
+
onClearAll: handleClearAll
|
|
295
|
+
}
|
|
296
|
+
),
|
|
297
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
298
|
+
NotificationList,
|
|
299
|
+
{
|
|
300
|
+
filter,
|
|
301
|
+
onMarkAsRead: handleMarkAsRead,
|
|
302
|
+
onClear: handleClear,
|
|
303
|
+
isLoading,
|
|
304
|
+
hasMore,
|
|
305
|
+
onLoadMore: loadMore
|
|
306
|
+
}
|
|
307
|
+
)
|
|
308
|
+
] });
|
|
309
|
+
}
|
|
310
|
+
exports.default = Index;
|