Member Counter template helpers (#15013)

ref https://github.com/TryGhost/Team/issues/1667

Introducing 2 new helper handlebars tags, `{{total_members}}` and `{{total_paid_members}}` ideal for Member Sites who want to display these metrics to incentivise users to upgrade.
This commit is contained in:
Ronald Langeveld 2022-07-14 10:10:02 +02:00 committed by GitHub
parent b6818b77bd
commit a0c8db46fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 195 additions and 1 deletions

View File

@ -0,0 +1,17 @@
// # Total Members Helper
// Usage: `{{total_members}}`
const {SafeString} = require('../services/handlebars');
const {memberCountRounding, getMemberStats} = require('../utils/member-count');
module.exports = async function total_members () { //eslint-disable-line
if (this.total) {
return new SafeString(memberCountRounding(this.total));
} else {
let memberStats = await getMemberStats();
const {total} = memberStats;
return new SafeString(total > 0 ? memberCountRounding(total) : 0);
}
};
module.exports.async = true;

View File

@ -0,0 +1,16 @@
// {{total_paid_members}} helper
const {SafeString} = require('../services/handlebars');
const {memberCountRounding, getMemberStats} = require('../utils/member-count');
module.exports = async function total_paid_members () { //eslint-disable-line
if (this.paid) {
return new SafeString(memberCountRounding(this.paid));
} else {
let memberStats = await getMemberStats();
const {paid} = memberStats;
return new SafeString(paid > 0 ? memberCountRounding(paid) : 0);
}
};
module.exports.async = true;

View File

@ -0,0 +1,50 @@
const humanNumber = require('human-number');
const {api} = require('../services/proxy');
async function getMemberStats() {
let memberStats = this.data || await api.stats.memberCountHistory.query();
const {free, paid, comped} = memberStats.meta.totals;
let total = free + paid + comped;
return {free, paid, comped, total};
}
const numberWithCommas = (n) => {
return n.toLocaleString();
};
const rounding = (n, roundTo) => {
return Math.floor(n / roundTo) * roundTo;
};
// Rounding https://github.com/TryGhost/Team/issues/1667
const memberCountRounding = (memberCount) => {
if (memberCount <= 50) {
return memberCount;
}
if (memberCount > 50 && memberCount <= 100) {
return `${numberWithCommas(rounding(memberCount, 10))}+`;
}
if (memberCount > 100 && memberCount <= 1000) {
return `${numberWithCommas(rounding(memberCount, 50))}+`;
}
if (memberCount > 1000 && memberCount <= 10000) {
return `${numberWithCommas(rounding(memberCount, 100))}+`;
}
if (memberCount > 10000 && memberCount <= 100000) {
return `${numberWithCommas(rounding(memberCount, 1000))}+`;
}
if (memberCount > 100000 && memberCount <= 1000000) {
return `${humanNumber(rounding(memberCount, 10000)).toLowerCase()}+`;
}
if (memberCount > 1000000) {
return `${humanNumber(rounding(memberCount, 100000)).toLowerCase()}+`;
}
};
module.exports = {memberCountRounding, getMemberStats};

View File

@ -146,6 +146,7 @@
"got": "9.6.0",
"gscan": "4.31.2",
"html-to-text": "8.2.0",
"human-number": "2.0.0",
"image-size": "1.0.2",
"intl": "1.2.5",
"intl-messageformat": "5.4.3",

View File

@ -0,0 +1,28 @@
const should = require('should');
const {memberCountRounding, getMemberStats} = require('../../core/frontend/utils/member-count.js');
describe('Front-end member stats ', function () {
it('should return free', async function () {
const members = await getMemberStats();
const {free} = members;
should.exist(free);
});
it('should return paid', async function () {
const members = await getMemberStats();
const {paid} = members;
should.exist(paid);
});
it('should return comped', async function () {
const members = await getMemberStats();
const {comped} = members;
should.exist(comped);
});
it('should return total', async function () {
const members = await getMemberStats();
const {total} = members;
should.exist(total);
});
});

View File

@ -0,0 +1,10 @@
const should = require('should');
const total_members = require('../../../../core/frontend/helpers/total_members');
describe('{{total_members}} helper', function () {
it('can render total members', async function () {
const rendered = await total_members.call({total: 50000});
should.equal(rendered.string, '50,000+');
});
});

View File

@ -0,0 +1,10 @@
const should = require('should');
const total_paid_members = require('../../../../core/frontend/helpers/total_paid_members');
describe('{{total_paid_members}} helper', function () {
it('can render total paid members', async function () {
const rendered = await total_paid_members.call({paid: 3000});
should.equal(rendered.string, '3,000+');
});
});

View File

@ -10,7 +10,7 @@ describe('Helpers', function () {
const ghostHelpers = [
'asset', 'authors', 'body_class', 'cancel_link', 'concat', 'content', 'date', 'encode', 'excerpt', 'facebook_url', 'foreach', 'get',
'ghost_foot', 'ghost_head', 'has', 'img_url', 'is', 'lang', 'link', 'link_class', 'meta_description', 'meta_title', 'navigation',
'next_post', 'page_url', 'pagination', 'plural', 'post_class', 'prev_post', 'price', 'raw', 'reading_time', 't', 'tags', 'title', 'twitter_url',
'next_post', 'page_url', 'pagination', 'plural', 'post_class', 'prev_post', 'price', 'raw', 'reading_time', 't', 'tags', 'title','total_members', 'total_paid_members', 'twitter_url',
'url', 'comment_count'
];
const experimentalHelpers = ['match', 'tiers', 'comments'];

View File

@ -0,0 +1,50 @@
const should = require('should');
const {memberCountRounding, getMemberStats} = require('../../../../core/frontend/utils/member-count');
const getMemberStatsMock = [
{
members: 30,
expected: '30'
},
{
members: 55,
expected: '50+'
},
{
members: 580,
expected: '550+'
},
{
members: 5555,
expected: '5,500+'
},
{
members: 55555,
expected: '55,000+'
},
{
members: 555555,
expected: '550k+'
},
{
members: 5555555,
expected: '5.5m+'
}
];
describe('Member Count', function () {
it('should return total members', async function () {
const meta = {data: {
meta: {totals: {paid: 1000, free: 500, comped: 500}}
}};
const members = await getMemberStats.call(meta);
return should.equal(members.total, 2000);
});
it('should return rounded numbers in correct format', function () {
getMemberStatsMock.map((mock) => {
const result = memberCountRounding(mock.members);
return should.equal(result, mock.expected);
});
});
});

View File

@ -6574,6 +6574,13 @@ human-interval@^2.0.0:
dependencies:
numbered "^1.1.0"
human-number@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/human-number/-/human-number-2.0.0.tgz#ffdfa7954c40d32c02aa0ebb3b0f54f8fc4ed410"
integrity sha512-hMb3DuF2aMTaFy795TU65AfQnZDd+UF6q8f29m3t6n648I3VCJyYXd1pJnnJsOJRYi7yBvYG29RdkrS59GwSEw==
dependencies:
round-to "~5.0.0"
humanize-ms@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed"
@ -10626,6 +10633,11 @@ rimraf@~2.4.0:
dependencies:
glob "^6.0.1"
round-to@~5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/round-to/-/round-to-5.0.0.tgz#a66292701a93b194f630a0d57f04c08821b6eeed"
integrity sha512-i4+Ntwmo5kY7UWWFSDEVN3RjT2PX1FqkZ9iCcAO3sKML3Ady9NgsjM/HLdYKUAnrxK4IlSvXzpBMDvMHZQALRQ==
rss@1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/rss/-/rss-1.2.2.tgz#50a1698876138133a74f9a05d2bdc8db8d27a921"