openvibers/openvibe.tips Commits
Empty states: no goals, no tips and no moderators use the shared kit (#262)
Changes
@@@ -88,6 +88,7 @@ function createLayout({ config, release }) {appIcon.headTags({ site: 'tips' }),release ? release.metaTag() : '',`<link rel="stylesheet" href="${asset('css/tips.css')}">`,++ `<link rel="stylesheet" href="${ovServe.url('kit.css')}">`,// openvibe-shared stylesheets a page asks for by name (the home's showcase.css)....(o.styles || []).map((name) => `<link rel="stylesheet" href="${ovServe.url(name)}">`),`<meta name="ov-boost" content="tips@${esc(RELEASE)}">`,@@@ -6,6 +6,7 @@* the creator's own headline is the only pitch on their page.*/const showcase = require('openvibe-shared/showcase');++const kit = require('openvibe-shared/kit');const glyphs = require('openvibe-shared/glyphs');const { esc } = require('./layout');const { VOICES } = require('../domain/profiles');@@@ -290,7 +291,7 @@ function postsAdmin({ posts = [], tiers = [], csrf, idem }) {+ '<form class="inline" method="post" action="/dashboard/posts/' + esc(p.id) + '/' + (p.status === 'hidden' ? 'unhide' : 'hide') + '">'+ hidden({ csrf, idem: idem + '-post-' + p.id })+ '<button class="btn ghost" type="submit">' + (p.status === 'hidden' ? 'Show again' : 'Hide') + '</button></form>'-- + '</article>').join('') : '<p class="muted small">No posts yet.</p>';++ + '</article>').join('') : kit.empty({ icon: 'file', title: 'No posts yet', text: 'Write a members-only post above and it appears here.' });return '<section class="card"><h2>Members-only posts</h2>'+ '<p class="muted small">A post is written for the members of one or more of your tiers. Only a member of that tier reads the text; everyone else sees the title and a way to join.</p>'+ composer + mine + '</section>';@@@ -313,7 +314,7 @@ ${supportLinksSection(profile)}`;function goalsPage({ profile, goals }) {return `<h1>${esc(profile.display_name)} — goals</h1><p><a href="/@${esc(profile.handle)}">Back to ${esc(profile.display_name)}</a></p>--${goals.length ? goals.map(goalCard).join('') : '<p>No goals yet.</p>'}++${goals.length ? goals.map(goalCard).join('') : kit.empty({ icon: 'flag', title: 'No goals yet', text: 'When this creator sets a goal it appears here, with what it pays for and how close it is.' })}<p class="fine">Goal totals count settled payments only.${profile.page.goal_supporters ? ' Supporters are listed as they chose to be shown.' : ''}</p>`;}@@@ -346,7 +347,7 @@ function privacyLine(p) {function receiptsPage({ rows, next, flash }) {return `<h1>Your receipts</h1>${flash ? `<p class="notice">${esc(flash)}</p>` : ''}--${rows.length ? `<table class="list"><thead><tr><th>When</th><th>Creator</th><th>What</th><th class="num">Vibes</th><th>Payment</th><th>Delivery</th></tr></thead><tbody>${rows.map((r) => receiptRow(r, 'supporter')).join('')}</tbody></table>` : '<p>No tips yet.</p>'}++${rows.length ? `<table class="list"><thead><tr><th>When</th><th>Creator</th><th>What</th><th class="num">Vibes</th><th>Payment</th><th>Delivery</th></tr></thead><tbody>${rows.map((r) => receiptRow(r, 'supporter')).join('')}</tbody></table>` : kit.empty({ icon: 'coin', title: 'No tips yet', text: 'Tips you send on OpenVibe.Tips appear here with their receipt.', action: { href: '/', label: 'Find a creator', icon: 'search' } })}${next ? `<p><a href="/receipts?cursor=${esc(next)}">Older</a></p>` : ''}<section class="card"><h2>Your data</h2><p><a class="btn ghost" href="/receipts/export">Download your tips (JSON)</a></p>@@@ -536,7 +537,7 @@ function moderationCard({ p, csrf, idem, mod }) {<button class="btn ghost" type="submit">Save the filter</button></form><h3>Moderators</h3>-- ${moderators.length ? `<ul class="plain">${moderators.map((m) => `<li>${esc(m.name || m.moderator.id)} <small>since ${when(m.added_at)}</small> <form class="inline" method="post" action="/dashboard/moderators/${esc(m.moderator.id)}/remove">${hidden({ csrf, idem: `${idem}-m-${m.moderator.id}` })}<button class="btn ghost" type="submit">Remove</button></form></li>`).join('')}</ul>` : '<p>No moderators yet.</p>'}++ ${moderators.length ? `<ul class="plain">${moderators.map((m) => `<li>${esc(m.name || m.moderator.id)} <small>since ${when(m.added_at)}</small> <form class="inline" method="post" action="/dashboard/moderators/${esc(m.moderator.id)}/remove">${hidden({ csrf, idem: `${idem}-m-${m.moderator.id}` })}<button class="btn ghost" type="submit">Remove</button></form></li>`).join('')}</ul>` : kit.empty({ icon: 'shield', title: 'No moderators yet', text: 'Invite someone below and they can help with paid messages.' })}${invites.length ? `<p class="fine">Open invitations: ${invites.map((x) => `created ${when(x.created_at)}, expires ${when(x.expires_at)} <form class="inline" method="post" action="/dashboard/moderator-invites/${esc(x.id)}/revoke">${hidden({ csrf, idem: `${idem}-ri-${x.id}` })}<button class="btn ghost" type="submit">Revoke</button></form>`).join(' · ')}</p>` : ''}<form method="post" action="/dashboard/moderator-invites">${hidden({ csrf, idem: `${idem}-i` })}<button class="btn ghost" type="submit">Create a moderator invitation link</button></form>${mod.log && mod.log.length ? `<details><summary>Latest moderation</summary><table class="list"><thead><tr><th>When</th><th>What</th><th>By</th><th>Receipt</th></tr></thead><tbody>${mod.log.map((l) => `<tr><td>${when(l.created_at)}</td><td>${esc(l.action)}</td><td>${esc(ROLE_LABEL[l.by_role] || l.by_role)}</td><td><a href="/receipts/${esc(l.interaction_id)}"><code>${esc(l.interaction_id.slice(-8))}</code></a></td></tr>`).join('')}</tbody></table></details>` : ''}nnew file mode 100644@@@ -0,0 +1,18 @@++'use strict';++// Empty pages on OpenVibe.Tips are the shared kit's empty state, with the next action as a kit button.++const assert = require('assert');++const views = require('../server/web/pages');++++const goals = views.goalsPage({ profile: { display_name: 'Alma', handle: 'alma', page: {} }, goals: [] });++assert.ok(goals.includes('class="ovk-empty"'), 'no goals is a kit empty state');++assert.ok(goals.includes('No goals yet'), goals.slice(0, 200));++++const receipts = views.receiptsPage({ rows: [], next: null, flash: null });++assert.ok(receipts.includes('class="ovk-empty"'), 'no tips is a kit empty state');++assert.ok(receipts.includes('No tips yet'));++assert.ok(/class="ovk-btn[^"]*"[^>]*href="\/"/.test(receipts), 'Find a creator is a kit button');++++const layout = require('fs').readFileSync(require('path').join(__dirname, '..', 'server', 'web', 'layout.js'), 'utf8');++assert.ok(layout.includes("ovServe.url('kit.css')"), 'the shell loads kit.css');++++console.log('empty states: Tips goals and receipts use kit.empty');
@@ -88,6 +88,7 @@ function createLayout({ config, release }) {
@@ -88,6 +88,7 @@ function createLayout({ config, release }) {
appIcon.headTags({ site: 'tips' }),
appIcon.headTags({ site: 'tips' }),
release ? release.metaTag() : '',
release ? release.metaTag() : '',
`<link rel="stylesheet" href="${asset('css/tips.css')}">`,
`<link rel="stylesheet" href="${asset('css/tips.css')}">`,
+ `<link rel="stylesheet" href="${ovServe.url('kit.css')}">`,
// openvibe-shared stylesheets a page asks for by name (the home's showcase.css).
// openvibe-shared stylesheets a page asks for by name (the home's showcase.css).
...(o.styles || []).map((name) => `<link rel="stylesheet" href="${ovServe.url(name)}">`),
...(o.styles || []).map((name) => `<link rel="stylesheet" href="${ovServe.url(name)}">`),
`<meta name="ov-boost" content="tips@${esc(RELEASE)}">`,
`<meta name="ov-boost" content="tips@${esc(RELEASE)}">`,
@@ -6,6 +6,7 @@
@@ -6,6 +6,7 @@
* the creator's own headline is the only pitch on their page.
* the creator's own headline is the only pitch on their page.
*/
*/
const showcase = require('openvibe-shared/showcase');
const showcase = require('openvibe-shared/showcase');
+const kit = require('openvibe-shared/kit');
const glyphs = require('openvibe-shared/glyphs');
const glyphs = require('openvibe-shared/glyphs');
const { esc } = require('./layout');
const { esc } = require('./layout');
const { VOICES } = require('../domain/profiles');
const { VOICES } = require('../domain/profiles');
@@ -290,7 +291,7 @@ function postsAdmin({ posts = [], tiers = [], csrf, idem }) {
@@ -290,7 +291,7 @@ function postsAdmin({ posts = [], tiers = [], csrf, idem }) {
+ '<form class="inline" method="post" action="/dashboard/posts/' + esc(p.id) + '/' + (p.status === 'hidden' ? 'unhide' : 'hide') + '">'
+ '<form class="inline" method="post" action="/dashboard/posts/' + esc(p.id) + '/' + (p.status === 'hidden' ? 'unhide' : 'hide') + '">'
+ hidden({ csrf, idem: idem + '-post-' + p.id })
+ hidden({ csrf, idem: idem + '-post-' + p.id })
+ '<button class="btn ghost" type="submit">' + (p.status === 'hidden' ? 'Show again' : 'Hide') + '</button></form>'
+ '<button class="btn ghost" type="submit">' + (p.status === 'hidden' ? 'Show again' : 'Hide') + '</button></form>'
- + '</article>').join('') : '<p class="muted small">No posts yet.</p>';
+ + '</article>').join('') : kit.empty({ icon: 'file', title: 'No posts yet', text: 'Write a members-only post above and it appears here.' });
return '<section class="card"><h2>Members-only posts</h2>'
return '<section class="card"><h2>Members-only posts</h2>'
+ '<p class="muted small">A post is written for the members of one or more of your tiers. Only a member of that tier reads the text; everyone else sees the title and a way to join.</p>'
+ '<p class="muted small">A post is written for the members of one or more of your tiers. Only a member of that tier reads the text; everyone else sees the title and a way to join.</p>'
+ composer + mine + '</section>';
+ composer + mine + '</section>';
@@ -313,7 +314,7 @@ ${supportLinksSection(profile)}`;
@@ -313,7 +314,7 @@ ${supportLinksSection(profile)}`;
function goalsPage({ profile, goals }) {
function goalsPage({ profile, goals }) {
return `<h1>${esc(profile.display_name)} — goals</h1>
return `<h1>${esc(profile.display_name)} — goals</h1>
<p><a href="/@${esc(profile.handle)}">Back to ${esc(profile.display_name)}</a></p>
<p><a href="/@${esc(profile.handle)}">Back to ${esc(profile.display_name)}</a></p>
-${goals.length ? goals.map(goalCard).join('') : '<p>No goals yet.</p>'}
+${goals.length ? goals.map(goalCard).join('') : kit.empty({ icon: 'flag', title: 'No goals yet', text: 'When this creator sets a goal it appears here, with what it pays for and how close it is.' })}
<p class="fine">Goal totals count settled payments only.${profile.page.goal_supporters ? ' Supporters are listed as they chose to be shown.' : ''}</p>`;
<p class="fine">Goal totals count settled payments only.${profile.page.goal_supporters ? ' Supporters are listed as they chose to be shown.' : ''}</p>`;
}
}
@@ -346,7 +347,7 @@ function privacyLine(p) {
@@ -346,7 +347,7 @@ function privacyLine(p) {
function receiptsPage({ rows, next, flash }) {
function receiptsPage({ rows, next, flash }) {
return `<h1>Your receipts</h1>
return `<h1>Your receipts</h1>
${flash ? `<p class="notice">${esc(flash)}</p>` : ''}
${flash ? `<p class="notice">${esc(flash)}</p>` : ''}
-${rows.length ? `<table class="list"><thead><tr><th>When</th><th>Creator</th><th>What</th><th class="num">Vibes</th><th>Payment</th><th>Delivery</th></tr></thead><tbody>${rows.map((r) => receiptRow(r, 'supporter')).join('')}</tbody></table>` : '<p>No tips yet.</p>'}
+${rows.length ? `<table class="list"><thead><tr><th>When</th><th>Creator</th><th>What</th><th class="num">Vibes</th><th>Payment</th><th>Delivery</th></tr></thead><tbody>${rows.map((r) => receiptRow(r, 'supporter')).join('')}</tbody></table>` : kit.empty({ icon: 'coin', title: 'No tips yet', text: 'Tips you send on OpenVibe.Tips appear here with their receipt.', action: { href: '/', label: 'Find a creator', icon: 'search' } })}
${next ? `<p><a href="/receipts?cursor=${esc(next)}">Older</a></p>` : ''}
${next ? `<p><a href="/receipts?cursor=${esc(next)}">Older</a></p>` : ''}
<section class="card"><h2>Your data</h2>
<section class="card"><h2>Your data</h2>
<p><a class="btn ghost" href="/receipts/export">Download your tips (JSON)</a></p>
<p><a class="btn ghost" href="/receipts/export">Download your tips (JSON)</a></p>
@@ -536,7 +537,7 @@ function moderationCard({ p, csrf, idem, mod }) {
@@ -536,7 +537,7 @@ function moderationCard({ p, csrf, idem, mod }) {
<button class="btn ghost" type="submit">Save the filter</button>
<button class="btn ghost" type="submit">Save the filter</button>
</form>
</form>
<h3>Moderators</h3>
<h3>Moderators</h3>
- ${moderators.length ? `<ul class="plain">${moderators.map((m) => `<li>${esc(m.name || m.moderator.id)} <small>since ${when(m.added_at)}</small> <form class="inline" method="post" action="/dashboard/moderators/${esc(m.moderator.id)}/remove">${hidden({ csrf, idem: `${idem}-m-${m.moderator.id}` })}<button class="btn ghost" type="submit">Remove</button></form></li>`).join('')}</ul>` : '<p>No moderators yet.</p>'}
+ ${moderators.length ? `<ul class="plain">${moderators.map((m) => `<li>${esc(m.name || m.moderator.id)} <small>since ${when(m.added_at)}</small> <form class="inline" method="post" action="/dashboard/moderators/${esc(m.moderator.id)}/remove">${hidden({ csrf, idem: `${idem}-m-${m.moderator.id}` })}<button class="btn ghost" type="submit">Remove</button></form></li>`).join('')}</ul>` : kit.empty({ icon: 'shield', title: 'No moderators yet', text: 'Invite someone below and they can help with paid messages.' })}
${invites.length ? `<p class="fine">Open invitations: ${invites.map((x) => `created ${when(x.created_at)}, expires ${when(x.expires_at)} <form class="inline" method="post" action="/dashboard/moderator-invites/${esc(x.id)}/revoke">${hidden({ csrf, idem: `${idem}-ri-${x.id}` })}<button class="btn ghost" type="submit">Revoke</button></form>`).join(' · ')}</p>` : ''}
${invites.length ? `<p class="fine">Open invitations: ${invites.map((x) => `created ${when(x.created_at)}, expires ${when(x.expires_at)} <form class="inline" method="post" action="/dashboard/moderator-invites/${esc(x.id)}/revoke">${hidden({ csrf, idem: `${idem}-ri-${x.id}` })}<button class="btn ghost" type="submit">Revoke</button></form>`).join(' · ')}</p>` : ''}
<form method="post" action="/dashboard/moderator-invites">${hidden({ csrf, idem: `${idem}-i` })}<button class="btn ghost" type="submit">Create a moderator invitation link</button></form>
<form method="post" action="/dashboard/moderator-invites">${hidden({ csrf, idem: `${idem}-i` })}<button class="btn ghost" type="submit">Create a moderator invitation link</button></form>
${mod.log && mod.log.length ? `<details><summary>Latest moderation</summary><table class="list"><thead><tr><th>When</th><th>What</th><th>By</th><th>Receipt</th></tr></thead><tbody>${mod.log.map((l) => `<tr><td>${when(l.created_at)}</td><td>${esc(l.action)}</td><td>${esc(ROLE_LABEL[l.by_role] || l.by_role)}</td><td><a href="/receipts/${esc(l.interaction_id)}"><code>${esc(l.interaction_id.slice(-8))}</code></a></td></tr>`).join('')}</tbody></table></details>` : ''}
${mod.log && mod.log.length ? `<details><summary>Latest moderation</summary><table class="list"><thead><tr><th>When</th><th>What</th><th>By</th><th>Receipt</th></tr></thead><tbody>${mod.log.map((l) => `<tr><td>${when(l.created_at)}</td><td>${esc(l.action)}</td><td>${esc(ROLE_LABEL[l.by_role] || l.by_role)}</td><td><a href="/receipts/${esc(l.interaction_id)}"><code>${esc(l.interaction_id.slice(-8))}</code></a></td></tr>`).join('')}</tbody></table></details>` : ''}
new file mode 100644
new file mode 100644
@@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
+'use strict';
+// Empty pages on OpenVibe.Tips are the shared kit's empty state, with the next action as a kit button.
+const assert = require('assert');
+const views = require('../server/web/pages');
+
+const goals = views.goalsPage({ profile: { display_name: 'Alma', handle: 'alma', page: {} }, goals: [] });
+assert.ok(goals.includes('class="ovk-empty"'), 'no goals is a kit empty state');
+assert.ok(goals.includes('No goals yet'), goals.slice(0, 200));
+
+const receipts = views.receiptsPage({ rows: [], next: null, flash: null });
+assert.ok(receipts.includes('class="ovk-empty"'), 'no tips is a kit empty state');
+assert.ok(receipts.includes('No tips yet'));
+assert.ok(/class="ovk-btn[^"]*"[^>]*href="\/"/.test(receipts), 'Find a creator is a kit button');
+
+const layout = require('fs').readFileSync(require('path').join(__dirname, '..', 'server', 'web', 'layout.js'), 'utf8');
+assert.ok(layout.includes("ovServe.url('kit.css')"), 'the shell loads kit.css');
+
+console.log('empty states: Tips goals and receipts use kit.empty');