openvibers/openvibe.fun Commits

Fun's home leads with what is real; one 'Be the first' row instead of empty sections (#27)

OP OpenVibers committed 13 days ago · a8dc6281a3 · 3 files, +48 −9

Changes

ddiff --git a/public/css/fun.css b/public/css/fun.css
iindex f9f9283..1214c70 100644
---- a/public/css/fun.css
++++ b/public/css/fun.css
@@@ -112,3 +112,11 @@ html.ov-motion .fu-flip-card { animation: fu-flip .8s cubic-bezier(.2,.7,.2,1) .
@media (max-width: 759px) { .sc-room-pic .fu-pin, .sc-room-pic .fu-flip-card { display: none; } }
@media (prefers-reduced-motion: reduce) { .fu-pin, .fu-flip-card { animation: none !important; } }
html[data-ov-motion=reduced] .fu-pin, html[data-ov-motion=reduced] .fu-flip-card { animation: none !important; }
++
++/* Real things first (10-31): the guide's call to action, and one short "be the first" row for what has no entries yet. */
++.fu-cta { display: inline-flex; align-items: center; min-height: 44px; padding: 0 18px; border-radius: 999px; background: var(--room-strong, var(--room-accent)); color: var(--room-on, #fff); font-weight: 700; text-decoration: none; }
++.fu-cta:focus-visible { outline: 2px solid var(--room-accent); outline-offset: 3px; }
++.fu-first-list { list-style: none; margin: 10px 0 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr)); gap: 10px; }
++.fu-first-list li { display: grid; gap: 4px; padding: 14px 16px; border-radius: 16px; border: 1px dashed color-mix(in srgb, var(--room-accent) 45%, var(--border, #2c3d5c)); }
++.fu-first-list span { color: var(--text-secondary, #a8b3c4); font-size: 14px; line-height: 1.45; }
++.fu-first-list a { justify-self: start; margin-top: 4px; font-weight: 700; color: var(--room-accent); }
ddiff --git a/server/views/index.js b/server/views/index.js
iindex eb9536c..2021a6f 100644
---- a/server/views/index.js
++++ b/server/views/index.js
@@@ -63,18 +63,27 @@ function cityMotif() {
}
function home({ counts = {}, featured = [], plans = [] } = {}) {
-- const live = `<section class="fu-section" id="fu-featured" aria-labelledby="fu-featured-h">
--<div class="fu-head-row"><h2 id="fu-featured-h">IRL quests on Live</h2><a class="fu-more" href="/quests">All quests</a></div>
--${featured.length ? `<ol class="fu-quests">${featured.slice(0, 6).map(questCard).join('')}</ol>` : empty('No IRL quest is public yet. A streamer opens a board and chat suggests the first one.', '/live', 'Open a board on Live')}
--</section>
--<section class="fu-section" id="fu-plans" aria-labelledby="fu-plans-h">
--<div class="fu-head-row"><h2 id="fu-plans-h">Plans, decided together</h2><a class="fu-more" href="/plans">All plans</a></div>
--${plans.length ? `<ol class="fu-plans">${plans.slice(0, 4).map(planCard).join('')}</ol>` : empty('No group plan is public yet. Start one and send the link to the group.', '/plans/new', 'Start a plan')}
--</section>
--<section class="fu-section" id="fu-guide-teaser" aria-labelledby="fu-guide-teaser-h">
++ // Real things first (owner, 10-31: empty sections made the rooms look unfinished). The local guide always has
++ // something; quests and plans lead once there are some, and until then share one short "be the first" row below.
++ const guide = `<section class="fu-section" id="fu-guide-teaser" aria-labelledby="fu-guide-teaser-h">
<div class="fu-head-row"><h2 id="fu-guide-teaser-h">What's fun near me?</h2><a class="fu-more" href="/guide">The local guide</a></div>
<p class="fu-lede">A city, a rainy afternoon or a cheap date: places from OpenStreetMap, local events from Ticketmaster when a key is configured, and a map link for every answer. No model call, no paid provider.</p>
++<p><a class="fu-cta" href="/guide">Find something near me</a></p>
</section>`;
++ const questsSec = featured.length ? `<section class="fu-section" id="fu-featured" aria-labelledby="fu-featured-h">
++<div class="fu-head-row"><h2 id="fu-featured-h">IRL quests on Live</h2><a class="fu-more" href="/quests">All quests</a></div>
++<ol class="fu-quests">${featured.slice(0, 6).map(questCard).join('')}</ol>
++</section>` : '';
++ const plansSec = plans.length ? `<section class="fu-section" id="fu-plans" aria-labelledby="fu-plans-h">
++<div class="fu-head-row"><h2 id="fu-plans-h">Plans, decided together</h2><a class="fu-more" href="/plans">All plans</a></div>
++<ol class="fu-plans">${plans.slice(0, 4).map(planCard).join('')}</ol>
++</section>` : '';
++ const first = [
++ !featured.length ? `<li><b>IRL quests on Live</b><span>A streamer opens a board and chat suggests the first one.</span><a href="/live">Open a board</a></li>` : '',
++ !plans.length ? `<li><b>Plans, decided together</b><span>Start one and send the link to the group; the vote picks.</span><a href="/plans/new">Start a plan</a></li>` : '',
++ ].filter(Boolean).join('');
++ const beFirst = first ? `<section class="fu-section fu-first" aria-labelledby="fu-first-h"><h2 id="fu-first-h">Be the first</h2><ul class="fu-first-list">${first}</ul></section>` : '';
++ const live = `${questsSec}${plansSec}${guide}${beFirst}`;
const room = sc.room({
room: 'fun',
hero: {
ddiff --git a/test/home-real-first.test.js b/test/home-real-first.test.js
nnew file mode 100644
iindex 0000000..afffe6a
---- /dev/null
++++ b/test/home-real-first.test.js
@@@ -0,0 +1,22 @@
++'use strict';
++// Fun's home leads with what is real (owner, 10-31: empty sections made the rooms look unfinished). With no public
++// quest or plan yet, the local guide leads and one short "Be the first" row sits below it; once there are some, they
++// lead in their own sections and the row says only what is still missing.
++const assert = require('assert');
++const fs = require('fs');
++const path = require('path');
++const views = require('../server/views');
++
++const src = fs.readFileSync(path.join(__dirname, '..', 'server/views/index.js'), 'utf8');
++assert.match(src, /const live = `\$\{questsSec\}\$\{plansSec\}\$\{guide\}\$\{beFirst\}`;/, 'the order: quests, plans, the guide, then what is still missing');
++
++const empty = views.home({ counts: {}, featured: [], plans: [] });
++assert.ok(!empty.includes('id="fu-featured"') && !empty.includes('id="fu-plans"'), 'no empty sections');
++assert.ok(empty.indexOf('id="fu-guide-teaser"') < empty.indexOf('Be the first'), 'the guide leads, the row follows');
++assert.ok(empty.includes('href="/live">Open a board</a>') && empty.includes('href="/plans/new">Start a plan</a>'), 'each missing thing says how to start it');
++
++const quest = { slug: 'bench', title: 'Find the best bench with a view', status: 'accepted', category: 'outdoors', votes: 12 };
++const some = views.home({ counts: { quests: 1 }, featured: [quest], plans: [] });
++assert.ok(some.indexOf('id="fu-featured"') < some.indexOf('id="fu-guide-teaser"'), 'quests lead once there are some');
++assert.ok(some.includes('Be the first') && !some.includes('href="/live">Open a board</a>'), 'and the row keeps only what is still missing');
++console.log('fun home: real first, the guide leads when nothing is public, one "be the first" row for the rest');