openvibers/openvibe.live Commits

The Rundown says what you can do with it: catch up on a stream, follow a topic, jump to the moment, get written up (a viewer: "still don't rly know wtf to do with this feature") (#270)

OP OpenVibers committed 24 days ago · a9101b0c4e · 3 files, +78 −1

Changes

ddiff --git a/public/css/features/rundown.css b/public/css/features/rundown.css
iindex 9e194743..8cec509c 100644
---- a/public/css/features/rundown.css
++++ b/public/css/features/rundown.css
@@@ -13,6 +13,17 @@
.rd-tab:hover { color: var(--text-primary); border-color: var(--border); }
.rd-tab.is-on { color: var(--text-primary); background: var(--bg-tertiary); border-color: var(--border); }
++/* What you can do here (rundown.js _rdUses): four entry points under the front page's masthead */
++.rd-uses { list-style: none; margin: 0 0 18px; padding: 0; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; }
++.rd-use { display: flex; gap: 10px; align-items: flex-start; height: 100%; padding: 12px; border-radius: 14px; background: var(--bg-card);
++ border: 1px solid var(--border); color: var(--text-secondary); text-decoration: none; line-height: 1.35; }
++a.rd-use:hover { border-color: var(--accent); }
++a.rd-use:hover b { color: var(--accent); }
++.rd-use i { flex: none; display: grid; place-items: center; width: 34px; height: 34px; border-radius: 10px; font-size: 0.92rem;
++ background: color-mix(in srgb, var(--accent) 16%, transparent); color: var(--accent); }
++.rd-use b { display: block; margin-bottom: 2px; color: var(--text-primary); font-size: 0.95rem; }
++.rd-use small { display: block; font-size: 0.85rem; }
++
/* Headings, layout */
.rd-h1 { margin: 6px 0 8px; font-size: clamp(1.7rem, 5vw, 2.6rem); line-height: 1.08; font-weight: 900; letter-spacing: -0.02em; }
.rd-h2 { display: flex; align-items: baseline; gap: 8px; margin: 26px 0 12px; font-size: 0.85rem; font-weight: 800; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-secondary); }
@@@ -195,10 +206,17 @@
.rd-topic-card .rd-what { margin: 0; font-size: 0.94rem; color: var(--text-secondary); }
@media (max-width: 960px) {
++ .rd-uses { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.rd-grid { grid-template-columns: minmax(0, 1fr); }
.rd-side { position: static; }
}
@media (max-width: 560px) {
++ /* Two by two, titles only: the four lines of text would push the first episode below the fold. */
++ .rd-uses { gap: 8px; margin-bottom: 14px; }
++ .rd-use { align-items: center; padding: 9px 10px; gap: 8px; }
++ .rd-use i { width: 28px; height: 28px; font-size: 0.8rem; }
++ .rd-use b { margin: 0; font-size: 0.86rem; }
++ .rd-use small { display: none; }
.rd-card { padding: 12px 13px; }
.rd-lead { padding: 14px; }
.rd-issue-head { padding: 12px 13px 10px; }
ddiff --git a/public/js/rundown.js b/public/js/rundown.js
iindex 03c51420..706eef5a 100644
---- a/public/js/rundown.js
++++ b/public/js/rundown.js
@@@ -177,6 +177,21 @@ function _rdMast(counts, active = 'latest') {
<nav class="rd-tabs" aria-label="The Rundown">${tab('latest', '/rundown', 'Latest')}${tab('topics', '/rundown/topics', 'Topics')}</nav>
</header>`;
}
++// What a visitor can do here, under the masthead of the front page. The page said what it is and never what it is for
++// ("still don't rly know wtf to do with this feature", a viewer, 2026-10-20). "Jump to the moment" opens the top take's.
++function _rdUses(lead) {
++ const item = (icon, title, text, href) => {
++ const body = `<i class="fa-solid ${icon}" aria-hidden="true"></i><span><b>${title}</b><small>${text}</small></span>`;
++ if (!href) return `<li><div class="rd-use">${body}</div></li>`;
++ return `<li>${href.startsWith('#') ? `<a class="rd-use" href="${href}">${body}</a>` : _rdA(href, body, 'rd-use')}</li>`;
++ };
++ return `<ul class="rd-uses" aria-label="What you can do here">
++ ${item('fa-forward', 'Catch up on a stream', 'Every stream becomes an episode: a recap, chapters and what was said.', '#rd-latest-h')}
++ ${item('fa-hashtag', 'Follow a topic', 'See every streamer who talked about it this week, side by side.', '/rundown/topics')}
++ ${item('fa-play', 'Jump to the moment', 'A take with a recording links to the second it was said.', lead && lead.vod_id ? videoHref('vod', lead.vod_id, Math.max(0, Math.floor(((lead.quotes || [])[0] || lead).sec || 0))) : null)}
++ ${item('fa-tower-broadcast', 'Get written up', 'Go live, and the desk turns your stream into an episode here.', '/broadcast')}
++ </ul>`;
++}
function _rdOnAir(list) {
if (!list.length) return '';
return `<section class="rd-onair" aria-labelledby="rd-onair-h"><h2 id="rd-onair-h" class="rd-h2"><span class="rd-dot" aria-hidden="true"></span> On air now</h2>
@@@ -207,7 +222,7 @@ async function _rdHome(root) {
const d = await api('/rundown');
_rdNext = d.streams_next;
const feed = d.streams || [];
-- root.innerHTML = `${_rdMast(d.counts, 'latest')}${_rdOnAir(d.on_air || [])}
++ root.innerHTML = `${_rdMast(d.counts, 'latest')}${_rdUses(d.lead)}${_rdOnAir(d.on_air || [])}
<div class="rd-grid">
<div class="rd-main">
${d.lead ? _rdLead(d.lead) : ''}
ddiff --git a/test/rundown-uses.test.js b/test/rundown-uses.test.js
nnew file mode 100644
iindex 00000000..baa5977e
---- /dev/null
++++ b/test/rundown-uses.test.js
@@@ -0,0 +1,44 @@
++/**
++ * The Rundown's front page says what a visitor can do there (public/js/rundown.js _rdUses): catch up on a stream (the
++ * stream-by-stream list), follow a topic (/rundown/topics), jump to the moment (the top take's second on OpenVibe.Video)
++ * and get written up (/broadcast). A viewer said of the page "still don't rly know wtf to do with this feature"
++ * (2026-10-20): it said what it is, never what it is for.
++ *
++ * node test/rundown-uses.test.js
++ */
++'use strict';
++const assert = require('assert');
++const fs = require('fs');
++const path = require('path');
++const vm = require('vm');
++
++const js = fs.readFileSync(path.join(__dirname, '../public/js/rundown.js'), 'utf8');
++const css = fs.readFileSync(path.join(__dirname, '../public/css/features/rundown.css'), 'utf8');
++const pick = (name) => {
++ const start = js.indexOf(`function ${name}(`);
++ assert.ok(start >= 0, `${name} is in rundown.js`);
++ const next = js.indexOf('\nfunction ', start + 1);
++ return js.slice(start, next < 0 ? undefined : next);
++};
++
++const ctx = { videoHref: (kind, id, t) => `https://openvibe.video/${kind === 'clip' ? 'c' : 'v'}/${id}${t ? `?t=${t}` : ''}` };
++vm.createContext(ctx);
++vm.runInContext([pick('_rdEsc'), pick('_rdA'), pick('_rdUses')].join('\n'), ctx);
++
++const links = (html) => [...html.matchAll(/<a class="rd-use" href="([^"]+)"/g)].map((m) => m[1]);
++const titles = (html) => [...html.matchAll(/<b>([^<]+)<\/b>/g)].map((m) => m[1]);
++
++const withLead = ctx._rdUses({ id: 7, vod_id: 5855, sec: 40, quotes: [{ text: 'x', sec: 95.6 }] });
++assert.deepStrictEqual(titles(withLead), ['Catch up on a stream', 'Follow a topic', 'Jump to the moment', 'Get written up']);
++assert.deepStrictEqual(links(withLead), ['#rd-latest-h', '/rundown/topics', 'https://openvibe.video/v/5855?t=95', '/broadcast'],
++ 'each entry goes somewhere: the list, the topics, the top take\'s moment (its quote\'s second), going live');
++assert.ok(withLead.includes('aria-label="What you can do here"'));
++
++const noLead = ctx._rdUses(null);
++assert.deepStrictEqual(links(noLead), ['#rd-latest-h', '/rundown/topics', '/broadcast'], 'no recording to jump into: that entry is text, not a dead link');
++assert.ok(noLead.includes('<div class="rd-use"><i class="fa-solid fa-play"'));
++
++assert.match(js, /root\.innerHTML = `\$\{_rdMast\(d\.counts, 'latest'\)\}\$\{_rdUses\(d\.lead\)\}/, 'the front page shows it under the masthead');
++assert.match(css, /@media \(max-width: 560px\) \{[^}]*\.rd-uses \{[^}]*\}[\s\S]*?\.rd-use small \{ display: none; \}/, 'phones show titles only, two by two');
++
++console.log('rundown uses: all checks passed');