openvibers/openvibe.community Commits
Empty states: the board, Pulse, Search, submissions and paste grids use the kit (#391)
Changes
@@@ -61,7 +61,7 @@ ${groups.length ? groups.map((g) => `<section class="board-group" id="g-${esc(g.<h2 class="board-group-h" id="gh-${esc(g.slug || 'more')}">${esc(g.name)}</h2>${g.description ? `<p class="muted small board-group-p">${esc(g.description)}</p>` : ''}${boardTable(g.spaces, g.name)}--</section>`).join('') : '<p class="empty">No forums yet.</p>'}++</section>`).join('') : kit.empty({ icon: 'chat', title: 'No forums yet', text: 'No forum has been started here yet.', action: canModerate ? { href: '/forum/new', label: 'New forum', icon: 'plus' } : { href: '/pulse', label: 'See what is happening', icon: 'live' } })}${user ? '' : '<p class="muted small">Sign in with your OpenVibe account to post.</p>'}`;return renderPage({title: 'Forum',@@@ -70,6 +70,7 @@ ${user ? '' : '<p class="muted small">Sign in with your OpenVibe account to postactive: 'spaces',feeds: [{ title: 'OpenVibe.Community — latest threads', href: '/forum/feed.xml' }],jsonLd: [ld.breadcrumbLd([{ name: 'Home', url: '/' }, { name: 'Forum', url: '/forum' }])],++ styles: ['kit.css'],body,});}@@@ -160,7 +161,7 @@ ${tags}${statuses}<section data-results>${threads.length ? `<table class="board-table topic-table"><caption class="sr-only">Topics in ${esc(space.name)}</caption><thead><tr><th scope="col" class="topic-icon"><span class="sr-only">Kind</span></th><th scope="col">Topic</th><th scope="col" class="num">Replies</th><th scope="col" class="num">Views</th><th scope="col">Last post</th></tr></thead>-- <tbody>${threads.map((t) => topicRow(t, space)).join('')}</tbody></table>` : `<p class="empty">${filtered ? `Nothing here yet. <a href="${esc(href({ category: null, status: null }))}">See every topic</a>.` : viewer.can_start !== false ? `No topics yet — <a href="/forum/${esc(space.slug)}/new">start the first one</a>.` : 'Nothing here yet.'}</p>`}++ <tbody>${threads.map((t) => topicRow(t, space)).join('')}</tbody></table>` : filtered ? kit.empty({ icon: 'search', title: 'Nothing here yet', text: 'No topic matches these filters.', action: { href: href({ category: null, status: null }), label: 'See every topic', icon: 'grid' } }) : viewer.can_start !== false ? kit.empty({ icon: 'chat', title: 'No topics yet', text: 'This forum has no topic yet.', action: { href: `/forum/${esc(space.slug)}/new`, label: space.thread_kind === 'request' ? 'Make the first request' : 'Start the first topic', icon: 'plus' } }) : kit.empty({ icon: 'chat', title: 'Nothing here yet', text: 'No topic has been started here yet.', action: { href: '/forum', label: 'See the other forums', icon: 'grid' } })}${pagerHtml}</section>${viewer.can_moderate ? settingsForm(space, groups) + moderatorsBox(space, moderators, { me, error: modError }) : ''}@@@ -173,6 +174,7 @@ ${viewer.can_moderate ? settingsForm(space, groups) + moderatorsBox(space, moderactive: 'spaces',feeds: feedable ? [{ title: `OpenVibe.Community — ${space.name}`, href: `/forum/${space.slug}/feed.xml` }] : [],jsonLd: [ld.breadcrumbLd([{ name: 'Home', url: '/' }, { name: 'Forum', url: '/forum' }, { name: space.name, url: `/forum/${space.slug}` }])],++ styles: ['kit.css'],body,});}@@@ -449,7 +449,7 @@ function moderationLogPage({ space, entries }) {</header>${entries.length ? `<table class="board-table mod-log"><caption class="sr-only">Moderation actions in ${esc(space.name)}</caption><thead><tr><th scope="col">When</th><th scope="col">What</th><th scope="col">Action</th><th scope="col">Rule</th><th scope="col">Reason</th><th scope="col">By</th></tr></thead>-- <tbody>${rows}</tbody></table>` : '<p class="empty">No moderation actions in this forum yet.</p>'}++ <tbody>${rows}</tbody></table>` : kit.empty({ icon: 'shield', title: 'No moderation actions yet', text: 'Nothing has been moderated in this forum yet.' })}<footer class="space-foot"><a class="small" href="/forum/${esc(space.slug)}">Back to ${esc(space.name)}</a>${space.visibility === 'public' && !space.members_only ? `<span class="sep"> · </span><a class="small" href="/forum/${esc(space.slug)}/feed.xml">RSS</a>` : ''}</footer>`;return renderPage({title: `Moderation log · ${space.name}`,@@@ -459,6 +459,7 @@ ${entries.length ? `<table class="board-table mod-log"><caption class="sr-only">active: 'spaces',footerVariant: 'compact',pageCss: ['modlog.css'],++ styles: ['kit.css'],jsonLd: [ld.breadcrumbLd([{ name: 'Home', url: '/' }, { name: 'Forum', url: '/forum' }, { name: space.name, url: `/forum/${space.slug}` }, { name: 'Moderation log', url: `/forum/${space.slug}/log` }])],body,});@@@ -135,8 +135,11 @@ function pasteCard(p, { eager = false } = {}) {</div></article>`;}--function cardGrid(pastes, empty = 'Nothing here yet.') {-- if (!pastes || !pastes.length) return `<p class="empty">${esc(empty)}</p>`;++function cardGrid(pastes, empty = null) {++ if (!pastes || !pastes.length) {++ const e = empty || { title: 'Nothing here yet', text: 'No pastes here yet.' };++ return kit.empty({ icon: e.icon || 'grid', title: e.title, text: e.text, action: e.action || null });++ }// The first picture in the grid is the one above the fold: it loads at high priority, the rest stay lazy.const firstShot = pastes.findIndex((p) => p.type === 'screenshot' && p.screenshot_url && !p.locked);return `<div class="grid">${pastes.map((p, i) => pasteCard(p, { eager: i === firstShot })).join('\n')}</div>`;@@@ -414,7 +417,7 @@ function browsePage(result, languages) {${(q || lang || sort !== 'new' || from !== 'people') ? `<a class="btn btn-ghost" href="/pastes">Clear</a>` : ''}</form><section id="results" data-results>-- ${cardGrid(pastes, q ? 'No pastes match that search.' : 'No pastes yet.')}++ ${cardGrid(pastes, q ? { title: 'No matches', text: 'No pastes match that search.', action: { href: '/pastes', label: 'See all pastes', icon: 'grid' } } : { title: 'No pastes yet', text: 'No paste has been shared here yet.', action: { href: '/new', label: 'Make a paste', icon: 'plus' } })}${pager}</section>`;return renderPage({@@@ -424,6 +427,7 @@ function browsePage(result, languages) {agentLayer: true, // the public page has a Markdown twin (server/agent-pages.js)robots: q ? 'noindex,follow' : 'index,follow',active: 'pastes',++ styles: ['kit.css'],jsonLd: [ld.breadcrumbLd([{ name: 'Home', url: '/' }, { name: 'Pastes', url: '/pastes' }])],body,});@@@ -555,7 +559,7 @@ ${p.burn_after_read ? '' : `<section class="section"></section>`}${!locked && related && related.length ? `<section class="section" id="related"><div class="section-head"><h2>More like this</h2><a class="more" href="${!isShot && p.language && p.language !== 'text' ? `/pastes?lang=${encodeURIComponent(p.language)}` : '/pastes'}">Browse <i class="fa-solid fa-arrow-right" aria-hidden="true"></i></a></div>-- ${cardGrid(related)}++ ${cardGrid(related, { title: 'No related pastes yet', text: 'Related pastes appear here when this one is linked with others.' })}</section>` : ''}<script src="${ovServe.url('ov-share.js')}" defer></script>${locked ? `<script>window.OpenVibeLockerConfig = { network: ${JSON.stringify(config.networkUrl)} };</script>@@@ -753,11 +757,11 @@ function myPage({ user, pastes, total, error }) {const body = `<header class="page-head"><h1>My pastes</h1>-- <p class="muted">${esc(user.display_name || user.username)} · ${num(total || (pastes || []).length)} ${total === 1 ? 'paste' : 'pastes'} · <a href="/new">new paste</a> · <a href="${NETWORK_URL}/my" rel="noopener">account</a></p>++ <p class="muted">${esc(user.display_name || user.username)} · ${num(total || (pastes || []).length)} ${total === 1 ? 'paste' : 'pastes'} · ${kit.button({ href: '/new', label: 'New paste', icon: 'plus', kind: 'quiet', small: true })} · <a href="${NETWORK_URL}/my" rel="noopener">account</a></p></header>${error ? `<p class="alert alert-error" role="alert">${esc(error)}</p>` : ''}<section data-results>-- ${cardGrid(pastes, 'You have not posted anything yet. Your public, unlisted and private pastes all show up here.')}++ ${cardGrid(pastes, { title: 'No pastes yet', text: 'You have not posted anything yet. Your public, unlisted and private pastes all show up here.', action: { href: '/new', label: 'Make a paste', icon: 'plus' } })}</section>`;return renderPage({title: 'My pastes',@@@ -766,6 +770,7 @@ ${error ? `<p class="alert alert-error" role="alert">${esc(error)}</p>` : ''}robots: 'noindex,nofollow',active: 'my',footerVariant: 'compact',++ styles: ['kit.css'],body,});}@@@ -11,6 +11,7 @@ const { renderPage } = require('./layout');const { escapeHtml: esc } = require('./highlight');const { timeTag } = require('./pages');const { who } = require('./forum');++const kit = require('openvibe-shared/kit');const FILTERS = [['', 'Everything'], ['user', 'People'], ['ai', 'AI'], ['system', 'System']];const SERVICE_NAMES = { community: 'Community', live: 'Live', media: 'Media', network: 'Network', tools: 'Tools', games: 'Games', wiki: 'Wiki', blog: 'Blog', reviews: 'Reviews' };@@@ -52,7 +53,7 @@ function pulsePage({ items, origin = '', after = null, nextCursor = null, origin<nav class="tabs sort-tabs" aria-label="Filter by origin">${tabs}</nav><section data-results${after ? '' : ' data-pulse-live'}><p class="pulse-new" hidden><button type="button" class="btn btn-sm" data-pulse-show></button></p>-- ${items.length ? `<ol class="thread-list pulse-list">${items.map(itemHtml).join('')}</ol>` : '<p class="empty">Nothing here yet.</p>'}++ ${items.length ? `<ol class="thread-list pulse-list">${items.map(itemHtml).join('')}</ol>` : kit.empty({ icon: 'live', title: 'Nothing here yet', text: origin ? 'Nothing from this part of the network has arrived yet.' : 'New public clips, threads, pastes and posts land here as they happen.', action: origin ? { href: '/pulse', label: 'Everything', icon: 'live' } : { href: '/forum', label: 'Visit the forum', icon: 'chat' } })}${after || nextCursor ? `<nav class="pager" aria-label="Pages">${after ? `<a href="${esc(href(origin))}"><i class="fa-solid fa-chevron-left" aria-hidden="true"></i> Latest</a>` : '<span></span>'}<span></span>${nextCursor ? `<a rel="next" href="${esc(href(origin, nextCursor))}">Older <i class="fa-solid fa-chevron-right" aria-hidden="true"></i></a>` : '<span></span>'}</nav>` : ''}</section>`;return renderPage({@@@ -64,6 +65,7 @@ function pulsePage({ items, origin = '', after = null, nextCursor = null, originactive: 'pulse',feeds: [{ title: 'OpenVibe.Community — latest threads', href: '/forum/feed.xml' }],jsonLd: [ld.breadcrumbLd([{ name: 'Home', url: '/' }, { name: 'Pulse', url: '/pulse' }])],++ styles: ['kit.css'],body,});}@@@ -6,6 +6,7 @@*/const { renderPage } = require('./layout');const { escapeHtml: esc } = require('./highlight');++const kit = require('openvibe-shared/kit');// Search escapes the text and adds only <mark>; keep exactly that, whatever arrives.const snippet = (html) => esc(String(html || '')).replace(/<(\/?)mark>/g, '<$1mark>');@@@ -28,9 +29,9 @@ function searchPage({ q = '', type = '', results = [], nextCursor = null, unavaiconst opt = (v, label) => `<option value="${v}"${type === v ? ' selected' : ''}>${label}</option>`;const more = nextCursor ? `<nav class="pager"><a class="btn" href="/search?${new URLSearchParams({ q, ...(type ? { type } : {}), cursor: nextCursor })}">More results</a></nav>` : '';let section;-- if (unavailable) section = `<p class="empty">Search is not answering right now. <a href="/pastes?q=${encodeURIComponent(q)}">Filter pastes for “${esc(q)}”</a> instead, or try again in a minute.</p>`;-- else if (!q) section = '<p class="empty">Search every public thread and paste on OpenVibe.Community.</p>';-- else if (!results.length) section = `<p class="empty">Nothing public matches “${esc(q)}”.</p>`;++ if (unavailable) section = kit.empty({ icon: 'search', title: 'Search is not answering', text: 'OpenVibe.Search is not answering right now. Try again in a minute, or filter pastes instead.', action: q ? { href: `/pastes?q=${encodeURIComponent(q)}`, label: 'Filter pastes', icon: 'search' } : { href: '/pastes', label: 'Browse pastes', icon: 'grid' } });++ else if (!q) section = kit.empty({ icon: 'search', title: 'Search Community', text: 'Search every public thread and paste on OpenVibe.Community.', action: { href: '/forum', label: 'Browse the forum', icon: 'chat' } });++ else if (!results.length) section = kit.empty({ icon: 'search', title: 'Nothing matches', text: `Nothing public matches “${q}”.`, action: { href: `https://search.openvibe.network/?q=${encodeURIComponent(q)}`, label: 'Search the whole network', icon: 'search' } });else section = `<ol class="search-results">${results.map(resultItem).join('\n')}</ol>${more}`;const body = `<header class="page-head">@@@ -49,6 +50,7 @@ function searchPage({ q = '', type = '', results = [], nextCursor = null, unavaicanonicalPath: '/search',robots: 'noindex,follow',active: 'search',++ styles: ['kit.css'],body,});}@@@ -11,6 +11,7 @@ const { escapeHtml: esc } = require('./highlight');const { renderMarkdown, markdownToText } = require('./markdown');const { timeTag } = require('./pages');const { who } = require('./forum');++const kit = require('openvibe-shared/kit');const KIND_LABELS = { clip: 'Clip', art: 'Art', idea: 'Idea', report: 'Report' };const STATUS_LABELS = { pending: 'Pending review', accepted: 'Accepted', rejected: 'Not accepted', withdrawn: 'Withdrawn' };@@@ -37,8 +38,8 @@ function itemHtml(s) {</li>`;}--function listBlock(items, { empty, prev, next }) {-- return `${items.length ? `<ol class="thread-list pulse-list">${items.map(itemHtml).join('')}</ol>` : `<p class="empty">${esc(empty)}</p>`}++function listBlock(items, { empty, action = null, prev, next }) {++ return `${items.length ? `<ol class="thread-list pulse-list">${items.map(itemHtml).join('')}</ol>` : kit.empty({ icon: 'mail', title: empty.title, text: empty.text, action })}${prev || next ? `<nav class="pager" aria-label="Pages">${prev ? `<a href="${esc(prev)}"><i class="fa-solid fa-chevron-left" aria-hidden="true"></i> Newest</a>` : '<span></span>'}<span></span>${next ? `<a rel="next" href="${esc(next)}">Older <i class="fa-solid fa-chevron-right" aria-hidden="true"></i></a>` : '<span></span>'}</nav>` : ''}`;}@@@ -72,7 +73,11 @@ function submissionsPage({ items, kind = '', mine = false, after = null, nextCur</header><nav class="tabs sort-tabs" aria-label="Filter by kind">${tabs}</nav><section data-results>-- ${listBlock(items, { empty: mine ? 'You have not submitted anything yet.' : 'Nothing accepted yet.', prev: after ? listHref({ kind, mine }) : null, next: nextCursor ? listHref({ kind, mine, after: nextCursor }) : null })}++ ${listBlock(items, {++ empty: mine ? { title: 'No submissions yet', text: 'You have not submitted anything yet.' } : { title: 'Nothing accepted yet', text: 'No submission has been accepted here yet.' },++ action: user ? { href: '#submit', label: 'Submit something', icon: 'plus' } : { href: '/auth/login?next=%2Fsubmissions', label: 'Sign in to submit', icon: 'plus' },++ prev: after ? listHref({ kind, mine }) : null, next: nextCursor ? listHref({ kind, mine, after: nextCursor }) : null,++ })}</section>${submitForm({ user, values, error })}`;return renderPage({@@@ -81,6 +86,7 @@ ${submitForm({ user, values, error })}`;canonicalPath: listHref({ kind, after }),robots: mine || after ? 'noindex,follow' : 'index,follow',active: 'submissions',++ styles: ['kit.css'],jsonLd: [ld.breadcrumbLd([{ name: 'Home', url: '/' }, { name: 'Submissions', url: '/submissions' }])],body,});@@@ -141,9 +147,9 @@ function reviewQueuePage({ items, after = null, nextCursor = null }) {<p class="muted">Submissions waiting for a decision, newest first. Open one to accept or reject it.</p></header><section data-results>-- ${listBlock(items, { empty: 'Nothing is waiting for review.', prev: after ? '/submissions/review' : null, next: nextCursor ? listHref({ after: nextCursor }, '/submissions/review') : null })}++ ${listBlock(items, { empty: { title: 'Nothing to review', text: 'Nothing is waiting for review.' }, action: { href: '/submissions', label: 'See submissions', icon: 'grid' }, prev: after ? '/submissions/review' : null, next: nextCursor ? listHref({ after: nextCursor }, '/submissions/review') : null })}</section>`;-- return renderPage({ title: 'Review queue', description: 'Submissions waiting for review.', canonicalPath: '/submissions/review', robots: 'noindex,nofollow', active: 'submissions', footerVariant: 'compact', body });++ return renderPage({ title: 'Review queue', description: 'Submissions waiting for review.', canonicalPath: '/submissions/review', robots: 'noindex,nofollow', active: 'submissions', footerVariant: 'compact', styles: ['kit.css'], body });}module.exports = { submissionsPage, submissionPage, reviewQueuePage, KIND_LABELS, STATUS_LABELS };nnew file mode 100644@@@ -0,0 +1,82 @@++'use strict';++/**++ * The plain-page sweep (owner ask 81): the pages that used to print a bare "No X yet" draw the shared kit's++ * empty state instead, and the one next action is a kit button. Each page that can show one loads kit.css.++ */++const assert = require('assert');++const { pulsePage } = require('../server/render/pulse');++const { boardIndexPage, forumSpacePage } = require('../server/render/board');++const { searchPage } = require('../server/render/search');++const { submissionsPage, reviewQueuePage } = require('../server/render/submissions');++const { cardGrid } = require('../server/render/pages');++++const has = (html, s, msg) => assert.ok(html.includes(s), msg || `expected to find: ${s}`);++const emptyState = (html, title) => {++ has(html, 'class="ovk-empty"');++ has(html, title);++ has(html, 'kit.css');++};++++const space = { slug: 'empty', name: 'Empty', description: '', visibility: 'public', members_only: 0, style: 'forum', thread_kind: 'topic', statuses: [] };++++const checks = [++ ['the board index with no forum', () => {++ const html = boardIndexPage({ groups: [] });++ emptyState(html, 'No forums yet');++ has(html, 'See what is happening');++ has(html, 'ovk-btn');++ }],++ ['a forum with no topic', () => {++ const html = forumSpacePage({ space, threads: [], page: 1, pages: 1, total: 0, viewer: { can_start: true } });++ emptyState(html, 'No topics yet');++ has(html, 'Start the first topic');++ }],++ ['a filtered forum with no match', () => {++ const html = forumSpacePage({ space, threads: [], page: 1, pages: 1, total: 0, category: 'other', viewer: { can_start: true } });++ emptyState(html, 'Nothing here yet');++ has(html, 'See every topic');++ }],++ ['Pulse with nothing', () => {++ const html = pulsePage({ items: [] });++ emptyState(html, 'Nothing here yet');++ has(html, 'Visit the forum');++ }],++ ['Search before a query', () => {++ const html = searchPage({ q: '', results: [] });++ emptyState(html, 'Search Community');++ has(html, 'Browse the forum');++ }],++ ['Search with no match', () => {++ const html = searchPage({ q: 'zzz', results: [] });++ emptyState(html, 'Nothing matches');++ has(html, 'Search the whole network');++ }],++ ['Search not answering', () => {++ const html = searchPage({ q: 'zzz', results: [], unavailable: true });++ emptyState(html, 'Search is not answering');++ has(html, 'Filter pastes');++ }],++ ['Submissions with nothing accepted', () => {++ const html = submissionsPage({ items: [] });++ emptyState(html, 'Nothing accepted yet');++ has(html, 'Sign in to submit');++ }],++ ['the review queue with nothing waiting', () => {++ const html = reviewQueuePage({ items: [] });++ emptyState(html, 'Nothing to review');++ has(html, 'See submissions');++ }],++ ['a paste grid with nothing', () => {++ const html = cardGrid([], { title: 'No pastes yet', text: 'No paste has been shared here yet.', action: { href: '/new', label: 'Make a paste', icon: 'plus' } });++ has(html, 'class="ovk-empty"');++ has(html, 'No pastes yet');++ has(html, 'Make a paste');++ }],++];++++let failed = 0;++for (const [name, run] of checks) {++ try { run(); console.log(`ok - ${name}`); }++ catch (e) { failed += 1; console.error(`not ok - ${name}: ${e.message}`); }++}++process.exitCode = failed ? 1 : 0;@@@ -182,7 +182,7 @@ const { boot, check, done } = require('./helpers/app');assert.ok(people.includes('<meta name="robots" content="index,follow">'));const empty = pulsePage({ items: [], origin: 'system', origins: ['user'] });assert.ok(empty.includes('aria-current="page">System</a>'), 'the filter asked for keeps its tab');-- assert.ok(empty.includes('Nothing here yet.') && empty.includes('<meta name="robots" content="noindex,follow">'));++ assert.ok(empty.includes('Nothing here yet') && empty.includes('class="ovk-empty"') && empty.includes('<meta name="robots" content="noindex,follow">'));assert.ok(pulsePage({ items: [item] }).includes('href="/pulse?origin=system"'), 'without the origins list every filter shows');const r = await t.get('/pulse');assert.ok(r.text.includes('href="/pulse?origin=system"'), 'system items exist here, so the tab shows');
@@ -61,7 +61,7 @@ ${groups.length ? groups.map((g) => `<section class="board-group" id="g-${esc(g.
@@ -61,7 +61,7 @@ ${groups.length ? groups.map((g) => `<section class="board-group" id="g-${esc(g.
<h2 class="board-group-h" id="gh-${esc(g.slug || 'more')}">${esc(g.name)}</h2>
<h2 class="board-group-h" id="gh-${esc(g.slug || 'more')}">${esc(g.name)}</h2>
${g.description ? `<p class="muted small board-group-p">${esc(g.description)}</p>` : ''}
${g.description ? `<p class="muted small board-group-p">${esc(g.description)}</p>` : ''}
${boardTable(g.spaces, g.name)}
${boardTable(g.spaces, g.name)}
-</section>`).join('') : '<p class="empty">No forums yet.</p>'}
+</section>`).join('') : kit.empty({ icon: 'chat', title: 'No forums yet', text: 'No forum has been started here yet.', action: canModerate ? { href: '/forum/new', label: 'New forum', icon: 'plus' } : { href: '/pulse', label: 'See what is happening', icon: 'live' } })}
${user ? '' : '<p class="muted small">Sign in with your OpenVibe account to post.</p>'}`;
${user ? '' : '<p class="muted small">Sign in with your OpenVibe account to post.</p>'}`;
return renderPage({
return renderPage({
title: 'Forum',
title: 'Forum',
@@ -70,6 +70,7 @@ ${user ? '' : '<p class="muted small">Sign in with your OpenVibe account to post
@@ -70,6 +70,7 @@ ${user ? '' : '<p class="muted small">Sign in with your OpenVibe account to post
active: 'spaces',
active: 'spaces',
feeds: [{ title: 'OpenVibe.Community — latest threads', href: '/forum/feed.xml' }],
feeds: [{ title: 'OpenVibe.Community — latest threads', href: '/forum/feed.xml' }],
jsonLd: [ld.breadcrumbLd([{ name: 'Home', url: '/' }, { name: 'Forum', url: '/forum' }])],
jsonLd: [ld.breadcrumbLd([{ name: 'Home', url: '/' }, { name: 'Forum', url: '/forum' }])],
+ styles: ['kit.css'],
body,
body,
});
});
}
}
@@ -160,7 +161,7 @@ ${tags}${statuses}
@@ -160,7 +161,7 @@ ${tags}${statuses}
<section data-results>
<section data-results>
${threads.length ? `<table class="board-table topic-table"><caption class="sr-only">Topics in ${esc(space.name)}</caption>
${threads.length ? `<table class="board-table topic-table"><caption class="sr-only">Topics in ${esc(space.name)}</caption>
<thead><tr><th scope="col" class="topic-icon"><span class="sr-only">Kind</span></th><th scope="col">Topic</th><th scope="col" class="num">Replies</th><th scope="col" class="num">Views</th><th scope="col">Last post</th></tr></thead>
<thead><tr><th scope="col" class="topic-icon"><span class="sr-only">Kind</span></th><th scope="col">Topic</th><th scope="col" class="num">Replies</th><th scope="col" class="num">Views</th><th scope="col">Last post</th></tr></thead>
- <tbody>${threads.map((t) => topicRow(t, space)).join('')}</tbody></table>` : `<p class="empty">${filtered ? `Nothing here yet. <a href="${esc(href({ category: null, status: null }))}">See every topic</a>.` : viewer.can_start !== false ? `No topics yet — <a href="/forum/${esc(space.slug)}/new">start the first one</a>.` : 'Nothing here yet.'}</p>`}
+ <tbody>${threads.map((t) => topicRow(t, space)).join('')}</tbody></table>` : filtered ? kit.empty({ icon: 'search', title: 'Nothing here yet', text: 'No topic matches these filters.', action: { href: href({ category: null, status: null }), label: 'See every topic', icon: 'grid' } }) : viewer.can_start !== false ? kit.empty({ icon: 'chat', title: 'No topics yet', text: 'This forum has no topic yet.', action: { href: `/forum/${esc(space.slug)}/new`, label: space.thread_kind === 'request' ? 'Make the first request' : 'Start the first topic', icon: 'plus' } }) : kit.empty({ icon: 'chat', title: 'Nothing here yet', text: 'No topic has been started here yet.', action: { href: '/forum', label: 'See the other forums', icon: 'grid' } })}
${pagerHtml}
${pagerHtml}
</section>
</section>
${viewer.can_moderate ? settingsForm(space, groups) + moderatorsBox(space, moderators, { me, error: modError }) : ''}
${viewer.can_moderate ? settingsForm(space, groups) + moderatorsBox(space, moderators, { me, error: modError }) : ''}
@@ -173,6 +174,7 @@ ${viewer.can_moderate ? settingsForm(space, groups) + moderatorsBox(space, moder
@@ -173,6 +174,7 @@ ${viewer.can_moderate ? settingsForm(space, groups) + moderatorsBox(space, moder
active: 'spaces',
active: 'spaces',
feeds: feedable ? [{ title: `OpenVibe.Community — ${space.name}`, href: `/forum/${space.slug}/feed.xml` }] : [],
feeds: feedable ? [{ title: `OpenVibe.Community — ${space.name}`, href: `/forum/${space.slug}/feed.xml` }] : [],
jsonLd: [ld.breadcrumbLd([{ name: 'Home', url: '/' }, { name: 'Forum', url: '/forum' }, { name: space.name, url: `/forum/${space.slug}` }])],
jsonLd: [ld.breadcrumbLd([{ name: 'Home', url: '/' }, { name: 'Forum', url: '/forum' }, { name: space.name, url: `/forum/${space.slug}` }])],
+ styles: ['kit.css'],
body,
body,
});
});
}
}
@@ -449,7 +449,7 @@ function moderationLogPage({ space, entries }) {
@@ -449,7 +449,7 @@ function moderationLogPage({ space, entries }) {
</header>
</header>
${entries.length ? `<table class="board-table mod-log"><caption class="sr-only">Moderation actions in ${esc(space.name)}</caption>
${entries.length ? `<table class="board-table mod-log"><caption class="sr-only">Moderation actions in ${esc(space.name)}</caption>
<thead><tr><th scope="col">When</th><th scope="col">What</th><th scope="col">Action</th><th scope="col">Rule</th><th scope="col">Reason</th><th scope="col">By</th></tr></thead>
<thead><tr><th scope="col">When</th><th scope="col">What</th><th scope="col">Action</th><th scope="col">Rule</th><th scope="col">Reason</th><th scope="col">By</th></tr></thead>
- <tbody>${rows}</tbody></table>` : '<p class="empty">No moderation actions in this forum yet.</p>'}
+ <tbody>${rows}</tbody></table>` : kit.empty({ icon: 'shield', title: 'No moderation actions yet', text: 'Nothing has been moderated in this forum yet.' })}
<footer class="space-foot"><a class="small" href="/forum/${esc(space.slug)}">Back to ${esc(space.name)}</a>${space.visibility === 'public' && !space.members_only ? `<span class="sep"> · </span><a class="small" href="/forum/${esc(space.slug)}/feed.xml">RSS</a>` : ''}</footer>`;
<footer class="space-foot"><a class="small" href="/forum/${esc(space.slug)}">Back to ${esc(space.name)}</a>${space.visibility === 'public' && !space.members_only ? `<span class="sep"> · </span><a class="small" href="/forum/${esc(space.slug)}/feed.xml">RSS</a>` : ''}</footer>`;
return renderPage({
return renderPage({
title: `Moderation log · ${space.name}`,
title: `Moderation log · ${space.name}`,
@@ -459,6 +459,7 @@ ${entries.length ? `<table class="board-table mod-log"><caption class="sr-only">
@@ -459,6 +459,7 @@ ${entries.length ? `<table class="board-table mod-log"><caption class="sr-only">
active: 'spaces',
active: 'spaces',
footerVariant: 'compact',
footerVariant: 'compact',
pageCss: ['modlog.css'],
pageCss: ['modlog.css'],
+ styles: ['kit.css'],
jsonLd: [ld.breadcrumbLd([{ name: 'Home', url: '/' }, { name: 'Forum', url: '/forum' }, { name: space.name, url: `/forum/${space.slug}` }, { name: 'Moderation log', url: `/forum/${space.slug}/log` }])],
jsonLd: [ld.breadcrumbLd([{ name: 'Home', url: '/' }, { name: 'Forum', url: '/forum' }, { name: space.name, url: `/forum/${space.slug}` }, { name: 'Moderation log', url: `/forum/${space.slug}/log` }])],
body,
body,
});
});
@@ -135,8 +135,11 @@ function pasteCard(p, { eager = false } = {}) {
@@ -135,8 +135,11 @@ function pasteCard(p, { eager = false } = {}) {
</div>
</div>
</article>`;
</article>`;
}
}
-function cardGrid(pastes, empty = 'Nothing here yet.') {
- if (!pastes || !pastes.length) return `<p class="empty">${esc(empty)}</p>`;
+function cardGrid(pastes, empty = null) {
+ if (!pastes || !pastes.length) {
+ const e = empty || { title: 'Nothing here yet', text: 'No pastes here yet.' };
+ return kit.empty({ icon: e.icon || 'grid', title: e.title, text: e.text, action: e.action || null });
+ }
// The first picture in the grid is the one above the fold: it loads at high priority, the rest stay lazy.
// The first picture in the grid is the one above the fold: it loads at high priority, the rest stay lazy.
const firstShot = pastes.findIndex((p) => p.type === 'screenshot' && p.screenshot_url && !p.locked);
const firstShot = pastes.findIndex((p) => p.type === 'screenshot' && p.screenshot_url && !p.locked);
return `<div class="grid">${pastes.map((p, i) => pasteCard(p, { eager: i === firstShot })).join('\n')}</div>`;
return `<div class="grid">${pastes.map((p, i) => pasteCard(p, { eager: i === firstShot })).join('\n')}</div>`;
@@ -414,7 +417,7 @@ function browsePage(result, languages) {
@@ -414,7 +417,7 @@ function browsePage(result, languages) {
${(q || lang || sort !== 'new' || from !== 'people') ? `<a class="btn btn-ghost" href="/pastes">Clear</a>` : ''}
${(q || lang || sort !== 'new' || from !== 'people') ? `<a class="btn btn-ghost" href="/pastes">Clear</a>` : ''}
</form>
</form>
<section id="results" data-results>
<section id="results" data-results>
- ${cardGrid(pastes, q ? 'No pastes match that search.' : 'No pastes yet.')}
+ ${cardGrid(pastes, q ? { title: 'No matches', text: 'No pastes match that search.', action: { href: '/pastes', label: 'See all pastes', icon: 'grid' } } : { title: 'No pastes yet', text: 'No paste has been shared here yet.', action: { href: '/new', label: 'Make a paste', icon: 'plus' } })}
${pager}
${pager}
</section>`;
</section>`;
return renderPage({
return renderPage({
@@ -424,6 +427,7 @@ function browsePage(result, languages) {
@@ -424,6 +427,7 @@ function browsePage(result, languages) {
agentLayer: true, // the public page has a Markdown twin (server/agent-pages.js)
agentLayer: true, // the public page has a Markdown twin (server/agent-pages.js)
robots: q ? 'noindex,follow' : 'index,follow',
robots: q ? 'noindex,follow' : 'index,follow',
active: 'pastes',
active: 'pastes',
+ styles: ['kit.css'],
jsonLd: [ld.breadcrumbLd([{ name: 'Home', url: '/' }, { name: 'Pastes', url: '/pastes' }])],
jsonLd: [ld.breadcrumbLd([{ name: 'Home', url: '/' }, { name: 'Pastes', url: '/pastes' }])],
body,
body,
});
});
@@ -555,7 +559,7 @@ ${p.burn_after_read ? '' : `<section class="section">
@@ -555,7 +559,7 @@ ${p.burn_after_read ? '' : `<section class="section">
</section>`}
</section>`}
${!locked && related && related.length ? `<section class="section" id="related">
${!locked && related && related.length ? `<section class="section" id="related">
<div class="section-head"><h2>More like this</h2><a class="more" href="${!isShot && p.language && p.language !== 'text' ? `/pastes?lang=${encodeURIComponent(p.language)}` : '/pastes'}">Browse <i class="fa-solid fa-arrow-right" aria-hidden="true"></i></a></div>
<div class="section-head"><h2>More like this</h2><a class="more" href="${!isShot && p.language && p.language !== 'text' ? `/pastes?lang=${encodeURIComponent(p.language)}` : '/pastes'}">Browse <i class="fa-solid fa-arrow-right" aria-hidden="true"></i></a></div>
- ${cardGrid(related)}
+ ${cardGrid(related, { title: 'No related pastes yet', text: 'Related pastes appear here when this one is linked with others.' })}
</section>` : ''}
</section>` : ''}
<script src="${ovServe.url('ov-share.js')}" defer></script>${locked ? `
<script src="${ovServe.url('ov-share.js')}" defer></script>${locked ? `
<script>window.OpenVibeLockerConfig = { network: ${JSON.stringify(config.networkUrl)} };</script>
<script>window.OpenVibeLockerConfig = { network: ${JSON.stringify(config.networkUrl)} };</script>
@@ -753,11 +757,11 @@ function myPage({ user, pastes, total, error }) {
@@ -753,11 +757,11 @@ function myPage({ user, pastes, total, error }) {
const body = `
const body = `
<header class="page-head">
<header class="page-head">
<h1>My pastes</h1>
<h1>My pastes</h1>
- <p class="muted">${esc(user.display_name || user.username)} · ${num(total || (pastes || []).length)} ${total === 1 ? 'paste' : 'pastes'} · <a href="/new">new paste</a> · <a href="${NETWORK_URL}/my" rel="noopener">account</a></p>
+ <p class="muted">${esc(user.display_name || user.username)} · ${num(total || (pastes || []).length)} ${total === 1 ? 'paste' : 'pastes'} · ${kit.button({ href: '/new', label: 'New paste', icon: 'plus', kind: 'quiet', small: true })} · <a href="${NETWORK_URL}/my" rel="noopener">account</a></p>
</header>
</header>
${error ? `<p class="alert alert-error" role="alert">${esc(error)}</p>` : ''}
${error ? `<p class="alert alert-error" role="alert">${esc(error)}</p>` : ''}
<section data-results>
<section data-results>
- ${cardGrid(pastes, 'You have not posted anything yet. Your public, unlisted and private pastes all show up here.')}
+ ${cardGrid(pastes, { title: 'No pastes yet', text: 'You have not posted anything yet. Your public, unlisted and private pastes all show up here.', action: { href: '/new', label: 'Make a paste', icon: 'plus' } })}
</section>`;
</section>`;
return renderPage({
return renderPage({
title: 'My pastes',
title: 'My pastes',
@@ -766,6 +770,7 @@ ${error ? `<p class="alert alert-error" role="alert">${esc(error)}</p>` : ''}
@@ -766,6 +770,7 @@ ${error ? `<p class="alert alert-error" role="alert">${esc(error)}</p>` : ''}
robots: 'noindex,nofollow',
robots: 'noindex,nofollow',
active: 'my',
active: 'my',
footerVariant: 'compact',
footerVariant: 'compact',
+ styles: ['kit.css'],
body,
body,
});
});
}
}
@@ -11,6 +11,7 @@ const { renderPage } = require('./layout');
@@ -11,6 +11,7 @@ const { renderPage } = require('./layout');
const { escapeHtml: esc } = require('./highlight');
const { escapeHtml: esc } = require('./highlight');
const { timeTag } = require('./pages');
const { timeTag } = require('./pages');
const { who } = require('./forum');
const { who } = require('./forum');
+const kit = require('openvibe-shared/kit');
const FILTERS = [['', 'Everything'], ['user', 'People'], ['ai', 'AI'], ['system', 'System']];
const FILTERS = [['', 'Everything'], ['user', 'People'], ['ai', 'AI'], ['system', 'System']];
const SERVICE_NAMES = { community: 'Community', live: 'Live', media: 'Media', network: 'Network', tools: 'Tools', games: 'Games', wiki: 'Wiki', blog: 'Blog', reviews: 'Reviews' };
const SERVICE_NAMES = { community: 'Community', live: 'Live', media: 'Media', network: 'Network', tools: 'Tools', games: 'Games', wiki: 'Wiki', blog: 'Blog', reviews: 'Reviews' };
@@ -52,7 +53,7 @@ function pulsePage({ items, origin = '', after = null, nextCursor = null, origin
@@ -52,7 +53,7 @@ function pulsePage({ items, origin = '', after = null, nextCursor = null, origin
<nav class="tabs sort-tabs" aria-label="Filter by origin">${tabs}</nav>
<nav class="tabs sort-tabs" aria-label="Filter by origin">${tabs}</nav>
<section data-results${after ? '' : ' data-pulse-live'}>
<section data-results${after ? '' : ' data-pulse-live'}>
<p class="pulse-new" hidden><button type="button" class="btn btn-sm" data-pulse-show></button></p>
<p class="pulse-new" hidden><button type="button" class="btn btn-sm" data-pulse-show></button></p>
- ${items.length ? `<ol class="thread-list pulse-list">${items.map(itemHtml).join('')}</ol>` : '<p class="empty">Nothing here yet.</p>'}
+ ${items.length ? `<ol class="thread-list pulse-list">${items.map(itemHtml).join('')}</ol>` : kit.empty({ icon: 'live', title: 'Nothing here yet', text: origin ? 'Nothing from this part of the network has arrived yet.' : 'New public clips, threads, pastes and posts land here as they happen.', action: origin ? { href: '/pulse', label: 'Everything', icon: 'live' } : { href: '/forum', label: 'Visit the forum', icon: 'chat' } })}
${after || nextCursor ? `<nav class="pager" aria-label="Pages">${after ? `<a href="${esc(href(origin))}"><i class="fa-solid fa-chevron-left" aria-hidden="true"></i> Latest</a>` : '<span></span>'}<span></span>${nextCursor ? `<a rel="next" href="${esc(href(origin, nextCursor))}">Older <i class="fa-solid fa-chevron-right" aria-hidden="true"></i></a>` : '<span></span>'}</nav>` : ''}
${after || nextCursor ? `<nav class="pager" aria-label="Pages">${after ? `<a href="${esc(href(origin))}"><i class="fa-solid fa-chevron-left" aria-hidden="true"></i> Latest</a>` : '<span></span>'}<span></span>${nextCursor ? `<a rel="next" href="${esc(href(origin, nextCursor))}">Older <i class="fa-solid fa-chevron-right" aria-hidden="true"></i></a>` : '<span></span>'}</nav>` : ''}
</section>`;
</section>`;
return renderPage({
return renderPage({
@@ -64,6 +65,7 @@ function pulsePage({ items, origin = '', after = null, nextCursor = null, origin
@@ -64,6 +65,7 @@ function pulsePage({ items, origin = '', after = null, nextCursor = null, origin
active: 'pulse',
active: 'pulse',
feeds: [{ title: 'OpenVibe.Community — latest threads', href: '/forum/feed.xml' }],
feeds: [{ title: 'OpenVibe.Community — latest threads', href: '/forum/feed.xml' }],
jsonLd: [ld.breadcrumbLd([{ name: 'Home', url: '/' }, { name: 'Pulse', url: '/pulse' }])],
jsonLd: [ld.breadcrumbLd([{ name: 'Home', url: '/' }, { name: 'Pulse', url: '/pulse' }])],
+ styles: ['kit.css'],
body,
body,
});
});
}
}
@@ -6,6 +6,7 @@
@@ -6,6 +6,7 @@
*/
*/
const { renderPage } = require('./layout');
const { renderPage } = require('./layout');
const { escapeHtml: esc } = require('./highlight');
const { escapeHtml: esc } = require('./highlight');
+const kit = require('openvibe-shared/kit');
// Search escapes the text and adds only <mark>; keep exactly that, whatever arrives.
// Search escapes the text and adds only <mark>; keep exactly that, whatever arrives.
const snippet = (html) => esc(String(html || '')).replace(/<(\/?)mark>/g, '<$1mark>');
const snippet = (html) => esc(String(html || '')).replace(/<(\/?)mark>/g, '<$1mark>');
@@ -28,9 +29,9 @@ function searchPage({ q = '', type = '', results = [], nextCursor = null, unavai
@@ -28,9 +29,9 @@ function searchPage({ q = '', type = '', results = [], nextCursor = null, unavai
const opt = (v, label) => `<option value="${v}"${type === v ? ' selected' : ''}>${label}</option>`;
const opt = (v, label) => `<option value="${v}"${type === v ? ' selected' : ''}>${label}</option>`;
const more = nextCursor ? `<nav class="pager"><a class="btn" href="/search?${new URLSearchParams({ q, ...(type ? { type } : {}), cursor: nextCursor })}">More results</a></nav>` : '';
const more = nextCursor ? `<nav class="pager"><a class="btn" href="/search?${new URLSearchParams({ q, ...(type ? { type } : {}), cursor: nextCursor })}">More results</a></nav>` : '';
let section;
let section;
- if (unavailable) section = `<p class="empty">Search is not answering right now. <a href="/pastes?q=${encodeURIComponent(q)}">Filter pastes for “${esc(q)}”</a> instead, or try again in a minute.</p>`;
- else if (!q) section = '<p class="empty">Search every public thread and paste on OpenVibe.Community.</p>';
- else if (!results.length) section = `<p class="empty">Nothing public matches “${esc(q)}”.</p>`;
+ if (unavailable) section = kit.empty({ icon: 'search', title: 'Search is not answering', text: 'OpenVibe.Search is not answering right now. Try again in a minute, or filter pastes instead.', action: q ? { href: `/pastes?q=${encodeURIComponent(q)}`, label: 'Filter pastes', icon: 'search' } : { href: '/pastes', label: 'Browse pastes', icon: 'grid' } });
+ else if (!q) section = kit.empty({ icon: 'search', title: 'Search Community', text: 'Search every public thread and paste on OpenVibe.Community.', action: { href: '/forum', label: 'Browse the forum', icon: 'chat' } });
+ else if (!results.length) section = kit.empty({ icon: 'search', title: 'Nothing matches', text: `Nothing public matches “${q}”.`, action: { href: `https://search.openvibe.network/?q=${encodeURIComponent(q)}`, label: 'Search the whole network', icon: 'search' } });
else section = `<ol class="search-results">${results.map(resultItem).join('\n')}</ol>${more}`;
else section = `<ol class="search-results">${results.map(resultItem).join('\n')}</ol>${more}`;
const body = `
const body = `
<header class="page-head">
<header class="page-head">
@@ -49,6 +50,7 @@ function searchPage({ q = '', type = '', results = [], nextCursor = null, unavai
@@ -49,6 +50,7 @@ function searchPage({ q = '', type = '', results = [], nextCursor = null, unavai
canonicalPath: '/search',
canonicalPath: '/search',
robots: 'noindex,follow',
robots: 'noindex,follow',
active: 'search',
active: 'search',
+ styles: ['kit.css'],
body,
body,
});
});
}
}
@@ -11,6 +11,7 @@ const { escapeHtml: esc } = require('./highlight');
@@ -11,6 +11,7 @@ const { escapeHtml: esc } = require('./highlight');
const { renderMarkdown, markdownToText } = require('./markdown');
const { renderMarkdown, markdownToText } = require('./markdown');
const { timeTag } = require('./pages');
const { timeTag } = require('./pages');
const { who } = require('./forum');
const { who } = require('./forum');
+const kit = require('openvibe-shared/kit');
const KIND_LABELS = { clip: 'Clip', art: 'Art', idea: 'Idea', report: 'Report' };
const KIND_LABELS = { clip: 'Clip', art: 'Art', idea: 'Idea', report: 'Report' };
const STATUS_LABELS = { pending: 'Pending review', accepted: 'Accepted', rejected: 'Not accepted', withdrawn: 'Withdrawn' };
const STATUS_LABELS = { pending: 'Pending review', accepted: 'Accepted', rejected: 'Not accepted', withdrawn: 'Withdrawn' };
@@ -37,8 +38,8 @@ function itemHtml(s) {
@@ -37,8 +38,8 @@ function itemHtml(s) {
</li>`;
</li>`;
}
}
-function listBlock(items, { empty, prev, next }) {
- return `${items.length ? `<ol class="thread-list pulse-list">${items.map(itemHtml).join('')}</ol>` : `<p class="empty">${esc(empty)}</p>`}
+function listBlock(items, { empty, action = null, prev, next }) {
+ return `${items.length ? `<ol class="thread-list pulse-list">${items.map(itemHtml).join('')}</ol>` : kit.empty({ icon: 'mail', title: empty.title, text: empty.text, action })}
${prev || next ? `<nav class="pager" aria-label="Pages">${prev ? `<a href="${esc(prev)}"><i class="fa-solid fa-chevron-left" aria-hidden="true"></i> Newest</a>` : '<span></span>'}<span></span>${next ? `<a rel="next" href="${esc(next)}">Older <i class="fa-solid fa-chevron-right" aria-hidden="true"></i></a>` : '<span></span>'}</nav>` : ''}`;
${prev || next ? `<nav class="pager" aria-label="Pages">${prev ? `<a href="${esc(prev)}"><i class="fa-solid fa-chevron-left" aria-hidden="true"></i> Newest</a>` : '<span></span>'}<span></span>${next ? `<a rel="next" href="${esc(next)}">Older <i class="fa-solid fa-chevron-right" aria-hidden="true"></i></a>` : '<span></span>'}</nav>` : ''}`;
}
}
@@ -72,7 +73,11 @@ function submissionsPage({ items, kind = '', mine = false, after = null, nextCur
@@ -72,7 +73,11 @@ function submissionsPage({ items, kind = '', mine = false, after = null, nextCur
</header>
</header>
<nav class="tabs sort-tabs" aria-label="Filter by kind">${tabs}</nav>
<nav class="tabs sort-tabs" aria-label="Filter by kind">${tabs}</nav>
<section data-results>
<section data-results>
- ${listBlock(items, { empty: mine ? 'You have not submitted anything yet.' : 'Nothing accepted yet.', prev: after ? listHref({ kind, mine }) : null, next: nextCursor ? listHref({ kind, mine, after: nextCursor }) : null })}
+ ${listBlock(items, {
+ empty: mine ? { title: 'No submissions yet', text: 'You have not submitted anything yet.' } : { title: 'Nothing accepted yet', text: 'No submission has been accepted here yet.' },
+ action: user ? { href: '#submit', label: 'Submit something', icon: 'plus' } : { href: '/auth/login?next=%2Fsubmissions', label: 'Sign in to submit', icon: 'plus' },
+ prev: after ? listHref({ kind, mine }) : null, next: nextCursor ? listHref({ kind, mine, after: nextCursor }) : null,
+ })}
</section>
</section>
${submitForm({ user, values, error })}`;
${submitForm({ user, values, error })}`;
return renderPage({
return renderPage({
@@ -81,6 +86,7 @@ ${submitForm({ user, values, error })}`;
@@ -81,6 +86,7 @@ ${submitForm({ user, values, error })}`;
canonicalPath: listHref({ kind, after }),
canonicalPath: listHref({ kind, after }),
robots: mine || after ? 'noindex,follow' : 'index,follow',
robots: mine || after ? 'noindex,follow' : 'index,follow',
active: 'submissions',
active: 'submissions',
+ styles: ['kit.css'],
jsonLd: [ld.breadcrumbLd([{ name: 'Home', url: '/' }, { name: 'Submissions', url: '/submissions' }])],
jsonLd: [ld.breadcrumbLd([{ name: 'Home', url: '/' }, { name: 'Submissions', url: '/submissions' }])],
body,
body,
});
});
@@ -141,9 +147,9 @@ function reviewQueuePage({ items, after = null, nextCursor = null }) {
@@ -141,9 +147,9 @@ function reviewQueuePage({ items, after = null, nextCursor = null }) {
<p class="muted">Submissions waiting for a decision, newest first. Open one to accept or reject it.</p>
<p class="muted">Submissions waiting for a decision, newest first. Open one to accept or reject it.</p>
</header>
</header>
<section data-results>
<section data-results>
- ${listBlock(items, { empty: 'Nothing is waiting for review.', prev: after ? '/submissions/review' : null, next: nextCursor ? listHref({ after: nextCursor }, '/submissions/review') : null })}
+ ${listBlock(items, { empty: { title: 'Nothing to review', text: 'Nothing is waiting for review.' }, action: { href: '/submissions', label: 'See submissions', icon: 'grid' }, prev: after ? '/submissions/review' : null, next: nextCursor ? listHref({ after: nextCursor }, '/submissions/review') : null })}
</section>`;
</section>`;
- return renderPage({ title: 'Review queue', description: 'Submissions waiting for review.', canonicalPath: '/submissions/review', robots: 'noindex,nofollow', active: 'submissions', footerVariant: 'compact', body });
+ return renderPage({ title: 'Review queue', description: 'Submissions waiting for review.', canonicalPath: '/submissions/review', robots: 'noindex,nofollow', active: 'submissions', footerVariant: 'compact', styles: ['kit.css'], body });
}
}
module.exports = { submissionsPage, submissionPage, reviewQueuePage, KIND_LABELS, STATUS_LABELS };
module.exports = { submissionsPage, submissionPage, reviewQueuePage, KIND_LABELS, STATUS_LABELS };
new file mode 100644
new file mode 100644
@@ -0,0 +1,82 @@
@@ -0,0 +1,82 @@
+'use strict';
+/**
+ * The plain-page sweep (owner ask 81): the pages that used to print a bare "No X yet" draw the shared kit's
+ * empty state instead, and the one next action is a kit button. Each page that can show one loads kit.css.
+ */
+const assert = require('assert');
+const { pulsePage } = require('../server/render/pulse');
+const { boardIndexPage, forumSpacePage } = require('../server/render/board');
+const { searchPage } = require('../server/render/search');
+const { submissionsPage, reviewQueuePage } = require('../server/render/submissions');
+const { cardGrid } = require('../server/render/pages');
+
+const has = (html, s, msg) => assert.ok(html.includes(s), msg || `expected to find: ${s}`);
+const emptyState = (html, title) => {
+ has(html, 'class="ovk-empty"');
+ has(html, title);
+ has(html, 'kit.css');
+};
+
+const space = { slug: 'empty', name: 'Empty', description: '', visibility: 'public', members_only: 0, style: 'forum', thread_kind: 'topic', statuses: [] };
+
+const checks = [
+ ['the board index with no forum', () => {
+ const html = boardIndexPage({ groups: [] });
+ emptyState(html, 'No forums yet');
+ has(html, 'See what is happening');
+ has(html, 'ovk-btn');
+ }],
+ ['a forum with no topic', () => {
+ const html = forumSpacePage({ space, threads: [], page: 1, pages: 1, total: 0, viewer: { can_start: true } });
+ emptyState(html, 'No topics yet');
+ has(html, 'Start the first topic');
+ }],
+ ['a filtered forum with no match', () => {
+ const html = forumSpacePage({ space, threads: [], page: 1, pages: 1, total: 0, category: 'other', viewer: { can_start: true } });
+ emptyState(html, 'Nothing here yet');
+ has(html, 'See every topic');
+ }],
+ ['Pulse with nothing', () => {
+ const html = pulsePage({ items: [] });
+ emptyState(html, 'Nothing here yet');
+ has(html, 'Visit the forum');
+ }],
+ ['Search before a query', () => {
+ const html = searchPage({ q: '', results: [] });
+ emptyState(html, 'Search Community');
+ has(html, 'Browse the forum');
+ }],
+ ['Search with no match', () => {
+ const html = searchPage({ q: 'zzz', results: [] });
+ emptyState(html, 'Nothing matches');
+ has(html, 'Search the whole network');
+ }],
+ ['Search not answering', () => {
+ const html = searchPage({ q: 'zzz', results: [], unavailable: true });
+ emptyState(html, 'Search is not answering');
+ has(html, 'Filter pastes');
+ }],
+ ['Submissions with nothing accepted', () => {
+ const html = submissionsPage({ items: [] });
+ emptyState(html, 'Nothing accepted yet');
+ has(html, 'Sign in to submit');
+ }],
+ ['the review queue with nothing waiting', () => {
+ const html = reviewQueuePage({ items: [] });
+ emptyState(html, 'Nothing to review');
+ has(html, 'See submissions');
+ }],
+ ['a paste grid with nothing', () => {
+ const html = cardGrid([], { title: 'No pastes yet', text: 'No paste has been shared here yet.', action: { href: '/new', label: 'Make a paste', icon: 'plus' } });
+ has(html, 'class="ovk-empty"');
+ has(html, 'No pastes yet');
+ has(html, 'Make a paste');
+ }],
+];
+
+let failed = 0;
+for (const [name, run] of checks) {
+ try { run(); console.log(`ok - ${name}`); }
+ catch (e) { failed += 1; console.error(`not ok - ${name}: ${e.message}`); }
+}
+process.exitCode = failed ? 1 : 0;
@@ -182,7 +182,7 @@ const { boot, check, done } = require('./helpers/app');
@@ -182,7 +182,7 @@ const { boot, check, done } = require('./helpers/app');
assert.ok(people.includes('<meta name="robots" content="index,follow">'));
assert.ok(people.includes('<meta name="robots" content="index,follow">'));
const empty = pulsePage({ items: [], origin: 'system', origins: ['user'] });
const empty = pulsePage({ items: [], origin: 'system', origins: ['user'] });
assert.ok(empty.includes('aria-current="page">System</a>'), 'the filter asked for keeps its tab');
assert.ok(empty.includes('aria-current="page">System</a>'), 'the filter asked for keeps its tab');
- assert.ok(empty.includes('Nothing here yet.') && empty.includes('<meta name="robots" content="noindex,follow">'));
+ assert.ok(empty.includes('Nothing here yet') && empty.includes('class="ovk-empty"') && empty.includes('<meta name="robots" content="noindex,follow">'));
assert.ok(pulsePage({ items: [item] }).includes('href="/pulse?origin=system"'), 'without the origins list every filter shows');
assert.ok(pulsePage({ items: [item] }).includes('href="/pulse?origin=system"'), 'without the origins list every filter shows');
const r = await t.get('/pulse');
const r = await t.get('/pulse');
assert.ok(r.text.includes('href="/pulse?origin=system"'), 'system items exist here, so the tab shows');
assert.ok(r.text.includes('href="/pulse?origin=system"'), 'system items exist here, so the tab shows');