openvibers/openvibe.quest Commits

Empty states: the quest log's three empty states use the shared kit (#282)

OP OpenVibers committed 2 days ago · eeec9ba1ad · 2 files, +12 −4

Nothing started, no completed quests and no badges yet are kit.empty now, each with the next action
as a kit button (Pick a quest, Explore quests), and the log route loads kit.css so they are styled.

Checked: ov test test/quest-visual.test.js (a person with no progress sees the three empty states
and their buttons).

Changes

ddiff --git a/server/http/pages.js b/server/http/pages.js
iindex 27e72ea..b0f7abe 100644
---- a/server/http/pages.js
++++ b/server/http/pages.js
@@@ -508,10 +508,10 @@ each quest still needs, and the badges you have earned.</p>
return page(req, res, {
title: 'Your quest log',
crumbs: [{ label: 'Quests', href: '/' }, { label: 'Your quest log' }],
-- scripts: ['js/quest-visual.js'], localStyles: ['css/quest-visual.css'],
++ styles: KIT_STYLES, scripts: ['js/quest-visual.js'], localStyles: ['css/quest-visual.css'],
body: html`<div class="q-log"><header class="q-log-hero"><div><p class="q-eyebrow">YOUR JOURNEY ACROSS OPENVIBE</p><h1>Your quest log</h1><p>Signed in as <strong>${req.viewer.displayName || req.viewer.username || 'you'}</strong>. One log across every OpenVibe site, showing what you have done and what each quest still needs.</p></div><div class="q-log-stats"><div class="q-streak"><span class="q-flame" style="--streak:${Math.min(daily.streak || 0, 30)}" aria-hidden="true">🔥</span><strong>${daily.streak || 0}</strong><span>day streak</span></div><div class="q-today-ring" style="--progress:${daily.progress || 0}"><strong>${daily.progress || 0}<small>/3</small></strong><span>today's quest</span></div><div class="q-week-coins">${icons.glyph('coin', 24)}<strong>${Number(creditedWeek) || 0}</strong><span>OpenCoins this week</span></div></div></header>
<div class="q-log-columns"><section class="q-log-work" aria-labelledby="q-in-progress"><div class="q-section-head"><div><p class="q-eyebrow">KEEP GOING</p><h2 id="q-in-progress">In progress</h2></div><span>${started.length} active</span></div>
--${started.length ? html`<ul class="q-progress-list">${started.map((q) => html`<li class="q-progress-card"><div class="q-progress-head"><span class="q-quest-tile" style="--h:${quests.hueOf(q.id)}">${icons.glyph(icons.quest(q), 30)}</span><div><a href="/quests/${q.id}" class="q-progress-title">${q.title}</a><span class="q-site-chip">${icons.glyph(icons.site(q.site), 16)}${siteName(q.site)}</span></div>${quests.stateChip(false)}</div><p>${q.why}</p><ul class="q-progress-steps">${q.steps.map((step, i) => { const value = Math.min((seen.get(q.id) || new Map()).get(i) || 0, step.count); return html`<li>${icons.glyph(icons.topic(step.event_type, q.site), 18)}<span>${step.label}<progress max="${step.count}" value="${value}" role="progressbar" aria-label="${step.label}" aria-valuemin="0" aria-valuemax="${step.count}" aria-valuenow="${value}" aria-valuetext="${value}/${step.count}${quests.unitOf(step) ? ` ${quests.unitOf(step)}` : ''}">${value}/${step.count}${quests.unitOf(step) ? ` ${quests.unitOf(step)}` : ''}</progress></span><b>${value >= step.count ? '✓' : `${value}/${step.count}${quests.unitOf(step) ? ` ${quests.unitOf(step)}` : ''}`}</b></li>`; })}</ul><div class="q-progress-reward">${quests.medal(q)}<span>Earn the <strong>${q.reward.badge.name}</strong> badge${q.reward.coins > 0 ? html` · ${q.reward.coins} OpenCoins${coinsUi.coinsEnabled ? '' : ' when enabled'}` : ''}</span></div></li>`)}</ul>` : html`<div class="q-empty"><span class="q-quest-tile">${icons.glyph('spark', 30)}</span><p>Nothing started yet. <a href="/quests">Pick a quest</a> — any of the sites you already use counts.</p></div>`}
++${started.length ? html`<ul class="q-progress-list">${started.map((q) => html`<li class="q-progress-card"><div class="q-progress-head"><span class="q-quest-tile" style="--h:${quests.hueOf(q.id)}">${icons.glyph(icons.quest(q), 30)}</span><div><a href="/quests/${q.id}" class="q-progress-title">${q.title}</a><span class="q-site-chip">${icons.glyph(icons.site(q.site), 16)}${siteName(q.site)}</span></div>${quests.stateChip(false)}</div><p>${q.why}</p><ul class="q-progress-steps">${q.steps.map((step, i) => { const value = Math.min((seen.get(q.id) || new Map()).get(i) || 0, step.count); return html`<li>${icons.glyph(icons.topic(step.event_type, q.site), 18)}<span>${step.label}<progress max="${step.count}" value="${value}" role="progressbar" aria-label="${step.label}" aria-valuemin="0" aria-valuemax="${step.count}" aria-valuenow="${value}" aria-valuetext="${value}/${step.count}${quests.unitOf(step) ? ` ${quests.unitOf(step)}` : ''}">${value}/${step.count}${quests.unitOf(step) ? ` ${quests.unitOf(step)}` : ''}</progress></span><b>${value >= step.count ? '✓' : `${value}/${step.count}${quests.unitOf(step) ? ` ${quests.unitOf(step)}` : ''}`}</b></li>`; })}</ul><div class="q-progress-reward">${quests.medal(q)}<span>Earn the <strong>${q.reward.badge.name}</strong> badge${q.reward.coins > 0 ? html` · ${q.reward.coins} OpenCoins${coinsUi.coinsEnabled ? '' : ' when enabled'}` : ''}</span></div></li>`)}</ul>` : raw(kit.empty({ icon: 'spark', title: 'Nothing started yet', text: 'Any of the sites you already use counts: pick a quest and its steps begin.', action: { href: '/quests', label: 'Pick a quest', icon: 'spark' } }))}
<details class="q-completed-fold"><summary><span>Done</span><strong>${finished.length}</strong><small>completed quests</small></summary>${finished.length ? html`<ul class="quest-list">${finished.map((q) => {
const c = done.get(q.id);
const item = q.reward.item ? (c.item_state === 'granted' ? html`${q.reward.item.emoji} <strong>${q.reward.item.name}</strong> is in <a href="https://inventory.openvibe.network/me">your inventory</a>. ` : c.item_state === 'failed' ? html`${q.reward.item.name} could not be given yet — this is recorded and retried. ` : html`${q.reward.item.name} is on its way to your inventory. `) : '';
@@@ -523,10 +523,10 @@ ${started.length ? html`<ul class="q-progress-list">${started.map((q) => html`<l
<p class="quest-earned">Completed <time datetime="${c.completed_at}">${String(c.completed_at).slice(0, 10)}</time> · Badge: <strong>${q.reward.badge.name}</strong></p>
${item || coins ? html`<p class="quest-why">${item}${coins}</p>` : ''}
</div></li>`;
-- })}</ul>` : html`<p class="muted">No quests completed yet.</p>`}</details>
++ })}</ul>` : raw(kit.empty({ icon: 'check', title: 'No quests completed yet', text: 'Finish a quest and it moves here with its badge.', action: { href: '/quests', label: 'Explore quests', icon: 'spark' } }))}</details>
<p class="q-more-quests"><a href="/quests">Explore quests you have not started ${icons.glyph('arrow-right', 16)}</a></p>
</section><aside class="q-log-badges" aria-labelledby="q-your-badges"><div class="q-section-head"><div><p class="q-eyebrow">THE COLLECTION</p><h2 id="q-your-badges">Your badges</h2></div><span>${badges.length} earned</span></div><p>Each badge marks a quest you finished. Tap one for its story and a link to share it.</p>
--${badges.length ? html`<ul class="q-badge-grid">${badges.map((b) => badgeTile(b))}</ul>` : html`<p class="muted">No badges yet. Finish a quest to earn your first.</p>`}
++${badges.length ? html`<ul class="q-badge-grid">${badges.map((b) => badgeTile(b))}</ul>` : raw(kit.empty({ icon: 'spark', title: 'No badges yet', text: 'Finish a quest to earn your first badge.', action: { href: '/quests', label: 'Pick a quest', icon: 'spark' } }))}
<details class="q-locked-fold"><summary>${locked.length} badges still to earn</summary><ul class="q-badge-grid">${locked.map((q) => badgeTile(q, true))}</ul></details>
<p class="q-badge-privacy">Anyone can see which badges you earned and when (<a href="/api/v1/profiles/${subject}/badges">your public badge list</a>). Your progress stays private.</p></aside></div>
<div class="q-log-foot">${quests.coinsNote(coinsUi)}${coinsUi.coinsEnabled ? html`<p class="muted">Credited today: <strong>${creditedToday}</strong> of ${coinsUi.dailyCap} OpenCoins.</p>` : ''}</div>${yourStuff(req)}</div>`,
ddiff --git a/test/quest-visual.test.js b/test/quest-visual.test.js
iindex c1d2845..c104290 100644
---- a/test/quest-visual.test.js
++++ b/test/quest-visual.test.js
@@@ -29,6 +29,14 @@ const { createGlyphDraftClient, nameForBadge, nameForDaily } = require('../serve
assert.ok(index.text.includes('Quests by site') && index.text.includes('q-site-chip'));
assert.ok(requests.some((x) => /^daily-\d{4}-\d{2}-\d{2}$/.test(x.name)), 'a unique daily drawing joins review');
});
++ await check('an empty quest log is a kit empty state with its next button', async () => {
++ const fresh = t.network.addUser('newbie');
++ const me = await t.get('/me', { as: fresh });
++ assert.equal(me.status, 200);
++ assert.ok(me.text.includes('ovk-empty'), 'the shared kit empty state');
++ for (const word of ['Nothing started yet', 'No quests completed yet', 'No badges yet']) assert.ok(me.text.includes(word), word);
++ assert.ok(/ovk-btn[^>]*href="\/quests"/.test(me.text), 'the next action is a kit button');
++ });
await check('only approved drawings replace the bundled glyph fallback', async () => {
let me = await t.get('/me', { as: person });
assert.ok(me.text.includes('data-icon="live"'), 'a bundled icon appears before review');