openvibers/openvibe.fun Commits
Fun's home leads with what is real; one 'Be the first' row instead of empty sections (#27)
Changes
@@@ -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); }@@@ -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: {nnew file mode 100644@@@ -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');
@@ -112,3 +112,11 @@ html.ov-motion .fu-flip-card { animation: fu-flip .8s cubic-bezier(.2,.7,.2,1) .
@@ -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 (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; } }
@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; }
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); }
@@ -63,18 +63,27 @@ function cityMotif() {
@@ -63,18 +63,27 @@ function cityMotif() {
}
}
function home({ counts = {}, featured = [], plans = [] } = {}) {
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>
<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 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>`;
</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({
const room = sc.room({
room: 'fun',
room: 'fun',
hero: {
hero: {
new file mode 100644
new file mode 100644
@@ -0,0 +1,22 @@
@@ -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');