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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user