openvibers/openvibe.media Commits

Media: an empty browse list draws the kit's empty state (#381)

OP OpenVibers committed 3 days ago · 3d000dea32 · 3 files, +15 −4

* Media: an empty browse list draws the kit's empty state

The index said 'Nothing here yet.' in a bare box with no way forward. It
now uses the shared kit's empty state and its button, Open your media; the
index loads kit.css for it. test/home-short.test.js checks the empty list,
its button and the stylesheet.

* Media: load kit.css only on the empty index

The empty list's look and button are the only kit styles the index needs,
so kit.css comes with the empty state instead of every page. The front
page keeps room.css alone, so the two-stylesheet budget is unchanged.

Changes

ddiff --git a/server/public/browse.js b/server/public/browse.js
iindex 80de9ae..8c466c6 100644
---- a/server/public/browse.js
++++ b/server/public/browse.js
@@@ -406,7 +406,7 @@ function renderPage(tab, page, data, counts, { home = false } = {}) {
// On the home the list is a section like the ones around it (the same inset, so its heading, tabs and grid line up
// with the hero and the developer sections); later pages and tabs keep the plain index layout.
const list = `<nav class="tabs" aria-label="Media types">${TABS.map(([k, label]) => `<a class="${k === tab ? 'on' : ''}" href="/?tab=${k}">${label}<span class="n">${(counts[k] ?? 0).toLocaleString()}</span></a>`).join('')}</nav>
--${data.cards.length ? `<div class="grid">${data.cards.map(renderCard).join('')}</div>` : '<div class="empty">Nothing here yet.</div>'}
++${data.cards.length ? `<div class="grid">${data.cards.map(renderCard).join('')}</div>` : kit.empty({ icon: 'grid', title: 'Nothing here yet', text: 'No public media is in this list yet.', action: { href: '/me', label: 'Open your media', icon: 'plus' } })}
<div class="pager">${nav(page - 1, '‹ Prev', page <= 1)}<span>Page ${page} / ${pages} · ${data.total.toLocaleString()} items</span>${nav(page + 1, 'Next ›', page >= pages)}</div>`;
const body = `${heading}
${first ? `<section class="sc-sec sc-media">${kit.rail({ id: 'media', wrap: true, title: 'Latest public media', more: { href: '/?tab=all', label: `See all ${data.total.toLocaleString('en-US')}` }, items: data.cards.slice(0, 12).map(asCard) })}<p class="sc-kinds" aria-label="Browse by kind">${TABS.filter(([k]) => k !== 'all' && (counts[k] ?? 0) > 0).map(([k, label]) => kit.button({ href: `/?tab=${k}`, label: `${label} ${(counts[k] ?? 0).toLocaleString('en-US')}`, kind: 'quiet', small: true })).join(' ')}</p></section>` : list}
@@@ -421,6 +421,7 @@ ${first ? foldBlock({ id: 'shipped', head: '<h2 id="shipped-h">Recently shipped
},
css: BROWSE_CSS + (rules.length ? `@supports selector(:has(*)){@media (max-width:640px){${rules.join('')}}}` : ''),
kit: first,
++ kitCss: !data.cards.length && !first,
body,
// Later pages are noindex and carry no structured data, the summary's WebPage included.
summary: page === 1 ? {
ddiff --git a/server/public/page-frame.js b/server/public/page-frame.js
iindex 2bba3bb..28f032c 100644
---- a/server/public/page-frame.js
++++ b/server/public/page-frame.js
@@@ -90,7 +90,7 @@ function fmtDate(dt) {
* `shell.page`'s `head`, so the shell still owns title, description, canonical, robots, og/twitter
* mirroring and JSON-LD.
*/
--function extraHead(seo, css, kit = false) {
++function extraHead(seo, css, kit = false, kitCss = false) {
const image = seo.image || DEFAULT_OG_IMAGE;
const videoTags = seo.video && seo.video.url ? [
`<meta property="og:video" content="${esc(seo.video.url)}">`,
@@@ -104,6 +104,8 @@ function extraHead(seo, css, kit = false) {
// A page with the kit's cards and rails (the front page's latest media) carries room.css, their look; it needs
// neither kit.css nor kit-web.js (no menus on these cards, and wide screens wrap the rail instead of scrolling it).
kit ? `<link rel="stylesheet" href="${ovServe.url('room.css')}">` : '',
++ // An empty list draws the kit's empty state, whose look and button live in kit.css: load it only on that page.
++ kitCss ? `<link rel="stylesheet" href="${ovServe.url('kit.css')}">` : '',
// A page with a video plays it in the network's player (openvibe-shared player.js/css on video[data-ov-player]).
seo.video ? `<link rel="stylesheet" href="${ovServe.url('player.css')}">\n<script src="${ovServe.url('player.js')}" defer></script>` : '',
`<style>${baseCss()}${css}</style>`,
@@@ -213,7 +215,7 @@ function oneGraph(nodes) {
return { '@context': 'https://schema.org', '@graph': [].concat(nodes).map(({ '@context': _context, ...node }) => node) };
}
--function page({ seo, css = '', body, history, footer, summary, agentLayer = false, kit = false }) {
++function page({ seo, css = '', body, history, footer, summary, agentLayer = false, kit = false, kitCss = false }) {
const sum = summary == null ? null : typeof summary === 'object' ? summary : { summary };
const summaryData = sum ? seoLib.pageSummary({ title: seo.title, summary: sum.summary, facts: sum.facts, url: sum.url || seo.canonical, updated: sum.updated, ...(sum.name ? { title: sum.name } : {}) }) : null;
const html = shell.page({
@@@ -229,7 +231,7 @@ function page({ seo, css = '', body, history, footer, summary, agentLayer = fals
type: seo.ogType,
robots: seo.robots || 'index, follow',
jsonLd: oneGraph([...[].concat(seo.jsonLd || []), ...(summaryData ? [summaryData.jsonLd] : [])]),
-- head: extraHead(seo, css, kit) + (summaryData && summaryData.meta ? `<meta name="ai-summary" content="${summaryData.meta}">` : '') + footerBootScript(footer),
++ head: extraHead(seo, css, kit, kitCss) + (summaryData && summaryData.meta ? `<meta name="ai-summary" content="${summaryData.meta}">` : '') + footerBootScript(footer),
body: `${summaryData ? summaryData.body : ''}<div id="navbar-mount"></div>\n<main id="main">\n${body}\n</main>`,
navLinks: [{ label: 'Videos', href: '/?tab=videos' }, { label: 'Clips', href: '/?tab=clips' }],
navbar: navOptions({ history }),
ddiff --git a/test/home-short.test.js b/test/home-short.test.js
iindex f44ddd4..5c8f25e 100644
---- a/test/home-short.test.js
++++ b/test/home-short.test.js
@@@ -34,6 +34,14 @@ const path = require('path');
assert.strictEqual(r.status, 200);
const text = r.body;
++ // The index with no public media draws the shared kit's empty state and one button forward.
++ const all = await get('/?tab=all');
++ assert.strictEqual(all.status, 200);
++ assert.match(all.body, /class="ovk-empty"/, 'an empty browse list draws the kit empty state');
++ assert.match(all.body, /No public media is in this list yet/);
++ assert.match(all.body, /class="ovk-btn[^"]*" href="\/me"/, 'the empty state offers one button forward');
++ assert.match(all.body, /\/shared\/kit\.css\?v=[0-9a-f]+/, 'the page loads kit.css for it');
++
// The code samples: one pick block, a chip per sample, the first one picked.
assert.ok(text.includes('<div class="pick" id="pick-api">'), 'the code samples are one pick block');
assert.match(text, /<nav class="pick-row" aria-label="The ways to call Media">/);