mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-28 22:43:30 +03:00
Introduced new layout to bring new Dashboard closer to release (#2342)
refs: https://github.com/TryGhost/Team/issues/1531 - broke apart the combined chart - added back in the paid mix chart - separated out the mini charts into separate components - made top chart work with total, paid and free - added in an overview section back at the top for total, paid, free - made metric labels and values larger and easier to parse Co-authored-by: Simon Backx <simon@ghost.org>
This commit is contained in:
parent
79eea318a9
commit
a06003e7b5
@ -3,23 +3,14 @@
|
||||
<GhLoadingSpinner />
|
||||
{{else}}
|
||||
{{#if this.areMembersEnabled}}
|
||||
{{#if this.hasPaidTiers}}
|
||||
<Dashboard::V5::Charts::Overview />
|
||||
{{/if}}
|
||||
<Dashboard::V5::Charts::Anchor />
|
||||
{{/if}}
|
||||
|
||||
{{!-- Could these if else statements be cleaned up more? --}}
|
||||
{{#if this.areNewslettersEnabled}}
|
||||
<Dashboard::V5::Charts::Email />
|
||||
<Dashboard::V5::Charts::Engagement />
|
||||
<Dashboard::V5::Charts::EmailOpenRate />
|
||||
{{else}}
|
||||
<Dashboard::V5::Charts::Engagement />
|
||||
{{/if}}
|
||||
|
||||
{{else}}
|
||||
{{#if this.areNewslettersEnabled}}
|
||||
<Dashboard::V5::Charts::Email />
|
||||
<Dashboard::V5::Charts::Engagement />
|
||||
<Dashboard::V5::Charts::EmailOpenRate />
|
||||
{{/if}}
|
||||
{{#if this.areNewslettersEnabled}}
|
||||
<Dashboard::V5::Charts::Engagement />
|
||||
{{/if}}
|
||||
|
||||
<Dashboard::V5::Charts::RecentPosts />
|
||||
|
@ -1,38 +1,28 @@
|
||||
<section class="gh-dashboard5-section gh-dashboard5-anchor" {{did-insert this.loadCharts}}>
|
||||
<article class="gh-dashboard5-box">
|
||||
{{#if this.hasPaidTiers}}
|
||||
<div class="gh-dashboard5-title">
|
||||
<h4>{{this.chartTitle}}</h4>
|
||||
</div>
|
||||
<Dashboard::v5::Parts::Metric
|
||||
@label="Members over time" />
|
||||
{{else}}
|
||||
<Dashboard::v5::Parts::Metric
|
||||
@label="Total members"
|
||||
@value={{format-number this.totalMembers}}
|
||||
@trends={{this.hasTrends}}
|
||||
@percentage={{this.totalMembersTrend}}
|
||||
@large={{true}} />
|
||||
{{/if}}
|
||||
<div class="gh-dashboard5-hero {{unless this.hasPaidTiers 'is-solo'}}">
|
||||
{{#unless this.hasPaidTiers}}
|
||||
<Dashboard::v5::Parts::Metric
|
||||
@label={{gh-pluralize this.totalMembers "Total member" without-count=true}}
|
||||
@value={{format-number this.totalMembers}}
|
||||
@trends={{this.hasTrends}}
|
||||
@percentage={{this.totalMembersTrend}}
|
||||
@large={{true}} />
|
||||
{{/unless}}
|
||||
|
||||
<div class="gh-dashboard5-hero {{unless this.hasPaidTiers 'is-solo'}}">
|
||||
<div class="gh-dashboard5-chart">
|
||||
{{#if this.loading}}
|
||||
<div class="gh-dashboard5-chart-loading" style={{html-safe (concat "height: " this.chartHeight "px;")}}/>
|
||||
{{else}}
|
||||
<div class="gh-dashboard5-chart-container">
|
||||
{{#if (eq this.chartType 'bar')}}
|
||||
<EmberChart
|
||||
@type="bar"
|
||||
@data={{this.chartData}}
|
||||
@options={{this.chartOptions}}
|
||||
@height={{if this.hasPaidTiers this.chartHeight this.chartHeightSmall}} />
|
||||
{{else}}
|
||||
<EmberChart
|
||||
@type="line"
|
||||
@data={{this.chartData}}
|
||||
@options={{this.chartOptions}}
|
||||
@height={{if this.hasPaidTiers this.chartHeight this.chartHeightSmall}} />
|
||||
{{/if}}
|
||||
<EmberChart
|
||||
@type="line"
|
||||
@data={{this.chartData}}
|
||||
@options={{this.chartOptions}}
|
||||
@height={{if this.hasPaidTiers this.chartHeight this.chartHeightSmall}} />
|
||||
</div>
|
||||
<div class="gh-dashboard5-chart-ticks">
|
||||
<span id="gh-dashboard5-anchor-date-start"> </span>
|
||||
@ -42,46 +32,28 @@
|
||||
</div>
|
||||
|
||||
{{#if this.hasPaidTiers}}
|
||||
<div class="gh-dashboard5-stats{{unless this.hasPaidTiers ' is-solo'}}">
|
||||
<button class="gh-dashboard5-stats-button {{if this.chartShowingTotal 'is-selected'}}" type="button" {{on "click" (fn this.changeChartDisplay "total")}}>
|
||||
<Dashboard::v5::Parts::Metric
|
||||
@label={{gh-pluralize this.totalMembers "Total member" without-count=true}}
|
||||
@value={{format-number this.totalMembers}}
|
||||
{{!-- @trends={{this.hasTrends}} --}}
|
||||
{{!-- @percentage={{this.totalMembersTrend}} --}}
|
||||
@center={{true}}
|
||||
@large={{true}} />
|
||||
</button>
|
||||
<button class="gh-dashboard5-stats-button {{if this.chartShowingPaid 'is-selected'}}" type="button" {{on "click" (fn this.changeChartDisplay "paid-total")}}>
|
||||
<Dashboard::v5::Parts::Metric
|
||||
@label={{gh-pluralize this.paidMembers "Total paid member" without-count=true}}
|
||||
@value={{format-number this.paidMembers}}
|
||||
{{!-- @trends={{this.hasTrends}} --}}
|
||||
{{!-- @percentage={{this.paidMembersTrend}} --}}
|
||||
@center={{true}}
|
||||
@large={{true}} />
|
||||
</button>
|
||||
<button class="gh-dashboard5-stats-button {{if this.chartShowingMrr 'is-selected'}}" type="button" {{on "click" (fn this.changeChartDisplay "mrr")}}>
|
||||
<Dashboard::v5::Parts::Metric
|
||||
@label="Monthly Revenue (MRR)"
|
||||
@value="{{this.mrrCurrencySymbol}}{{gh-price-amount this.currentMRR}}"
|
||||
{{!-- @trends={{this.hasTrends}} --}}
|
||||
{{!-- @percentage={{this.mrrTrend}} --}}
|
||||
@center={{true}}
|
||||
@large={{true}} />
|
||||
</button>
|
||||
</div>
|
||||
<article class="gh-dashboard5-columns">
|
||||
<div class="gh-dashboard5-column gh-dashboard5-mrr">
|
||||
<Dashboard::v5::Charts::Mrr />
|
||||
</div>
|
||||
<div class="gh-dashboard5-column gh-dashboard5-breakdown">
|
||||
<Dashboard::v5::Charts::PaidBreakdown />
|
||||
</div>
|
||||
<div class="gh-dashboard5-column gh-dashboard5-mix">
|
||||
<Dashboard::v5::Charts::PaidMix />
|
||||
</div>
|
||||
</article>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="gh-dashboard5-selects">
|
||||
{{#if this.chartShowingPaid}}
|
||||
{{#if this.hasPaidTiers}}
|
||||
<div class="gh-dashboard5-select">
|
||||
<PowerSelect
|
||||
@selected={{this.selectedPaidOption}}
|
||||
@options={{this.paidOptions}}
|
||||
@selected={{this.selectedDisplayOption}}
|
||||
@options={{this.displayOptions}}
|
||||
@searchEnabled={{false}}
|
||||
@onChange={{this.onPaidChange}}
|
||||
@onChange={{this.onDisplayChange}}
|
||||
@triggerComponent="gh-power-select/trigger"
|
||||
@triggerClass="gh-contentfilter-menu-trigger"
|
||||
@dropdownClass="gh-contentfilter-menu-dropdown"
|
||||
@ -92,6 +64,7 @@
|
||||
</PowerSelect>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="gh-dashboard5-select">
|
||||
<PowerSelect
|
||||
@selected={{this.selectedDaysOption}}
|
||||
|
@ -22,12 +22,15 @@ const DAYS_OPTIONS = [{
|
||||
value: 'all'
|
||||
}];
|
||||
|
||||
const PAID_OPTIONS = [{
|
||||
name: 'Total',
|
||||
value: 'paid-total'
|
||||
const DISPLAY_OPTIONS = [{
|
||||
name: 'All members',
|
||||
value: 'total'
|
||||
}, {
|
||||
name: 'By Day',
|
||||
value: 'paid-breakdown'
|
||||
name: 'Paid members',
|
||||
value: 'paid'
|
||||
}, {
|
||||
name: 'Free members',
|
||||
value: 'free'
|
||||
}];
|
||||
|
||||
export default class Anchor extends Component {
|
||||
@ -36,7 +39,7 @@ export default class Anchor extends Component {
|
||||
@tracked chartDisplay = 'total';
|
||||
|
||||
daysOptions = DAYS_OPTIONS;
|
||||
paidOptions = PAID_OPTIONS;
|
||||
displayOptions = DISPLAY_OPTIONS;
|
||||
|
||||
get days() {
|
||||
return this.dashboardStats.chartDays;
|
||||
@ -46,29 +49,18 @@ export default class Anchor extends Component {
|
||||
this.dashboardStats.chartDays = days;
|
||||
}
|
||||
|
||||
@action
|
||||
onInsert() {
|
||||
this.dashboardStats.loadSiteStatus();
|
||||
}
|
||||
|
||||
@action
|
||||
loadCharts() {
|
||||
this.dashboardStats.loadMemberCountStats();
|
||||
this.dashboardStats.loadMrrStats();
|
||||
}
|
||||
|
||||
@action
|
||||
changeChartDisplay(type) {
|
||||
this.chartDisplay = type;
|
||||
if (this.hasPaidTiers) {
|
||||
this.dashboardStats.loadMrrStats();
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
onPaidChange(selected) {
|
||||
this.changeChartDisplay(selected.value);
|
||||
|
||||
// The graph won't switch correctly from line -> bar
|
||||
// So we need to recreate it somehow.
|
||||
// Solution: recreate the DOM by using an #if in hbs
|
||||
onDisplayChange(selected) {
|
||||
this.chartDisplay = selected.value;
|
||||
}
|
||||
|
||||
@action
|
||||
@ -80,33 +72,12 @@ export default class Anchor extends Component {
|
||||
return this.daysOptions.find(d => d.value === this.days);
|
||||
}
|
||||
|
||||
get selectedPaidOption() {
|
||||
return this.paidOptions.find(d => d.value === this.chartDisplay) ?? this.paidOptions[0];
|
||||
}
|
||||
|
||||
get chartShowingTotal() {
|
||||
return (this.chartDisplay === 'total');
|
||||
}
|
||||
|
||||
get chartShowingPaid() {
|
||||
return (this.chartDisplay === 'paid-total' || this.chartDisplay === 'paid-breakdown');
|
||||
}
|
||||
|
||||
get chartShowingMrr() {
|
||||
return (this.chartDisplay === 'mrr');
|
||||
get selectedDisplayOption() {
|
||||
return this.displayOptions.find(d => d.value === this.chartDisplay) ?? this.displayOptions[0];
|
||||
}
|
||||
|
||||
get loading() {
|
||||
if (this.chartDisplay === 'total') {
|
||||
return this.dashboardStats.memberCountStats === null;
|
||||
} else if (this.chartDisplay === 'paid-total') {
|
||||
return this.dashboardStats.memberCountStats === null;
|
||||
} else if (this.chartDisplay === 'paid-breakdown') {
|
||||
return this.dashboardStats.memberCountStats === null;
|
||||
} else if (this.chartDisplay === 'mrr') {
|
||||
return this.dashboardStats.mrrStats === null;
|
||||
}
|
||||
return true;
|
||||
return this.dashboardStats.memberCountStats === null;
|
||||
}
|
||||
|
||||
get totalMembers() {
|
||||
@ -121,15 +92,9 @@ export default class Anchor extends Component {
|
||||
return this.dashboardStats.memberCounts?.free ?? 0;
|
||||
}
|
||||
|
||||
get currentMRR() {
|
||||
return this.dashboardStats.currentMRR ?? 0;
|
||||
}
|
||||
|
||||
get hasTrends() {
|
||||
return this.dashboardStats.memberCounts !== null
|
||||
&& this.dashboardStats.memberCountsTrend !== null
|
||||
&& this.dashboardStats.currentMRR !== null
|
||||
&& this.dashboardStats.currentMRRTrend !== null;
|
||||
&& this.dashboardStats.memberCountsTrend !== null;
|
||||
}
|
||||
|
||||
get totalMembersTrend() {
|
||||
@ -144,92 +109,29 @@ export default class Anchor extends Component {
|
||||
return this.calculatePercentage(this.dashboardStats.memberCountsTrend.free, this.dashboardStats.memberCounts.free);
|
||||
}
|
||||
|
||||
get mrrTrend() {
|
||||
return this.calculatePercentage(this.dashboardStats.currentMRRTrend, this.dashboardStats.currentMRR);
|
||||
}
|
||||
|
||||
get hasPaidTiers() {
|
||||
return this.dashboardStats.siteStatus?.hasPaidTiers;
|
||||
}
|
||||
|
||||
get chartTitle() {
|
||||
if (this.chartDisplay === 'paid-total') {
|
||||
return 'Total paid members';
|
||||
} else if (this.chartDisplay === 'paid-breakdown') {
|
||||
return 'Paid members by day';
|
||||
} else if (this.chartDisplay === 'mrr') {
|
||||
return 'Monthly revenue (MRR)';
|
||||
}
|
||||
return 'Total members';
|
||||
}
|
||||
|
||||
get chartType() {
|
||||
if (this.chartDisplay === 'paid-breakdown') {
|
||||
return 'bar';
|
||||
}
|
||||
return 'line';
|
||||
}
|
||||
|
||||
get chartData() {
|
||||
if (this.chartDisplay === 'paid-breakdown') {
|
||||
const stats = this.dashboardStats.filledMemberCountStats;
|
||||
const labels = stats.map(stat => stat.date);
|
||||
const newData = stats.map(stat => stat.paidSubscribed);
|
||||
const canceledData = stats.map(stat => -stat.paidCanceled);
|
||||
const netData = stats.map(stat => stat.paidSubscribed - stat.paidCanceled);
|
||||
const barThickness = (this.selectedDaysOption.value < 90 ? 18 : 7);
|
||||
|
||||
return {
|
||||
labels: labels,
|
||||
datasets: [
|
||||
{
|
||||
type: 'line',
|
||||
data: netData,
|
||||
tension: 0,
|
||||
cubicInterpolationMode: 'monotone',
|
||||
fill: false,
|
||||
pointRadius: 0,
|
||||
pointHitRadius: 10,
|
||||
pointBorderColor: '#14B8FF',
|
||||
pointBackgroundColor: '#14B8FF',
|
||||
pointHoverBackgroundColor: '#14B8FF',
|
||||
pointHoverBorderColor: '#14B8FF',
|
||||
pointHoverRadius: 0,
|
||||
borderColor: 'rgba(189, 197, 204, 0.5)',
|
||||
borderJoinStyle: 'miter',
|
||||
borderWidth: 3
|
||||
}, {
|
||||
data: newData,
|
||||
fill: false,
|
||||
backgroundColor: '#BD96F6',
|
||||
cubicInterpolationMode: 'monotone',
|
||||
barThickness: barThickness,
|
||||
minBarLength: 3
|
||||
}, {
|
||||
data: canceledData,
|
||||
fill: false,
|
||||
backgroundColor: '#FB76B4',
|
||||
cubicInterpolationMode: 'monotone',
|
||||
barThickness: barThickness,
|
||||
minBarLength: 3
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
let stats;
|
||||
let labels;
|
||||
let data;
|
||||
|
||||
if (this.chartDisplay === 'paid-total') {
|
||||
// paid-total
|
||||
if (this.chartDisplay === 'paid') {
|
||||
// paid
|
||||
stats = this.dashboardStats.filledMemberCountStats;
|
||||
labels = stats.map(stat => stat.date);
|
||||
data = stats.map(stat => stat.paid);
|
||||
} else if (this.chartDisplay === 'mrr') {
|
||||
// mrr
|
||||
stats = this.dashboardStats.filledMrrStats;
|
||||
data = stats.map(stat => stat.paid + stat.comped);
|
||||
} else if (this.chartDisplay === 'free') {
|
||||
// free
|
||||
stats = this.dashboardStats.filledMemberCountStats;
|
||||
labels = stats.map(stat => stat.date);
|
||||
data = stats.map(stat => stat.mrr);
|
||||
data = stats.map(stat => stat.free);
|
||||
} else {
|
||||
// total
|
||||
stats = this.dashboardStats.filledMemberCountStats;
|
||||
@ -244,16 +146,16 @@ export default class Anchor extends Component {
|
||||
tension: 0,
|
||||
cubicInterpolationMode: 'monotone',
|
||||
fill: true,
|
||||
fillColor: 'rgba(20, 184, 255, 0.07)',
|
||||
backgroundColor: 'rgba(20, 184, 255, 0.07)',
|
||||
fillColor: 'rgba(142, 66, 255, 0.05)',
|
||||
backgroundColor: 'rgba(142, 66, 255, 0.05)',
|
||||
pointRadius: 0,
|
||||
pointHitRadius: 10,
|
||||
pointBorderColor: '#14B8FF',
|
||||
pointBackgroundColor: '#14B8FF',
|
||||
pointHoverBackgroundColor: '#14B8FF',
|
||||
pointHoverBorderColor: '#14B8FF',
|
||||
pointBorderColor: '#8E42FF',
|
||||
pointBackgroundColor: '#8E42FF',
|
||||
pointHoverBackgroundColor: '#8E42FF',
|
||||
pointHoverBorderColor: '#8E42FF',
|
||||
pointHoverRadius: 0,
|
||||
borderColor: '#14B8FF',
|
||||
borderColor: '#8E42FF',
|
||||
borderJoinStyle: 'miter'
|
||||
}]
|
||||
};
|
||||
@ -270,107 +172,6 @@ export default class Anchor extends Component {
|
||||
|
||||
get chartOptions() {
|
||||
let barColor = this.feature.nightShift ? 'rgba(200, 204, 217, 0.25)' : 'rgba(200, 204, 217, 0.65)';
|
||||
|
||||
if (this.chartDisplay === 'paid-breakdown') {
|
||||
return {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
title: {
|
||||
display: false
|
||||
},
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
hover: {
|
||||
onHover: function (e) {
|
||||
e.target.style.cursor = 'pointer';
|
||||
}
|
||||
},
|
||||
tooltips: {
|
||||
intersect: false,
|
||||
mode: 'index',
|
||||
displayColors: false,
|
||||
backgroundColor: '#15171A',
|
||||
xPadding: 7,
|
||||
yPadding: 7,
|
||||
cornerRadius: 5,
|
||||
caretSize: 7,
|
||||
caretPadding: 5,
|
||||
bodyFontSize: 12.5,
|
||||
titleFontSize: 12,
|
||||
titleFontStyle: 'normal',
|
||||
titleFontColor: 'rgba(255, 255, 255, 0.7)',
|
||||
titleMarginBottom: 3,
|
||||
callbacks: {
|
||||
label: (tooltipItems, data) => {
|
||||
let valueText = data.datasets[tooltipItems.datasetIndex].data[tooltipItems.index].toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
|
||||
if (tooltipItems.datasetIndex === 0) {
|
||||
return `Net: ${valueText}`;
|
||||
}
|
||||
|
||||
if (tooltipItems.datasetIndex === 1) {
|
||||
return `New paid: ${valueText}`;
|
||||
}
|
||||
|
||||
if (tooltipItems.datasetIndex === 2) {
|
||||
return `Canceled paid: ${Math.abs(valueText)}`;
|
||||
}
|
||||
},
|
||||
title: (tooltipItems) => {
|
||||
return moment(tooltipItems[0].xLabel).format(DATE_FORMAT);
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
yAxes: [{
|
||||
offset: false,
|
||||
gridLines: {
|
||||
drawTicks: false,
|
||||
display: true,
|
||||
drawBorder: false,
|
||||
color: 'rgba(255, 255, 255, 0.1)',
|
||||
lineWidth: 0,
|
||||
zeroLineColor: barColor,
|
||||
zeroLineWidth: 1
|
||||
},
|
||||
ticks: {
|
||||
display: false,
|
||||
maxTicksLimit: 5,
|
||||
fontColor: '#7C8B9A',
|
||||
padding: 8,
|
||||
precision: 0
|
||||
}
|
||||
}],
|
||||
xAxes: [{
|
||||
offset: true,
|
||||
stacked: true,
|
||||
gridLines: {
|
||||
color: barColor,
|
||||
borderDash: [4,4],
|
||||
display: true,
|
||||
drawBorder: false,
|
||||
drawTicks: false,
|
||||
zeroLineWidth: 1,
|
||||
zeroLineColor: barColor,
|
||||
zeroLineBorderDash: [4,4]
|
||||
},
|
||||
ticks: {
|
||||
display: false,
|
||||
callback: function (value, index, values) {
|
||||
if (index === 0) {
|
||||
document.getElementById('gh-dashboard5-anchor-date-start').innerHTML = moment(value).format(DATE_FORMAT);
|
||||
}
|
||||
if (index === values.length - 1) {
|
||||
document.getElementById('gh-dashboard5-anchor-date-end').innerHTML = moment(value).format(DATE_FORMAT);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
responsive: true,
|
||||
@ -482,11 +283,11 @@ export default class Anchor extends Component {
|
||||
}
|
||||
|
||||
get chartHeight() {
|
||||
return 250;
|
||||
return 200;
|
||||
}
|
||||
|
||||
get chartHeightSmall() {
|
||||
return 225;
|
||||
return 200;
|
||||
}
|
||||
|
||||
calculatePercentage(from, to) {
|
||||
|
@ -1,16 +1,28 @@
|
||||
<section class="gh-dashboard5-section gh-dashboard5-engagement">
|
||||
<article {{did-insert this.loadCharts}} class="gh-dashboard5-box">
|
||||
<div class="gh-dashboard5-rows">
|
||||
<div class="gh-dashboard5-row">
|
||||
<Dashboard::v5::Parts::Metric
|
||||
@label="Engagement" />
|
||||
|
||||
<div class="gh-dashboard5-columns">
|
||||
<div class="gh-dashboard5-column">
|
||||
<Dashboard::v5::Parts::Metric
|
||||
@label="Engaged over 30 days"
|
||||
@label="Engaged in the last 30 days"
|
||||
@value={{this.data30Days}}
|
||||
@secondary={{true}}
|
||||
/>
|
||||
</div>
|
||||
<div class="gh-dashboard5-row">
|
||||
<div class="gh-dashboard5-column">
|
||||
<Dashboard::v5::Parts::Metric
|
||||
@label="Engaged over 7 days"
|
||||
@label="Engaged in the last 7 days"
|
||||
@value={{this.data7Days}}
|
||||
@secondary={{true}}
|
||||
/>
|
||||
</div>
|
||||
<div class="gh-dashboard5-column">
|
||||
<Dashboard::v5::Parts::Metric
|
||||
@label="Newsletter subscribers"
|
||||
@value={{this.dataSubscribers}}
|
||||
@secondary={{true}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,16 +1,17 @@
|
||||
import Component from '@glimmer/component';
|
||||
import {action} from '@ember/object';
|
||||
import {formatNumber} from '../../../../helpers/format-number';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
|
||||
const STATUS_OPTIONS = [{
|
||||
name: 'All',
|
||||
name: 'All members',
|
||||
value: 'total'
|
||||
}, {
|
||||
name: 'Paid',
|
||||
name: 'Paid members',
|
||||
value: 'paid'
|
||||
}, {
|
||||
name: 'Free',
|
||||
name: 'Free members',
|
||||
value: 'free'
|
||||
}];
|
||||
|
||||
@ -22,6 +23,7 @@ export default class Engagement extends Component {
|
||||
this.dashboardStats.lastSeenFilterStatus = this.status;
|
||||
this.dashboardStats.loadLastSeen();
|
||||
this.dashboardStats.loadMemberCountStats();
|
||||
this.dashboardStats.loadNewsletterSubscribers();
|
||||
}
|
||||
|
||||
@tracked status = 'total';
|
||||
@ -74,4 +76,16 @@ export default class Engagement extends Component {
|
||||
const percentage = Math.round(part / total * 100);
|
||||
return `${percentage}%`;
|
||||
}
|
||||
|
||||
get dataSubscribers() {
|
||||
if (!this.dashboardStats.newsletterSubscribers) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
return formatNumber(this.dashboardStats.newsletterSubscribers[this.status]);
|
||||
}
|
||||
|
||||
get dataEmailsSent() {
|
||||
return this.dashboardStats.emailsSent30d ?? 0;
|
||||
}
|
||||
}
|
||||
|
17
ghost/admin/app/components/dashboard/v5/charts/mrr.hbs
Normal file
17
ghost/admin/app/components/dashboard/v5/charts/mrr.hbs
Normal file
@ -0,0 +1,17 @@
|
||||
<Dashboard::v5::Parts::Metric
|
||||
@label={{this.chartTitle}}
|
||||
@value="{{this.currentMRRFormatted}}"
|
||||
@trends={{this.hasTrends}}
|
||||
@percentage={{this.mrrTrend}} />
|
||||
|
||||
<div class="gh-dashboard5-chart">
|
||||
{{#if this.loading}}
|
||||
<div class="gh-dashboard5-chart-loading" style={{html-safe (concat "height: " this.chartHeight "px;")}}/>
|
||||
{{else}}
|
||||
<EmberChart
|
||||
@type={{this.chartType}}
|
||||
@data={{this.chartData}}
|
||||
@options={{this.chartOptions}}
|
||||
@height={{this.chartHeight}} />
|
||||
{{/if}}
|
||||
</div>
|
185
ghost/admin/app/components/dashboard/v5/charts/mrr.js
Normal file
185
ghost/admin/app/components/dashboard/v5/charts/mrr.js
Normal file
@ -0,0 +1,185 @@
|
||||
import Component from '@glimmer/component';
|
||||
import moment from 'moment';
|
||||
import {getSymbol} from 'ghost-admin/utils/currency';
|
||||
import {ghPriceAmount} from '../../../../helpers/gh-price-amount';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
const DATE_FORMAT = 'D MMM';
|
||||
|
||||
export default class Mrr extends Component {
|
||||
@service dashboardStats;
|
||||
@service feature;
|
||||
|
||||
get loading() {
|
||||
return this.dashboardStats.mrrStats === null;
|
||||
}
|
||||
|
||||
get currentMRR() {
|
||||
return this.dashboardStats.currentMRR ?? 0;
|
||||
}
|
||||
|
||||
get mrrTrend() {
|
||||
return this.calculatePercentage(this.dashboardStats.currentMRRTrend, this.dashboardStats.currentMRR);
|
||||
}
|
||||
|
||||
get hasTrends() {
|
||||
return this.dashboardStats.currentMRR !== null
|
||||
&& this.dashboardStats.currentMRRTrend !== null;
|
||||
}
|
||||
|
||||
get chartTitle() {
|
||||
return 'MRR';
|
||||
}
|
||||
|
||||
get chartType() {
|
||||
return 'line';
|
||||
}
|
||||
|
||||
get mrrCurrencySymbol() {
|
||||
if (this.dashboardStats.mrrStats === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const firstCurrency = this.dashboardStats.mrrStats[0] ? this.dashboardStats.mrrStats[0].currency : 'usd';
|
||||
return getSymbol(firstCurrency);
|
||||
}
|
||||
|
||||
get currentMRRFormatted() {
|
||||
if (this.dashboardStats.mrrStats === null) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
const valueText = ghPriceAmount(this.currentMRR);
|
||||
return `${this.mrrCurrencySymbol}${valueText}`;
|
||||
}
|
||||
|
||||
get chartData() {
|
||||
const stats = this.dashboardStats.filledMrrStats;
|
||||
const labels = stats.map(stat => stat.date);
|
||||
const data = stats.map(stat => stat.mrr);
|
||||
|
||||
return {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
data: data,
|
||||
tension: 0,
|
||||
cubicInterpolationMode: 'monotone',
|
||||
fill: true,
|
||||
fillColor: 'rgba(142, 66, 255, 0.02)',
|
||||
backgroundColor: 'rgba(142, 66, 255, 0.02)',
|
||||
pointRadius: 0,
|
||||
pointHitRadius: 10,
|
||||
pointBorderColor: '#8E42FF',
|
||||
pointBackgroundColor: '#8E42FF',
|
||||
pointHoverBackgroundColor: '#8E42FF',
|
||||
pointHoverBorderColor: '#8E42FF',
|
||||
pointHoverRadius: 0,
|
||||
borderColor: '#8E42FF',
|
||||
borderJoinStyle: 'miter'
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
get chartOptions() {
|
||||
let barColor = this.feature.nightShift ? 'rgba(200, 204, 217, 0.25)' : 'rgba(200, 204, 217, 0.65)';
|
||||
|
||||
return {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
title: {
|
||||
display: false
|
||||
},
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
layout: {
|
||||
padding: {
|
||||
top: 2,
|
||||
bottom: 2
|
||||
}
|
||||
},
|
||||
hover: {
|
||||
onHover: function (e) {
|
||||
e.target.style.cursor = 'pointer';
|
||||
}
|
||||
},
|
||||
tooltips: {
|
||||
intersect: false,
|
||||
mode: 'index',
|
||||
displayColors: false,
|
||||
backgroundColor: '#15171A',
|
||||
xPadding: 7,
|
||||
yPadding: 7,
|
||||
cornerRadius: 5,
|
||||
caretSize: 7,
|
||||
caretPadding: 5,
|
||||
bodyFontSize: 12.5,
|
||||
titleFontSize: 12,
|
||||
titleFontStyle: 'normal',
|
||||
titleFontColor: 'rgba(255, 255, 255, 0.7)',
|
||||
titleMarginBottom: 3,
|
||||
callbacks: {
|
||||
label: (tooltipItems, data) => {
|
||||
// Convert integer in cents to value in USD/other currency.
|
||||
const valueText = ghPriceAmount(data.datasets[tooltipItems.datasetIndex].data[tooltipItems.index]);
|
||||
return `MRR: ${this.mrrCurrencySymbol}${valueText}`;
|
||||
},
|
||||
title: (tooltipItems) => {
|
||||
return moment(tooltipItems[0].xLabel).format(DATE_FORMAT);
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
yAxes: [{
|
||||
display: true,
|
||||
gridLines: {
|
||||
drawTicks: false,
|
||||
display: false,
|
||||
drawBorder: false,
|
||||
color: 'transparent',
|
||||
zeroLineColor: barColor,
|
||||
zeroLineWidth: 1
|
||||
},
|
||||
ticks: {
|
||||
display: false
|
||||
}
|
||||
}],
|
||||
xAxes: [{
|
||||
display: true,
|
||||
scaleLabel: {
|
||||
align: 'start'
|
||||
},
|
||||
gridLines: {
|
||||
color: barColor,
|
||||
borderDash: [4,4],
|
||||
display: false,
|
||||
drawBorder: true,
|
||||
drawTicks: false,
|
||||
zeroLineWidth: 1,
|
||||
zeroLineColor: barColor,
|
||||
zeroLineBorderDash: [4,4]
|
||||
},
|
||||
ticks: {
|
||||
display: false,
|
||||
beginAtZero: true
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
get chartHeight() {
|
||||
return 90;
|
||||
}
|
||||
|
||||
calculatePercentage(from, to) {
|
||||
if (from === 0) {
|
||||
if (to > 0) {
|
||||
return 100;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Math.round((to - from) / from * 100);
|
||||
}
|
||||
}
|
32
ghost/admin/app/components/dashboard/v5/charts/overview.hbs
Normal file
32
ghost/admin/app/components/dashboard/v5/charts/overview.hbs
Normal file
@ -0,0 +1,32 @@
|
||||
<section class="gh-dashboard5-section gh-dashboard5-overview">
|
||||
<article {{did-insert this.loadCharts}} class="gh-dashboard5-box is-secondary">
|
||||
|
||||
<div class="gh-dashboard5-columns">
|
||||
<div class="gh-dashboard5-column">
|
||||
<Dashboard::v5::Parts::Metric
|
||||
@label={{gh-pluralize this.totalMembers "Total member" without-count=true}}
|
||||
@value={{this.totalMembersFormatted}}
|
||||
@trends={{this.hasTrends}}
|
||||
@percentage={{this.totalMembersTrend}}
|
||||
@large={{true}} />
|
||||
</div>
|
||||
<div class="gh-dashboard5-column">
|
||||
<Dashboard::v5::Parts::Metric
|
||||
@label={{gh-pluralize this.paidMembers "Paid member" without-count=true}}
|
||||
@value={{this.paidMembersFormatted}}
|
||||
@trends={{this.hasTrends}}
|
||||
@percentage={{this.paidMembersTrend}}
|
||||
@large={{true}} />
|
||||
</div>
|
||||
<div class="gh-dashboard5-column">
|
||||
<Dashboard::v5::Parts::Metric
|
||||
@label={{gh-pluralize this.freeMembers "Free member" without-count=true}}
|
||||
@value={{this.freeMembersFormatted}}
|
||||
@trends={{this.hasTrends}}
|
||||
@percentage={{this.freeMembersTrend}}
|
||||
@large={{true}} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</article>
|
||||
</section>
|
85
ghost/admin/app/components/dashboard/v5/charts/overview.js
Normal file
85
ghost/admin/app/components/dashboard/v5/charts/overview.js
Normal file
@ -0,0 +1,85 @@
|
||||
import Component from '@glimmer/component';
|
||||
import {action} from '@ember/object';
|
||||
import {formatNumber} from '../../../../helpers/format-number';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default class Overview extends Component {
|
||||
@service dashboardStats;
|
||||
@service feature;
|
||||
|
||||
@action
|
||||
loadCharts() {
|
||||
this.dashboardStats.loadMemberCountStats();
|
||||
}
|
||||
|
||||
get loading() {
|
||||
return this.dashboardStats.memberCountStats === null;
|
||||
}
|
||||
|
||||
get totalMembers() {
|
||||
return this.dashboardStats.memberCounts?.total ?? 0;
|
||||
}
|
||||
|
||||
get paidMembers() {
|
||||
return this.dashboardStats.memberCounts?.paid ?? 0;
|
||||
}
|
||||
|
||||
get freeMembers() {
|
||||
return this.dashboardStats.memberCounts?.free ?? 0;
|
||||
}
|
||||
|
||||
get totalMembersFormatted() {
|
||||
if (this.dashboardStats.memberCounts === null) {
|
||||
return '-';
|
||||
}
|
||||
return formatNumber(this.totalMembers);
|
||||
}
|
||||
|
||||
get paidMembersFormatted() {
|
||||
if (this.dashboardStats.memberCounts === null) {
|
||||
return '-';
|
||||
}
|
||||
return formatNumber(this.paidMembers);
|
||||
}
|
||||
|
||||
get freeMembersFormatted() {
|
||||
if (this.dashboardStats.memberCounts === null) {
|
||||
return '-';
|
||||
}
|
||||
return formatNumber(this.freeMembers);
|
||||
}
|
||||
|
||||
get hasTrends() {
|
||||
return this.dashboardStats.memberCounts !== null
|
||||
&& this.dashboardStats.memberCountsTrend !== null
|
||||
&& this.dashboardStats.currentMRR !== null
|
||||
&& this.dashboardStats.currentMRRTrend !== null;
|
||||
}
|
||||
|
||||
get totalMembersTrend() {
|
||||
return this.calculatePercentage(this.dashboardStats.memberCountsTrend.total, this.dashboardStats.memberCounts.total);
|
||||
}
|
||||
|
||||
get paidMembersTrend() {
|
||||
return this.calculatePercentage(this.dashboardStats.memberCountsTrend.paid, this.dashboardStats.memberCounts.paid);
|
||||
}
|
||||
|
||||
get freeMembersTrend() {
|
||||
return this.calculatePercentage(this.dashboardStats.memberCountsTrend.free, this.dashboardStats.memberCounts.free);
|
||||
}
|
||||
|
||||
get hasPaidTiers() {
|
||||
return this.dashboardStats.siteStatus?.hasPaidTiers;
|
||||
}
|
||||
|
||||
calculatePercentage(from, to) {
|
||||
if (from === 0) {
|
||||
if (to > 0) {
|
||||
return 100;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Math.round((to - from) / from * 100);
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
<Dashboard::v5::Parts::Metric
|
||||
@label={{this.chartTitle}} />
|
||||
|
||||
<div class="gh-dashboard5-chart" {{did-insert this.loadCharts}}>
|
||||
{{#if this.loading}}
|
||||
<div class="gh-dashboard5-chart-loading" style={{html-safe (concat "height: " this.chartHeight "px;")}}/>
|
||||
{{else}}
|
||||
<EmberChart
|
||||
@type={{this.chartType}}
|
||||
@data={{this.chartData}}
|
||||
@options={{this.chartOptions}}
|
||||
@height={{this.chartHeight}} />
|
||||
{{/if}}
|
||||
</div>
|
||||
|
172
ghost/admin/app/components/dashboard/v5/charts/paid-breakdown.js
Normal file
172
ghost/admin/app/components/dashboard/v5/charts/paid-breakdown.js
Normal file
@ -0,0 +1,172 @@
|
||||
import Component from '@glimmer/component';
|
||||
import moment from 'moment';
|
||||
import {action} from '@ember/object';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
const DATE_FORMAT = 'D MMM';
|
||||
|
||||
export default class PaidBreakdown extends Component {
|
||||
@service dashboardStats;
|
||||
@service feature;
|
||||
|
||||
@action
|
||||
loadCharts() {
|
||||
// todo: load the new data here
|
||||
}
|
||||
|
||||
get loading() {
|
||||
return this.dashboardStats.memberCountStats === null;
|
||||
}
|
||||
|
||||
get chartTitle() {
|
||||
return 'Paid subscribers';
|
||||
}
|
||||
|
||||
get chartType() {
|
||||
return 'bar';
|
||||
}
|
||||
|
||||
get chartData() {
|
||||
const stats = this.dashboardStats.filledMemberCountStats;
|
||||
const labels = stats.map(stat => stat.date);
|
||||
const newData = stats.map(stat => stat.paidSubscribed);
|
||||
const canceledData = stats.map(stat => -stat.paidCanceled);
|
||||
const netData = stats.map(stat => stat.paidSubscribed - stat.paidCanceled);
|
||||
// const barThickness = (this.daysSelected.value < 90 ? 18 : 7);
|
||||
const barThickness = 4;
|
||||
|
||||
return {
|
||||
labels: labels,
|
||||
datasets: [
|
||||
{
|
||||
type: 'line',
|
||||
data: netData,
|
||||
tension: 0,
|
||||
cubicInterpolationMode: 'monotone',
|
||||
fill: false,
|
||||
pointRadius: 0,
|
||||
pointHitRadius: 10,
|
||||
pointBorderColor: '#14B8FF',
|
||||
pointBackgroundColor: '#14B8FF',
|
||||
pointHoverBackgroundColor: '#14B8FF',
|
||||
pointHoverBorderColor: '#14B8FF',
|
||||
pointHoverRadius: 0,
|
||||
borderColor: 'rgba(189, 197, 204, 0.5)',
|
||||
borderJoinStyle: 'miter',
|
||||
borderWidth: 3
|
||||
}, {
|
||||
data: newData,
|
||||
fill: false,
|
||||
backgroundColor: '#8E42FF',
|
||||
cubicInterpolationMode: 'monotone',
|
||||
barThickness: barThickness,
|
||||
minBarLength: 3
|
||||
}, {
|
||||
data: canceledData,
|
||||
fill: false,
|
||||
backgroundColor: '#FB76B4',
|
||||
cubicInterpolationMode: 'monotone',
|
||||
barThickness: barThickness,
|
||||
minBarLength: 3
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
get chartOptions() {
|
||||
let barColor = this.feature.nightShift ? 'rgba(200, 204, 217, 0.25)' : 'rgba(200, 204, 217, 0.65)';
|
||||
|
||||
return {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
title: {
|
||||
display: false
|
||||
},
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
hover: {
|
||||
onHover: function (e) {
|
||||
e.target.style.cursor = 'pointer';
|
||||
}
|
||||
},
|
||||
tooltips: {
|
||||
intersect: false,
|
||||
mode: 'index',
|
||||
displayColors: false,
|
||||
backgroundColor: '#15171A',
|
||||
xPadding: 7,
|
||||
yPadding: 7,
|
||||
cornerRadius: 5,
|
||||
caretSize: 7,
|
||||
caretPadding: 5,
|
||||
bodyFontSize: 12.5,
|
||||
titleFontSize: 12,
|
||||
titleFontStyle: 'normal',
|
||||
titleFontColor: 'rgba(255, 255, 255, 0.7)',
|
||||
titleMarginBottom: 3,
|
||||
callbacks: {
|
||||
label: (tooltipItems, data) => {
|
||||
let valueText = data.datasets[tooltipItems.datasetIndex].data[tooltipItems.index].toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
|
||||
if (tooltipItems.datasetIndex === 0) {
|
||||
return `Net: ${valueText}`;
|
||||
}
|
||||
|
||||
if (tooltipItems.datasetIndex === 1) {
|
||||
return `New paid: ${valueText}`;
|
||||
}
|
||||
|
||||
if (tooltipItems.datasetIndex === 2) {
|
||||
return `Canceled paid: ${Math.abs(valueText)}`;
|
||||
}
|
||||
},
|
||||
title: (tooltipItems) => {
|
||||
return moment(tooltipItems[0].xLabel).format(DATE_FORMAT);
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
yAxes: [{
|
||||
offset: false,
|
||||
gridLines: {
|
||||
drawTicks: false,
|
||||
display: true,
|
||||
drawBorder: false,
|
||||
color: 'rgba(255, 255, 255, 0.1)',
|
||||
lineWidth: 0,
|
||||
zeroLineColor: barColor,
|
||||
zeroLineWidth: 1
|
||||
},
|
||||
ticks: {
|
||||
display: false,
|
||||
maxTicksLimit: 5,
|
||||
fontColor: '#7C8B9A',
|
||||
padding: 8,
|
||||
precision: 0
|
||||
}
|
||||
}],
|
||||
xAxes: [{
|
||||
offset: true,
|
||||
stacked: true,
|
||||
gridLines: {
|
||||
color: barColor,
|
||||
borderDash: [4,4],
|
||||
display: false,
|
||||
drawBorder: false,
|
||||
drawTicks: false,
|
||||
zeroLineWidth: 1,
|
||||
zeroLineColor: barColor,
|
||||
zeroLineBorderDash: [4,4]
|
||||
},
|
||||
ticks: {
|
||||
display: false
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
get chartHeight() {
|
||||
return 100;
|
||||
}
|
||||
}
|
34
ghost/admin/app/components/dashboard/v5/charts/paid-mix.hbs
Normal file
34
ghost/admin/app/components/dashboard/v5/charts/paid-mix.hbs
Normal file
@ -0,0 +1,34 @@
|
||||
{{#if this.hasMultipleTiers }}
|
||||
<div class="gh-dashboard5-select">
|
||||
<PowerSelect
|
||||
@selected={{this.selectedModeOption}}
|
||||
@options={{this.modeOptions}}
|
||||
@searchEnabled={{false}}
|
||||
@onChange={{this.onSwitchMode}}
|
||||
@triggerComponent="gh-power-select/trigger"
|
||||
@triggerClass="gh-contentfilter-menu-trigger"
|
||||
@dropdownClass="gh-contentfilter-menu-dropdown"
|
||||
@matchTriggerWidth={{false}}
|
||||
as |option|
|
||||
>
|
||||
{{#if option.name}}{{option.name}}{{else}}<span class="red">Unknown option</span>{{/if}}
|
||||
</PowerSelect>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<Dashboard::v5::Parts::Metric
|
||||
@label="Paid mix" />
|
||||
|
||||
<div class="gh-dashboard5-chart" {{did-insert this.loadCharts}}>
|
||||
{{#if this.loading}}
|
||||
<div class="gh-dashboard5-chart-loading" style={{html-safe (concat "height: " this.chartHeight "px;")}}/>
|
||||
{{else}}
|
||||
<div class="gh-dashboard5-chart-container">
|
||||
<EmberChart
|
||||
@type={{this.chartType}}
|
||||
@data={{this.chartData}}
|
||||
@options={{this.chartOptions}}
|
||||
@height={{this.chartHeight}} />
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
151
ghost/admin/app/components/dashboard/v5/charts/paid-mix.js
Normal file
151
ghost/admin/app/components/dashboard/v5/charts/paid-mix.js
Normal file
@ -0,0 +1,151 @@
|
||||
import Component from '@glimmer/component';
|
||||
import {action} from '@ember/object';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
|
||||
const MODE_OPTIONS = [{
|
||||
name: 'Cadence',
|
||||
value: 'cadence'
|
||||
}, {
|
||||
name: 'Tiers',
|
||||
value: 'tiers'
|
||||
}];
|
||||
|
||||
export default class PaidMix extends Component {
|
||||
@service dashboardStats;
|
||||
|
||||
/**
|
||||
* Call this method when you need to fetch new data from the server.
|
||||
*/
|
||||
@action
|
||||
loadCharts() {
|
||||
// The dashboard stats service will take care or reusing and limiting API-requests between charts
|
||||
if (this.mode === 'cadence') {
|
||||
this.dashboardStats.loadPaidMembersByCadence();
|
||||
} else {
|
||||
this.dashboardStats.loadPaidMembersByTier();
|
||||
}
|
||||
}
|
||||
|
||||
@tracked mode = 'cadence';
|
||||
modeOptions = MODE_OPTIONS;
|
||||
|
||||
get selectedModeOption() {
|
||||
return this.modeOptions.find(option => option.value === this.mode);
|
||||
}
|
||||
|
||||
get hasMultipleTiers() {
|
||||
return this.dashboardStats.siteStatus?.hasMultipleTiers;
|
||||
}
|
||||
|
||||
@action
|
||||
onSwitchMode(selected) {
|
||||
this.mode = selected.value;
|
||||
|
||||
if (this.loading) {
|
||||
// We don't have the data yet for the newly selected mode
|
||||
this.loadCharts();
|
||||
}
|
||||
}
|
||||
|
||||
get loading() {
|
||||
if (this.mode === 'cadence') {
|
||||
return this.dashboardStats.paidMembersByCadence === null;
|
||||
}
|
||||
return this.dashboardStats.paidMembersByTier === null;
|
||||
}
|
||||
|
||||
get chartType() {
|
||||
return 'horizontalBar';
|
||||
}
|
||||
|
||||
get chartData() {
|
||||
if (this.mode === 'cadence') {
|
||||
return {
|
||||
labels: ['Monthly', 'Annual'],
|
||||
datasets: [{
|
||||
data: [this.dashboardStats.paidMembersByCadence.monthly, this.dashboardStats.paidMembersByCadence.annual],
|
||||
fill: false,
|
||||
backgroundColor: ['#8E42FF', '#FB76B4', '#E9B0CC', '#F1D5E3'],
|
||||
barThickness: 7
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
const labels = this.dashboardStats.paidMembersByTier.map(stat => stat.tier.name);
|
||||
const data = this.dashboardStats.paidMembersByTier.map(stat => stat.members);
|
||||
|
||||
return {
|
||||
labels,
|
||||
datasets: [{
|
||||
data,
|
||||
fill: false,
|
||||
backgroundColor: ['#8E42FF', '#FB76B4', '#E9B0CC', '#F1D5E3'],
|
||||
barThickness: 7
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
get chartOptions() {
|
||||
return {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
cutoutPercentage: (this.mode === 'cadence' ? 85 : 75),
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
animation: {
|
||||
duration: 0
|
||||
},
|
||||
hover: {
|
||||
onHover: function (e) {
|
||||
e.target.style.cursor = 'pointer';
|
||||
}
|
||||
},
|
||||
tooltips: {
|
||||
intersect: false,
|
||||
mode: 'index',
|
||||
displayColors: false,
|
||||
backgroundColor: '#15171A',
|
||||
xPadding: 7,
|
||||
yPadding: 7,
|
||||
cornerRadius: 5,
|
||||
caretSize: 7,
|
||||
caretPadding: 5,
|
||||
bodyFontSize: 12.5,
|
||||
titleFontSize: 12,
|
||||
titleFontStyle: 'normal',
|
||||
titleFontColor: 'rgba(255, 255, 255, 0.7)',
|
||||
titleMarginBottom: 3
|
||||
},
|
||||
scales: {
|
||||
yAxes: [{
|
||||
gridLines: {
|
||||
display: true,
|
||||
drawBorder: false,
|
||||
color: 'transparent',
|
||||
lineWidth: 0,
|
||||
zeroLineColor: 'transparent',
|
||||
zeroLineWidth: 1
|
||||
},
|
||||
ticks: {
|
||||
display: true
|
||||
}
|
||||
}],
|
||||
xAxes: [{
|
||||
stacked: true,
|
||||
gridLines: {
|
||||
display: false
|
||||
},
|
||||
ticks: {
|
||||
display: false
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
get chartHeight() {
|
||||
return 75;
|
||||
}
|
||||
}
|
@ -1,28 +1,42 @@
|
||||
<div class="gh-dashboard5-metric {{if @center "is-center"}} {{if @reverse "is-reverse"}} {{if @large "is-large"}}">
|
||||
<div class="gh-dashboard5-metric-data">
|
||||
{{#if @reverse}}
|
||||
{{#if @secondary}}
|
||||
{{#if @value}}
|
||||
<div class="gh-dashboard5-metric-value">
|
||||
<div class="gh-dashboard5-metric-value {{if @secondary 'is-secondary'}}">
|
||||
{{@value}}
|
||||
{{#if @trends}}
|
||||
<Dashboard::v5::Parts::Percentage @percentage={{@percentage}}/>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
<h5 class="gh-dashboard5-metric-label">
|
||||
<h5 class="gh-dashboard5-metric-label {{if @secondary 'is-secondary'}}">
|
||||
{{@label}}
|
||||
</h5>
|
||||
{{else}}
|
||||
<h5 class="gh-dashboard5-metric-label">
|
||||
{{@label}}
|
||||
</h5>
|
||||
{{#if @value}}
|
||||
<div class="gh-dashboard5-metric-value">
|
||||
{{@value}}
|
||||
{{#if @trends}}
|
||||
{{#if @reverse}}
|
||||
{{#if @value}}
|
||||
<div class="gh-dashboard5-metric-value">
|
||||
{{@value}}
|
||||
{{#if @trends}}
|
||||
<Dashboard::v5::Parts::Percentage @percentage={{@percentage}}/>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
<h5 class="gh-dashboard5-metric-label">
|
||||
{{@label}}
|
||||
</h5>
|
||||
{{else}}
|
||||
<h5 class="gh-dashboard5-metric-label">
|
||||
{{@label}}
|
||||
</h5>
|
||||
{{#if @value}}
|
||||
<div class="gh-dashboard5-metric-value">
|
||||
{{@value}}
|
||||
{{#if @trends}}
|
||||
<Dashboard::v5::Parts::Percentage @percentage={{@percentage}}/>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
@ -387,7 +387,8 @@ export default class DashboardMocksService extends Service {
|
||||
this.mrrStats = stats.map((s) => {
|
||||
return {
|
||||
date: s.date,
|
||||
mrr: s.tier1 * 500 + s.tier2 * 2500
|
||||
mrr: s.tier1 * 500 + s.tier2 * 2500,
|
||||
currency: 'usd'
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -502,8 +502,8 @@ export default class DashboardStatsService extends Service {
|
||||
}
|
||||
|
||||
const [paid, free] = yield Promise.all([
|
||||
this.membersCountCache.count('subscribed:true+status:paid'),
|
||||
this.membersCountCache.count('subscribed:true+status:-paid')
|
||||
this.membersCountCache.count('subscribed:true+status:-free'),
|
||||
this.membersCountCache.count('subscribed:true+status:free')
|
||||
]);
|
||||
|
||||
this.newsletterSubscribers = {
|
||||
|
@ -1121,3 +1121,11 @@ kbd {
|
||||
.gh-dashboard5 .gh-dashboard5-list-footer {
|
||||
border-color: #394047;
|
||||
}
|
||||
|
||||
.gh-dashboard5 .gh-dashboard5-box.is-secondary {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.gh-dashboard5 .gh-dashboard5-column {
|
||||
border-color: #2b2d31;
|
||||
}
|
||||
|
@ -68,12 +68,13 @@ Dashboard v5 Layout */
|
||||
|
||||
.gh-dashboard5-layout {
|
||||
display: grid;
|
||||
grid-gap: 32px;
|
||||
grid-gap: 24px;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
grid-template-rows: auto auto auto auto;
|
||||
grid-template-rows: auto auto auto auto auto;
|
||||
grid-template-areas:
|
||||
"overview overview overview overview"
|
||||
"anchor anchor anchor anchor"
|
||||
"email engagement email-open-rate email-open-rate"
|
||||
"engagement engagement engagement engagement"
|
||||
"recent-posts recent-posts recent-activity recent-activity"
|
||||
"help help help help";
|
||||
}
|
||||
@ -118,13 +119,43 @@ Dashboard v5 Layout */
|
||||
flex: 1;
|
||||
border: 1px solid var(--whitegrey);
|
||||
padding: 24px;
|
||||
border-radius: 3px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.gh-dashboard5-box.is-secondary {
|
||||
background: #fcfcfc;
|
||||
}
|
||||
|
||||
.gh-dashboard5-columns {
|
||||
flex: 1;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
position: relative;
|
||||
align-items: stretch;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.gh-dashboard5-columns > .gh-dashboard5-column {
|
||||
flex: 1;
|
||||
border-left: 1px solid var(--whitegrey);
|
||||
padding: 4px 24px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.gh-dashboard5-columns > .gh-dashboard5-column:first-child {
|
||||
padding-left: 0;
|
||||
border-left: 0 none;
|
||||
}
|
||||
|
||||
.gh-dashboard5-columns > .gh-dashboard5-column:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.gh-dashboard5-hero {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
@ -163,8 +194,7 @@ Dashboard v5 Layout */
|
||||
}
|
||||
|
||||
.gh-dashboard5-select .ember-power-select-selected-item {
|
||||
font-size: 1.1rem;
|
||||
text-transform: uppercase;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: .3px;
|
||||
line-height: 1em;
|
||||
@ -266,25 +296,14 @@ Dashboard v5 Metric */
|
||||
|
||||
.gh-dashboard5-metric-label {
|
||||
align-items: center;
|
||||
font-size: 1.1rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .2px;
|
||||
font-weight: 500;
|
||||
letter-spacing: .3px;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
line-height: 1em;
|
||||
margin: 0 0 12px;
|
||||
margin: 0 0 10px;
|
||||
padding: 0;
|
||||
color: var(--middarkgrey);
|
||||
color: var(--black);
|
||||
white-space: nowrap;
|
||||
|
||||
font-size: 1.1rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .2px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.gh-dashboard5-metric.is-large .gh-dashboard5-metric-label {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.gh-dashboard5-metric.is-reverse .gh-dashboard5-metric-label {
|
||||
@ -296,11 +315,17 @@ Dashboard v5 Metric */
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.gh-dashboard5-metric-label.is-secondary {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 500;
|
||||
color: var(--middarkgrey);
|
||||
}
|
||||
|
||||
.gh-dashboard5-metric-value {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
font-size: 2.4rem;
|
||||
font-weight: 600;
|
||||
font-size: 2.6rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -.1px;
|
||||
line-height: 1em;
|
||||
white-space: nowrap;
|
||||
@ -309,8 +334,7 @@ Dashboard v5 Metric */
|
||||
}
|
||||
|
||||
.gh-dashboard5-metric.is-large .gh-dashboard5-metric-value {
|
||||
font-size: 3rem;
|
||||
font-weight: 600;
|
||||
font-size: 3.2rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@ -322,6 +346,10 @@ Dashboard v5 Metric */
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.gh-dashboard5-metric-value.is-secondary {
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.gh-dashboard5-metric-extra {
|
||||
text-transform: none;
|
||||
font-weight: 500;
|
||||
@ -430,6 +458,31 @@ Dashboard v5 List */
|
||||
}
|
||||
|
||||
|
||||
/* ---------------------------------
|
||||
Dashboard v5 Section Overview */
|
||||
|
||||
.gh-dashboard5-overview {
|
||||
grid-area: overview;
|
||||
}
|
||||
|
||||
.gh-dashboard5-overview .gh-dashboard5-area {
|
||||
flex-direction: row;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.gh-dashboard5-overview .gh-dashboard5-area > div {
|
||||
flex: 1;
|
||||
border-right: 1px solid var(--whitegrey);
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.gh-dashboard5-overview .gh-dashboard5-area > div:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
|
||||
/* ---------------------------------
|
||||
Dashboard v5 Section Anchor */
|
||||
|
||||
@ -439,7 +492,7 @@ Dashboard v5 Section Anchor */
|
||||
}
|
||||
|
||||
.gh-dashboard5-anchor .gh-dashboard5-box {
|
||||
padding: 20px 24px 8px;
|
||||
padding: 28px 24px 8px;
|
||||
}
|
||||
|
||||
.gh-dashboard5-anchor .gh-dashboard5-stats {
|
||||
@ -507,6 +560,7 @@ Dashboard v5 Section Anchor */
|
||||
|
||||
.gh-dashboard5-anchor .gh-dashboard5-chart {
|
||||
flex-direction: column;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.gh-dashboard5-anchor .gh-dashboard5-chart-ticks {
|
||||
@ -516,6 +570,9 @@ Dashboard v5 Section Anchor */
|
||||
color: var(--midlightgrey);
|
||||
}
|
||||
|
||||
.gh-dashboard5-anchor .gh-dashboard5-columns {
|
||||
margin-top: 48px;
|
||||
}
|
||||
|
||||
/* ---------------------------------
|
||||
Dashboard v5 Section Engagement */
|
||||
@ -529,6 +586,10 @@ Dashboard v5 Section Engagement */
|
||||
padding-top: 28px;
|
||||
}
|
||||
|
||||
.gh-dashboard5-engagement .gh-dashboard5-columns {
|
||||
padding-top: 32px;
|
||||
}
|
||||
|
||||
|
||||
/* ---------------------------------
|
||||
Dashboard v5 Section Email */
|
||||
@ -679,3 +740,18 @@ Dashboard v5 Misc */
|
||||
.gh-dashboard5 .gh-list-header {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.gh-dashboard5-anchor .gh-dashboard5-mix .gh-dashboard5-select {
|
||||
top: -11px;
|
||||
right: -18px;
|
||||
}
|
||||
|
||||
.gh-dashboard5-anchor .gh-dashboard5-mix .gh-dashboard5-chart {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.gh-dashboard5-anchor .gh-dashboard5-mrr .gh-dashboard5-chart {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user