Skip to content

Commit c9d8a45

Browse files
committed
notifications: add typed categories and align type selector in admin toolbar
1 parent 52e5ff4 commit c9d8a45

7 files changed

Lines changed: 99 additions & 10 deletions

File tree

api/src/controllers/notifications.controller.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ function stripHtmlToText(html) {
2525
.trim();
2626
}
2727

28+
function normalizeNotificationType(rawType) {
29+
const value = String(rawType || "general").trim().toLowerCase();
30+
if (value === "security" || value === "general" || value === "updates") {
31+
return value;
32+
}
33+
return "";
34+
}
35+
2836
export const getMine = asyncHandler(async (req, res) => {
2937
const notifications = await listActiveNotificationsForUser(req.user.id, 20);
3038
res.json({ notifications });
@@ -46,15 +54,22 @@ export const listAdmin = asyncHandler(async (_req, res) => {
4654

4755
export const createAdmin = asyncHandler(async (req, res) => {
4856
const rawHtml = String(req.body?.messageHtml || "").trim();
57+
const notificationType = normalizeNotificationType(req.body?.notificationType);
4958
const html = sanitizeNotificationHtml(rawHtml);
5059
const text = stripHtmlToText(html);
5160
if (!html || !text) {
5261
return res.status(400).json({ message: "Notification text is required" });
5362
}
63+
if (!notificationType) {
64+
return res
65+
.status(400)
66+
.json({ message: "notificationType must be one of: security, general, updates" });
67+
}
5468

5569
const notification = await createNotification({
5670
messageHtml: html,
5771
messageText: text,
72+
notificationType,
5873
createdBy: req.user.id,
5974
});
6075
res.status(201).json({
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
-- src/db/migrations/029_add_notification_type.sql
2+
-- Adds explicit type for notifications.
3+
4+
alter table notifications
5+
add column if not exists notification_type text not null default 'general';
6+
7+
update notifications
8+
set notification_type = 'general'
9+
where notification_type is null
10+
or trim(notification_type) = '';
11+
12+
alter table notifications
13+
drop constraint if exists notifications_notification_type_check;
14+
15+
alter table notifications
16+
add constraint notifications_notification_type_check
17+
check (notification_type in ('security', 'general', 'updates'));

api/src/models/notification.model.js

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,30 @@
11
import { query } from "../config/db.js";
22

3-
export async function createNotification({ messageHtml, messageText, createdBy }) {
3+
export async function createNotification({
4+
messageHtml,
5+
messageText,
6+
notificationType = "general",
7+
createdBy,
8+
}) {
49
const { rows } = await query(
510
`
611
INSERT INTO notifications (
712
message_html,
813
message_text,
14+
notification_type,
915
created_by
1016
)
11-
VALUES ($1, $2, $3)
12-
RETURNING id, message_html, message_text, is_active, created_by, created_at
17+
VALUES ($1, $2, $3, $4)
18+
RETURNING
19+
id,
20+
message_html,
21+
message_text,
22+
notification_type,
23+
is_active,
24+
created_by,
25+
created_at
1326
`,
14-
[messageHtml, messageText, createdBy || null]
27+
[messageHtml, messageText, notificationType, createdBy || null]
1528
);
1629
return rows[0] || null;
1730
}
@@ -24,6 +37,7 @@ export async function listNotificationHistory(limit = 100) {
2437
n.id,
2538
n.message_html,
2639
n.message_text,
40+
n.notification_type,
2741
n.is_active,
2842
n.created_by,
2943
n.created_at,
@@ -46,6 +60,7 @@ export async function listActiveNotificationsForUser(userId, limit = 20) {
4660
n.id,
4761
n.message_html,
4862
n.message_text,
63+
n.notification_type,
4964
n.created_at
5065
FROM notifications n
5166
LEFT JOIN user_notification_dismissals d
@@ -83,6 +98,7 @@ export async function listPendingWeeklyNotificationsForUser(userId, limit = 100)
8398
n.id,
8499
n.message_html,
85100
n.message_text,
101+
n.notification_type,
86102
n.created_at
87103
FROM notifications n
88104
LEFT JOIN user_notification_dismissals d

web/admin.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,14 @@ <h2>Notifications</h2>
399399
<button class="btn btn--link" type="button" data-notification-cmd="insertOrderedList">1. List</button>
400400
<button class="btn btn--link" type="button" data-notification-cmd="createLink">Link</button>
401401
<button class="btn btn--link" type="button" data-notification-cmd="removeFormat">Clear</button>
402+
<label class="admin-notification-toolbar-type">
403+
<span>Type</span>
404+
<select id="notificationTypeInput">
405+
<option value="general">general</option>
406+
<option value="updates">updates</option>
407+
<option value="security">security</option>
408+
</select>
409+
</label>
402410
</div>
403411
<div
404412
id="notificationEditor"

web/scripts/admin.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ const els = {
5959
adminAchievementsList: document.getElementById("adminAchievementsList"),
6060
achievementStatus: document.getElementById("achievementStatus"),
6161
notificationEditor: document.getElementById("notificationEditor"),
62+
notificationTypeInput: document.getElementById("notificationTypeInput"),
6263
publishNotificationBtn: document.getElementById("publishNotificationBtn"),
6364
notificationHistoryList: document.getElementById("notificationHistoryList"),
6465
notificationAdminStatus: document.getElementById("notificationAdminStatus"),
@@ -659,9 +660,10 @@ function renderNotificationHistory() {
659660
const content = html || fallback;
660661
const creator = escapeHtml(String(item.created_by_username || "admin"));
661662
const createdAt = escapeHtml(formatDateTime(item.created_at));
663+
const type = escapeHtml(String(item.notification_type || "general"));
662664
return `
663665
<article class="admin-notification-item">
664-
<div class="admin-notification-meta">${createdAt} • by ${creator}</div>
666+
<div class="admin-notification-meta">${createdAt}${type}by ${creator}</div>
665667
<p>${content}</p>
666668
</article>
667669
`;
@@ -684,20 +686,28 @@ async function loadNotificationHistory() {
684686
async function publishNotificationFromEditor() {
685687
const html = String(els.notificationEditor?.innerHTML || "").trim();
686688
const text = extractNotificationTextFromHtml(html);
689+
const notificationType = String(els.notificationTypeInput?.value || "general").trim().toLowerCase();
687690
if (!text) {
688691
setStatus(els.notificationAdminStatus, "Notification text is required.", "error");
689692
return;
690693
}
694+
if (!["security", "general", "updates"].includes(notificationType)) {
695+
setStatus(els.notificationAdminStatus, "Notification type is invalid.", "error");
696+
return;
697+
}
691698

692699
if (els.publishNotificationBtn) {
693700
els.publishNotificationBtn.disabled = true;
694701
}
695702
setStatus(els.notificationAdminStatus, "Publishing notification...");
696703
try {
697-
await api.admin.createNotification({ messageHtml: html });
704+
await api.admin.createNotification({ messageHtml: html, notificationType });
698705
if (els.notificationEditor) {
699706
els.notificationEditor.innerHTML = "";
700707
}
708+
if (els.notificationTypeInput) {
709+
els.notificationTypeInput.value = "general";
710+
}
701711
await loadNotificationHistory();
702712
setStatus(els.notificationAdminStatus, "Notification published.", "ok");
703713
} catch (err) {

web/scripts/default.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -538,10 +538,13 @@ function showNextNotificationToast() {
538538
}
539539
notificationToastShowing = true;
540540
notificationToastCurrentId = String(next.id || "");
541+
const notificationType = String(
542+
next?.notification_type || next?.type || next?.kind || next?.category || "general"
543+
)
544+
.toLowerCase()
545+
.trim();
541546
const isSecurity =
542-
String(next?.type || "").toLowerCase() === "security" ||
543-
String(next?.kind || "").toLowerCase() === "security" ||
544-
String(next?.category || "").toLowerCase() === "security";
547+
notificationType === "security";
545548

546549
const host = ensureNotificationUiHost();
547550
const toast = document.createElement("article");
@@ -560,7 +563,11 @@ function showNextNotificationToast() {
560563
body.className = "notification-toast-body";
561564
const title = document.createElement("p");
562565
title.className = "notification-toast-title";
563-
title.textContent = isSecurity ? "Security Alert" : "Notification";
566+
if (notificationType === "updates") {
567+
title.textContent = "Update";
568+
} else {
569+
title.textContent = isSecurity ? "Security Alert" : "Notification";
570+
}
564571
const textWrap = document.createElement("div");
565572
textWrap.className = "notification-toast-text";
566573
textWrap.innerHTML = String(next.message_html || next.message_text || "");

web/styles/admin.css

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,22 @@
368368
min-width: auto;
369369
}
370370

371+
.admin-notification-toolbar-type {
372+
margin-left: auto;
373+
display: inline-flex;
374+
align-items: center;
375+
gap: 0.45rem;
376+
}
377+
378+
.admin-notification-toolbar-type span {
379+
font-size: 0.86rem;
380+
color: var(--muted);
381+
}
382+
383+
.admin-notification-toolbar-type select {
384+
min-width: 130px;
385+
}
386+
371387
.admin-notification-input {
372388
min-height: 150px;
373389
border: 1px solid var(--border);

0 commit comments

Comments
 (0)