openvibers/openvibe.quest Commits
Quest's home says what you get in plain words (no repeated name, buttons side by side, numbers with icons) and its bar is tidied (#110)
Changes
@@@ -80,16 +80,17 @@ function createPageRoutes(ctx) {const countText = (n) => n.toLocaleString('en-US');/** The figures under the hero: each one only when it has something to say. */-- const statsStrip = (p) => {-- const items = [-- [catalog.all().length, `quests on ${siteCount} sites`],-- p.completions > 0 && [p.completions, p.completions === 1 ? 'quest completed' : 'quests completed'],-- p.people > 0 && [p.people, p.people === 1 ? 'person has earned badges' : 'people have earned badges'],-- p.recent > 0 && [p.recent, 'completed in the last 7 days'],-- coinsUi.coinsEnabled && p.coins > 0 && [p.coins, 'OpenCoins credited'],-- ].filter(Boolean);-- return html`<ul class="q-stats" aria-label="${SITE_NAME} in numbers">${items.map(([n, label]) => html`<li><b>${countText(n)}</b><span>${label}</span></li>`)}</ul>`;-- };++ // The numbers under the hero: the shared strip (three across on a phone), each with its icon.++ const statsStrip = (p) => raw(showcase.stats({++ title: `${SITE_NAME} in numbers`,++ items: [++ { value: countText(catalog.all().length), label: `quests on ${siteCount} sites`, icon: 'ov:quest' },++ p.completions > 0 && { value: countText(p.completions), label: p.completions === 1 ? 'quest finished' : 'quests finished', icon: 'ov:check' },++ p.people > 0 && { value: countText(p.people), label: p.people === 1 ? 'person with badges' : 'people with badges', icon: 'ov:account' },++ p.recent > 0 && { value: countText(p.recent), label: 'finished this week', icon: 'ov:history' },++ coinsUi.coinsEnabled && p.coins > 0 && { value: countText(p.coins), label: 'OpenCoins given', icon: 'ov:tips' },++ ].filter(Boolean),++ }));/** The badges people hold most (all time), with the quest that pays each. */const topBadges = (p) => {@@@ -133,15 +134,16 @@ function createPageRoutes(ctx) {r.get('/', async (req, res) => {const [appList, p] = await Promise.all([ctx.appQuests ? ctx.appQuests.listed(24) : [], pulse()]);const site = String(config.baseUrl).replace(/\/+$/, '');++ // The navbar already says OpenVibe.Quest: the hero says what you get, in plain words, and where to start.const hero = showcase.hero({-- eyebrow: SITE_NAME,-- title: 'Quests across OpenVibe.',-- accent: 'Badges for what you really do.',-- lede: `Go live, start a thread, publish a world, share a picture: ${SITE_NAME} counts what you really do on ${siteCount} OpenVibe sites and rewards it with badges${coinsUi.coinsEnabled ? ' and OpenCoins' : ' (and OpenCoins, once they are switched on)'}. One quest log for the whole network, with nothing new to install or learn.`,++ eyebrow: { icon: 'ov:quest', text: 'Free, nothing to install' },++ title: 'Earn badges',++ accent: 'for what you already do on OpenVibe.',++ lede: `Go live, post in the forum, publish a world: every quest you finish on any of ${siteCount} OpenVibe sites earns a badge${coinsUi.coinsEnabled ? ' and OpenCoins' : ''}. One quest log follows you everywhere.`,actions: signedIn(req)-- ? [{ label: 'Your quest log', href: '/me', primary: true }, { label: 'See the quests', href: '#the-quests' }]-- : [{ label: 'Sign in to start', href: '/auth/login?next=%2Fme', primary: true }, { label: 'See the quests', href: '#the-quests' }],-- note: 'Open source (AGPL-3.0). Rewards are badges, items and OpenCoins: loyalty points, never money and never Vibes.',++ ? [{ label: 'Your quest log', href: '/me', primary: true, icon: 'ov:quest' }, { label: 'Browse quests', href: '#the-quests', icon: 'ov:search' }]++ : [{ label: 'Start earning', href: '/auth/login?next=%2Fme', primary: true, icon: 'ov:quest' }, { label: 'Browse quests', href: '#the-quests', icon: 'ov:search' }],++ note: 'Badges, items and OpenCoins: loyalty points, never money. Open source (AGPL-3.0).',});page(req, res, {index: true, cache: signedIn(req) ? null : PUBLIC_CACHE,@@@ -22,12 +22,12 @@ const NETWORK_URL = 'https://openvibe.network';const SITE_NAME = 'OpenVibe.Quest';const TAGLINE = 'Quests across the whole network.';const PUBLIC_DIR = path.join(__dirname, '..', '..', 'public');--// The product's own navigation: one entry per public page it serves.++// The product's own places: the main ones carry an icon (phones get them as the bottom tab bar), the rest fold into++// the navbar's More (Shared 4.22). Search is the navbar's magnifier and What shipped is the footer's.const NAV = [-- { label: 'Quests', href: '/' },-- { label: 'Your quest log', href: '/me' },-- { label: 'How it works', href: '/how-it-works' },-- { label: 'What shipped', href: '/updates' },++ { label: 'Quests', href: '/', icon: 'fa-star' },++ { label: 'Your quest log', href: '/me', icon: 'fa-list-check' },++ { label: 'How it works', href: '/how-it-works', icon: 'fa-circle-info', more: true },];const hashes = new Map();@@@ -58,7 +58,8 @@ function renderPage(o) {const nav = {service: 'quest',apiBase: NETWORK_URL,-- links: NAV.map((l) => ({ label: l.label, href: l.href, active: pathNow === l.href || pathNow.startsWith(`${l.href}/`) })),++ links: NAV.map((l) => ({ label: l.label, href: l.href, ...(l.icon ? { icon: l.icon } : {}), ...(l.more ? { more: true } : {}), active: pathNow === l.href || pathNow.startsWith(`${l.href}/`) })),++ appBar: [{ label: 'Quests', href: '/', icon: 'fa-star' }, { label: 'Quest log', href: '/me', icon: 'fa-list-check' }, { label: 'How it works', href: '/how-it-works', icon: 'fa-circle-info' }], // phones: the bottom tab bar (Shared 4.20)history: { type: 'page', title: o.title || SITE_NAME },silentLogin: `${o.config.baseUrl}/auth/login?silent=1&next={url}`,sessionUrl: '/auth/me',@@@ -21,7 +21,7 @@ const BUDGETS = {// Raised 2026-10-22 with the rich home: figures under the hero, the most-earned badges, each card's "earned by",// the badges nobody holds yet, the sites Quest follows, a FAQ and the quests as ItemList + FAQPage JSON-LD.// Measured 54.3 KB raw / 10.6 KB brotli on a fresh database.-- htmlRawKB: 66, // 61.2 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; measured 54.3 (the home page: hero, figures, badges, features, the quests, sites, FAQ, cta, JSON-LD)++ htmlRawKB: 69, // 67.5 the hero's and the numbers' icons (inline ring icons, 2026-10-28); 61.2 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; measured 54.3 (the home page: hero, figures, badges, features, the quests, sites, FAQ, cta, JSON-LD)htmlBrotliKB: 13, // 11.8 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 10.6jsFiles: 5, // 5 (theme-loader, web-runtime, navbar, footer, boost: openvibe-shared/shell)jsRawKB: 297, // 293 openvibe-shared 4.22.0: the navbar's magnifier search button and More dropdown (search.js and loading.js load only when used); 287.2 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 273.2 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 264.3 with Shared 4.13.0 (2026-10-26: account links); 239.8@@@ -133,7 +133,7 @@ const { render } = require('../server/render/html');// The live parts are counts, never people: kim's completion shows as a number and nothing about her.assert.ok(home.text.includes('Badges people earn most'), 'the most-earned badges');assert.ok(home.text.includes('Earned by 1 person'), 'a quest card says how many hold its badge');-- assert.ok(home.text.includes('<b>1</b><span>quest completed</span>'), 'the figures under the hero');++ assert.ok(/<dt>quest finished<\/dt><dd><span class="sc-stat-ic">[\s\S]*?<\/span>1<\/dd>/.test(home.text), 'the figures under the hero, each with its icon');assert.ok(home.text.includes('Nobody has these badges yet'), 'the quests nobody has finished');assert.ok(home.text.includes('"@type":"FAQPage"') && home.text.includes('Can other people see my progress?'), 'the FAQ and its JSON-LD');assert.ok(home.text.includes('"@type":"ItemList"'), 'the quests as an ItemList');@@@ -200,7 +200,7 @@ const { render } = require('../server/render/html');const home = await on.get('/');assert.strictEqual(home.status, 200);assert.ok(!home.text.includes('once they are switched on'), 'no promise of something already on');-- assert.ok(home.text.includes('rewards it with badges and OpenCoins'));++ assert.ok(home.text.includes('earns a badge and OpenCoins'));assert.ok(home.text.includes('OpenCoins into your OpenVibe.Network wallet'));});} finally { await on.close(); }
@@ -80,16 +80,17 @@ function createPageRoutes(ctx) {
@@ -80,16 +80,17 @@ function createPageRoutes(ctx) {
const countText = (n) => n.toLocaleString('en-US');
const countText = (n) => n.toLocaleString('en-US');
/** The figures under the hero: each one only when it has something to say. */
/** The figures under the hero: each one only when it has something to say. */
- const statsStrip = (p) => {
- const items = [
- [catalog.all().length, `quests on ${siteCount} sites`],
- p.completions > 0 && [p.completions, p.completions === 1 ? 'quest completed' : 'quests completed'],
- p.people > 0 && [p.people, p.people === 1 ? 'person has earned badges' : 'people have earned badges'],
- p.recent > 0 && [p.recent, 'completed in the last 7 days'],
- coinsUi.coinsEnabled && p.coins > 0 && [p.coins, 'OpenCoins credited'],
- ].filter(Boolean);
- return html`<ul class="q-stats" aria-label="${SITE_NAME} in numbers">${items.map(([n, label]) => html`<li><b>${countText(n)}</b><span>${label}</span></li>`)}</ul>`;
- };
+ // The numbers under the hero: the shared strip (three across on a phone), each with its icon.
+ const statsStrip = (p) => raw(showcase.stats({
+ title: `${SITE_NAME} in numbers`,
+ items: [
+ { value: countText(catalog.all().length), label: `quests on ${siteCount} sites`, icon: 'ov:quest' },
+ p.completions > 0 && { value: countText(p.completions), label: p.completions === 1 ? 'quest finished' : 'quests finished', icon: 'ov:check' },
+ p.people > 0 && { value: countText(p.people), label: p.people === 1 ? 'person with badges' : 'people with badges', icon: 'ov:account' },
+ p.recent > 0 && { value: countText(p.recent), label: 'finished this week', icon: 'ov:history' },
+ coinsUi.coinsEnabled && p.coins > 0 && { value: countText(p.coins), label: 'OpenCoins given', icon: 'ov:tips' },
+ ].filter(Boolean),
+ }));
/** The badges people hold most (all time), with the quest that pays each. */
/** The badges people hold most (all time), with the quest that pays each. */
const topBadges = (p) => {
const topBadges = (p) => {
@@ -133,15 +134,16 @@ function createPageRoutes(ctx) {
@@ -133,15 +134,16 @@ function createPageRoutes(ctx) {
r.get('/', async (req, res) => {
r.get('/', async (req, res) => {
const [appList, p] = await Promise.all([ctx.appQuests ? ctx.appQuests.listed(24) : [], pulse()]);
const [appList, p] = await Promise.all([ctx.appQuests ? ctx.appQuests.listed(24) : [], pulse()]);
const site = String(config.baseUrl).replace(/\/+$/, '');
const site = String(config.baseUrl).replace(/\/+$/, '');
+ // The navbar already says OpenVibe.Quest: the hero says what you get, in plain words, and where to start.
const hero = showcase.hero({
const hero = showcase.hero({
- eyebrow: SITE_NAME,
- title: 'Quests across OpenVibe.',
- accent: 'Badges for what you really do.',
- lede: `Go live, start a thread, publish a world, share a picture: ${SITE_NAME} counts what you really do on ${siteCount} OpenVibe sites and rewards it with badges${coinsUi.coinsEnabled ? ' and OpenCoins' : ' (and OpenCoins, once they are switched on)'}. One quest log for the whole network, with nothing new to install or learn.`,
+ eyebrow: { icon: 'ov:quest', text: 'Free, nothing to install' },
+ title: 'Earn badges',
+ accent: 'for what you already do on OpenVibe.',
+ lede: `Go live, post in the forum, publish a world: every quest you finish on any of ${siteCount} OpenVibe sites earns a badge${coinsUi.coinsEnabled ? ' and OpenCoins' : ''}. One quest log follows you everywhere.`,
actions: signedIn(req)
actions: signedIn(req)
- ? [{ label: 'Your quest log', href: '/me', primary: true }, { label: 'See the quests', href: '#the-quests' }]
- : [{ label: 'Sign in to start', href: '/auth/login?next=%2Fme', primary: true }, { label: 'See the quests', href: '#the-quests' }],
- note: 'Open source (AGPL-3.0). Rewards are badges, items and OpenCoins: loyalty points, never money and never Vibes.',
+ ? [{ label: 'Your quest log', href: '/me', primary: true, icon: 'ov:quest' }, { label: 'Browse quests', href: '#the-quests', icon: 'ov:search' }]
+ : [{ label: 'Start earning', href: '/auth/login?next=%2Fme', primary: true, icon: 'ov:quest' }, { label: 'Browse quests', href: '#the-quests', icon: 'ov:search' }],
+ note: 'Badges, items and OpenCoins: loyalty points, never money. Open source (AGPL-3.0).',
});
});
page(req, res, {
page(req, res, {
index: true, cache: signedIn(req) ? null : PUBLIC_CACHE,
index: true, cache: signedIn(req) ? null : PUBLIC_CACHE,
@@ -22,12 +22,12 @@ const NETWORK_URL = 'https://openvibe.network';
@@ -22,12 +22,12 @@ const NETWORK_URL = 'https://openvibe.network';
const SITE_NAME = 'OpenVibe.Quest';
const SITE_NAME = 'OpenVibe.Quest';
const TAGLINE = 'Quests across the whole network.';
const TAGLINE = 'Quests across the whole network.';
const PUBLIC_DIR = path.join(__dirname, '..', '..', 'public');
const PUBLIC_DIR = path.join(__dirname, '..', '..', 'public');
-// The product's own navigation: one entry per public page it serves.
+// The product's own places: the main ones carry an icon (phones get them as the bottom tab bar), the rest fold into
+// the navbar's More (Shared 4.22). Search is the navbar's magnifier and What shipped is the footer's.
const NAV = [
const NAV = [
- { label: 'Quests', href: '/' },
- { label: 'Your quest log', href: '/me' },
- { label: 'How it works', href: '/how-it-works' },
- { label: 'What shipped', href: '/updates' },
+ { label: 'Quests', href: '/', icon: 'fa-star' },
+ { label: 'Your quest log', href: '/me', icon: 'fa-list-check' },
+ { label: 'How it works', href: '/how-it-works', icon: 'fa-circle-info', more: true },
];
];
const hashes = new Map();
const hashes = new Map();
@@ -58,7 +58,8 @@ function renderPage(o) {
@@ -58,7 +58,8 @@ function renderPage(o) {
const nav = {
const nav = {
service: 'quest',
service: 'quest',
apiBase: NETWORK_URL,
apiBase: NETWORK_URL,
- links: NAV.map((l) => ({ label: l.label, href: l.href, active: pathNow === l.href || pathNow.startsWith(`${l.href}/`) })),
+ links: NAV.map((l) => ({ label: l.label, href: l.href, ...(l.icon ? { icon: l.icon } : {}), ...(l.more ? { more: true } : {}), active: pathNow === l.href || pathNow.startsWith(`${l.href}/`) })),
+ appBar: [{ label: 'Quests', href: '/', icon: 'fa-star' }, { label: 'Quest log', href: '/me', icon: 'fa-list-check' }, { label: 'How it works', href: '/how-it-works', icon: 'fa-circle-info' }], // phones: the bottom tab bar (Shared 4.20)
history: { type: 'page', title: o.title || SITE_NAME },
history: { type: 'page', title: o.title || SITE_NAME },
silentLogin: `${o.config.baseUrl}/auth/login?silent=1&next={url}`,
silentLogin: `${o.config.baseUrl}/auth/login?silent=1&next={url}`,
sessionUrl: '/auth/me',
sessionUrl: '/auth/me',
@@ -21,7 +21,7 @@ const BUDGETS = {
@@ -21,7 +21,7 @@ const BUDGETS = {
// Raised 2026-10-22 with the rich home: figures under the hero, the most-earned badges, each card's "earned by",
// Raised 2026-10-22 with the rich home: figures under the hero, the most-earned badges, each card's "earned by",
// the badges nobody holds yet, the sites Quest follows, a FAQ and the quests as ItemList + FAQPage JSON-LD.
// the badges nobody holds yet, the sites Quest follows, a FAQ and the quests as ItemList + FAQPage JSON-LD.
// Measured 54.3 KB raw / 10.6 KB brotli on a fresh database.
// Measured 54.3 KB raw / 10.6 KB brotli on a fresh database.
- htmlRawKB: 66, // 61.2 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; measured 54.3 (the home page: hero, figures, badges, features, the quests, sites, FAQ, cta, JSON-LD)
+ htmlRawKB: 69, // 67.5 the hero's and the numbers' icons (inline ring icons, 2026-10-28); 61.2 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; measured 54.3 (the home page: hero, figures, badges, features, the quests, sites, FAQ, cta, JSON-LD)
htmlBrotliKB: 13, // 11.8 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 10.6
htmlBrotliKB: 13, // 11.8 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 10.6
jsFiles: 5, // 5 (theme-loader, web-runtime, navbar, footer, boost: openvibe-shared/shell)
jsFiles: 5, // 5 (theme-loader, web-runtime, navbar, footer, boost: openvibe-shared/shell)
jsRawKB: 297, // 293 openvibe-shared 4.22.0: the navbar's magnifier search button and More dropdown (search.js and loading.js load only when used); 287.2 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 273.2 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 264.3 with Shared 4.13.0 (2026-10-26: account links); 239.8
jsRawKB: 297, // 293 openvibe-shared 4.22.0: the navbar's magnifier search button and More dropdown (search.js and loading.js load only when used); 287.2 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 273.2 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 264.3 with Shared 4.13.0 (2026-10-26: account links); 239.8
@@ -133,7 +133,7 @@ const { render } = require('../server/render/html');
@@ -133,7 +133,7 @@ const { render } = require('../server/render/html');
// The live parts are counts, never people: kim's completion shows as a number and nothing about her.
// The live parts are counts, never people: kim's completion shows as a number and nothing about her.
assert.ok(home.text.includes('Badges people earn most'), 'the most-earned badges');
assert.ok(home.text.includes('Badges people earn most'), 'the most-earned badges');
assert.ok(home.text.includes('Earned by 1 person'), 'a quest card says how many hold its badge');
assert.ok(home.text.includes('Earned by 1 person'), 'a quest card says how many hold its badge');
- assert.ok(home.text.includes('<b>1</b><span>quest completed</span>'), 'the figures under the hero');
+ assert.ok(/<dt>quest finished<\/dt><dd><span class="sc-stat-ic">[\s\S]*?<\/span>1<\/dd>/.test(home.text), 'the figures under the hero, each with its icon');
assert.ok(home.text.includes('Nobody has these badges yet'), 'the quests nobody has finished');
assert.ok(home.text.includes('Nobody has these badges yet'), 'the quests nobody has finished');
assert.ok(home.text.includes('"@type":"FAQPage"') && home.text.includes('Can other people see my progress?'), 'the FAQ and its JSON-LD');
assert.ok(home.text.includes('"@type":"FAQPage"') && home.text.includes('Can other people see my progress?'), 'the FAQ and its JSON-LD');
assert.ok(home.text.includes('"@type":"ItemList"'), 'the quests as an ItemList');
assert.ok(home.text.includes('"@type":"ItemList"'), 'the quests as an ItemList');
@@ -200,7 +200,7 @@ const { render } = require('../server/render/html');
@@ -200,7 +200,7 @@ const { render } = require('../server/render/html');
const home = await on.get('/');
const home = await on.get('/');
assert.strictEqual(home.status, 200);
assert.strictEqual(home.status, 200);
assert.ok(!home.text.includes('once they are switched on'), 'no promise of something already on');
assert.ok(!home.text.includes('once they are switched on'), 'no promise of something already on');
- assert.ok(home.text.includes('rewards it with badges and OpenCoins'));
+ assert.ok(home.text.includes('earns a badge and OpenCoins'));
assert.ok(home.text.includes('OpenCoins into your OpenVibe.Network wallet'));
assert.ok(home.text.includes('OpenCoins into your OpenVibe.Network wallet'));
});
});
} finally { await on.close(); }
} finally { await on.close(); }