Topbar unread drawer (#28)

Adds an Unread dropdown to the topbar utilities (before notifications), per docs/superpowers/plans/2026-07-17-topbar-unread-drawer.md.

- New partial `templates/partials/header/unread-drawer.tpl`: toggle with live `component="unread/count"` badge (core socket updates), server-rendered empty/footer rows.
- `public/client.js`: on dropdown open, fetches `GET /api/unread` and injects up to 10 topic rows (titles/categories via textContent only); fetch failure falls back to the empty state.
- Mobile burger drawer gets an Unread row with the same badge.
- SCSS: unread dropdown joins the shared dropdown width rule; flex row styles for topic items.

Deviations from the plan's example code, found by live verification against the dev forum:
- Plan's `api.get('/unread')` hits the v3 write API and 404s → replaced with a fetch of the `/api/unread` read route.
- NodeBB 4 has no global `window.require`, so runtime translation of the static `[[unread:...]]` strings never ran → those strings are now server-rendered in the template (also removes the plan's translate-over-innerHTML step, which would have re-parsed untrusted titles as HTML).

Verified: full plain-node test suite passes; Playwright against the dev container passed all checks (badge count, item ordering before notifications, dropdown topic links + navigation, /unread footer link, mobile drawer badge at 390px).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed-on: #28
Co-authored-by: vickydotbat <vickydotbat@tutamail.com>
Co-committed-by: vickydotbat <vickydotbat@tutamail.com>
This commit was merged in pull request #28.
This commit is contained in:
2026-07-17 16:43:12 +00:00
committed by archvillainette
parent 1b99c93193
commit 004be9b038
8 changed files with 574 additions and 1 deletions
+69
View File
@@ -159,6 +159,55 @@
});
}
function renderUnreadMenu(menuEl, topics) {
if (!menuEl) {
return;
}
const relativePath = (window.config && window.config.relative_path) || '';
// remove the loading row and any previously injected topic rows;
// the empty-state and footer/divider rows are server-rendered and stay put
const loading = menuEl.querySelector('[data-wg-unread-loading]');
if (loading) {
loading.remove();
}
toArray(menuEl.querySelectorAll('.wg-unread-topic')).forEach((li) => {
li.remove();
});
const emptyEl = menuEl.querySelector('[data-wg-unread-empty]');
const footerEl = menuEl.querySelector('[data-wg-unread-footer]');
const list = (topics || []).slice(0, 10);
if (emptyEl) {
emptyEl.classList.toggle('hidden', list.length > 0);
}
list.forEach((topic) => {
const link = document.createElement('a');
link.className = 'dropdown-item wg-unread-item text-truncate';
link.href = `${relativePath}/topic/${topic.slug}`;
link.textContent = topic.title;
if (topic.category && topic.category.name) {
const category = document.createElement('small');
category.className = 'wg-unread-item__category';
category.textContent = topic.category.name;
link.appendChild(category);
}
const li = document.createElement('li');
li.className = 'wg-unread-topic';
li.appendChild(link);
if (footerEl) {
menuEl.insertBefore(li, footerEl);
} else {
menuEl.appendChild(li);
}
});
}
function getWestgateTopbar() {
if (!document || typeof document.querySelector !== 'function') {
return null;
@@ -225,11 +274,31 @@
.on('shown.bs.dropdown.westgateTopbar', '.wg-topbar [component="sidebar/search"]', function () {
$(this).find('[component="search/fields"] input[name="query"]').trigger('focus');
});
$(document)
.off('show.bs.dropdown.westgateUnread')
.on('show.bs.dropdown.westgateUnread', '.wg-topbar [component="unread"]', function () {
const menuEl = this.querySelector('[data-wg-unread-menu]');
const relativePath = (window.config && window.config.relative_path) || '';
fetch(relativePath + '/api/unread', {
headers: { accept: 'application/json' },
}).then(function (res) {
if (!res.ok) {
throw new Error('unread fetch failed: ' + res.status);
}
return res.json();
}).then(function (data) {
renderUnreadMenu(menuEl, data && data.topics);
}).catch(function () {
renderUnreadMenu(menuEl, []);
});
});
}
window.westgateTheme = window.westgateTheme || {};
window.westgateTheme.wrapWikiTables = wrapWestgateWikiTables;
window.westgateTheme.initTopbar = initWestgateTopbar;
window.westgateTheme.renderUnreadMenu = renderUnreadMenu;
$(document).ready(function () {
wrapWestgateWikiTables(document);