mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-03-25 02:31:59 -05:00
Moving over the new Dashboard to replace the old (#2389)
refs: https://github.com/TryGhost/Team/issues/1631 Co-authored-by: James Morris <moreofmorris@users.noreply.github.com>
This commit is contained in:
parent
8b5b3aa734
commit
8502ebb96a
57 changed files with 2464 additions and 4393 deletions
|
@ -1,7 +1,7 @@
|
|||
<section class="gh-dashboard5-section gh-dashboard5-anchor" {{did-insert this.loadCharts}}>
|
||||
<article class="gh-dashboard5-box">
|
||||
<section class="gh-dashboard-section gh-dashboard-anchor" {{did-insert this.loadCharts}}>
|
||||
<article class="gh-dashboard-box">
|
||||
{{#if this.hasPaidTiers}}
|
||||
<div class="gh-dashboard5-select-title">
|
||||
<div class="gh-dashboard-select-title">
|
||||
<PowerSelect
|
||||
@selected={{this.selectedDisplayOption}}
|
||||
@options={{this.displayOptions}}
|
||||
|
@ -17,7 +17,7 @@
|
|||
</PowerSelect>
|
||||
</div>
|
||||
{{else}}
|
||||
<Dashboard::v5::Parts::Metric
|
||||
<Dashboard::Parts::Metric
|
||||
@label="Total members"
|
||||
@value={{format-number this.totalMembers}}
|
||||
@trends={{this.hasTrends}}
|
||||
|
@ -25,12 +25,12 @@
|
|||
@large={{true}} />
|
||||
{{/if}}
|
||||
|
||||
<div class="gh-dashboard5-hero {{unless this.hasPaidTiers 'is-solo'}}">
|
||||
<div class="gh-dashboard5-chart gh-dashboard5-totals">
|
||||
<div class="gh-dashboard5-chart-container">
|
||||
<div class="gh-dashboard5-chart-box">
|
||||
<div class="gh-dashboard-hero {{unless this.hasPaidTiers 'is-solo'}}">
|
||||
<div class="gh-dashboard-chart gh-dashboard-totals">
|
||||
<div class="gh-dashboard-chart-container">
|
||||
<div class="gh-dashboard-chart-box">
|
||||
{{#if this.loading}}
|
||||
<div class="gh-dashboard5-chart-loading">
|
||||
<div class="gh-dashboard-chart-loading">
|
||||
<div class="gh-loading-spinner"></div>
|
||||
</div>
|
||||
{{else}}
|
||||
|
@ -41,28 +41,28 @@
|
|||
@height={{200}} />
|
||||
{{/if}}
|
||||
</div>
|
||||
<div id="gh-dashboard5-anchor-tooltip" class="gh-dashboard5-tooltip">
|
||||
<div class="gh-dashboard5-tooltip-label">
|
||||
<div id="gh-dashboard-anchor-tooltip" class="gh-dashboard-tooltip">
|
||||
<div class="gh-dashboard-tooltip-label">
|
||||
-
|
||||
</div>
|
||||
<div class="gh-dashboard5-tooltip-value">
|
||||
<div class="gh-dashboard-tooltip-value">
|
||||
<span class="indicator line"></span>
|
||||
<span class="value">–</span>
|
||||
<span class="metric">{{this.selectedDisplayOption.name}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-dashboard5-chart-ticks">
|
||||
<span id="gh-dashboard5-anchor-date-start">-</span>
|
||||
<span id="gh-dashboard5-anchor-date-end">-</span>
|
||||
<div class="gh-dashboard-chart-ticks">
|
||||
<span id="gh-dashboard-anchor-date-start">-</span>
|
||||
<span id="gh-dashboard-anchor-date-end">-</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if this.hasPaidTiers}}
|
||||
<article class="gh-dashboard5-minicharts">
|
||||
<Dashboard::v5::Charts::PaidMrr />
|
||||
<Dashboard::v5::Charts::PaidBreakdown />
|
||||
<Dashboard::v5::Charts::PaidMix />
|
||||
<article class="gh-dashboard-minicharts">
|
||||
<Dashboard::Charts::PaidMrr />
|
||||
<Dashboard::Charts::PaidBreakdown />
|
||||
<Dashboard::Charts::PaidMix />
|
||||
</article>
|
||||
{{/if}}
|
||||
</div>
|
|
@ -236,7 +236,7 @@ export default class Anchor extends Component {
|
|||
mode: 'index',
|
||||
custom: function (tooltip) {
|
||||
// get tooltip element
|
||||
const tooltipEl = document.getElementById('gh-dashboard5-anchor-tooltip');
|
||||
const tooltipEl = document.getElementById('gh-dashboard-anchor-tooltip');
|
||||
const chartContainerEl = tooltipEl.parentElement;
|
||||
const chartWidth = chartContainerEl.offsetWidth;
|
||||
const tooltipWidth = tooltipEl.offsetWidth;
|
||||
|
@ -262,11 +262,11 @@ export default class Anchor extends Component {
|
|||
callbacks: {
|
||||
label: (tooltipItems, data) => {
|
||||
const value = data.datasets[tooltipItems.datasetIndex].data[tooltipItems.index].toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
document.querySelector('#gh-dashboard5-anchor-tooltip .gh-dashboard5-tooltip-value .value').innerHTML = value;
|
||||
document.querySelector('#gh-dashboard-anchor-tooltip .gh-dashboard-tooltip-value .value').innerHTML = value;
|
||||
},
|
||||
title: (tooltipItems) => {
|
||||
const value = moment(tooltipItems[0].xLabel).format(DATE_FORMAT);
|
||||
document.querySelector('#gh-dashboard5-anchor-tooltip .gh-dashboard5-tooltip-label').innerHTML = value;
|
||||
document.querySelector('#gh-dashboard-anchor-tooltip .gh-dashboard-tooltip-label').innerHTML = value;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -305,10 +305,10 @@ export default class Anchor extends Component {
|
|||
autoSkip: false,
|
||||
callback: function (value, index, values) {
|
||||
if (index === 0) {
|
||||
document.getElementById('gh-dashboard5-anchor-date-start').innerHTML = moment(value).format(DATE_FORMAT);
|
||||
document.getElementById('gh-dashboard-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);
|
||||
document.getElementById('gh-dashboard-anchor-date-end').innerHTML = moment(value).format(DATE_FORMAT);
|
||||
}
|
||||
|
||||
if (activeDays === (30 + 1)) {
|
|
@ -1,25 +1,25 @@
|
|||
<section class="gh-dashboard5-section gh-dashboard5-engagement">
|
||||
<article {{did-insert this.loadCharts}} class="gh-dashboard5-box">
|
||||
<Dashboard::v5::Parts::Metric
|
||||
<section class="gh-dashboard-section gh-dashboard-engagement">
|
||||
<article {{did-insert this.loadCharts}} class="gh-dashboard-box">
|
||||
<Dashboard::Parts::Metric
|
||||
@label="Engagement" />
|
||||
|
||||
<div class="gh-dashboard5-columns">
|
||||
<div class="gh-dashboard5-column gh-dashboard5-engagement-30days">
|
||||
<Dashboard::v5::Parts::Metric
|
||||
<div class="gh-dashboard-columns">
|
||||
<div class="gh-dashboard-column gh-dashboard-engagement-30days">
|
||||
<Dashboard::Parts::Metric
|
||||
@label="Engaged in the last 30 days"
|
||||
@value={{this.data30Days}}
|
||||
@secondary={{true}}
|
||||
/>
|
||||
</div>
|
||||
<div class="gh-dashboard5-column gh-dashboard5-engagement-7days">
|
||||
<Dashboard::v5::Parts::Metric
|
||||
<div class="gh-dashboard-column gh-dashboard-engagement-7days">
|
||||
<Dashboard::Parts::Metric
|
||||
@label="Engaged in the last 7 days"
|
||||
@value={{this.data7Days}}
|
||||
@secondary={{true}}
|
||||
/>
|
||||
</div>
|
||||
<div class="gh-dashboard5-column gh-dashboard5-engagement-subscribers">
|
||||
<Dashboard::v5::Parts::Metric
|
||||
<div class="gh-dashboard-column gh-dashboard-engagement-subscribers">
|
||||
<Dashboard::Parts::Metric
|
||||
@label="Newsletter subscribers"
|
||||
@value={{this.dataSubscribers}}
|
||||
@secondary={{true}}
|
||||
|
@ -29,7 +29,7 @@
|
|||
</article>
|
||||
|
||||
{{#if this.hasPaidTiers}}
|
||||
<div class="gh-dashboard5-select">
|
||||
<div class="gh-dashboard-select">
|
||||
<PowerSelect
|
||||
@selected={{this.selectedStatusOption}}
|
||||
@options={{this.statusOptions}}
|
|
@ -1,6 +1,6 @@
|
|||
import Component from '@glimmer/component';
|
||||
import {action} from '@ember/object';
|
||||
import {formatNumber} from '../../../../helpers/format-number';
|
||||
import {formatNumber} from '../../../helpers/format-number';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
|
|
@ -1,24 +1,24 @@
|
|||
<section class="gh-dashboard5-section gh-dashboard5-overview {{unless this.isTotalMembersMoreThanZero 'is-hidden'}}">
|
||||
<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
|
||||
<section class="gh-dashboard-section gh-dashboard-overview {{unless this.isTotalMembersMoreThanZero 'is-hidden'}}">
|
||||
<article {{did-insert this.loadCharts}} class="gh-dashboard-box is-secondary">
|
||||
<div class="gh-dashboard-columns">
|
||||
<div class="gh-dashboard-column">
|
||||
<Dashboard::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
|
||||
<div class="gh-dashboard-column">
|
||||
<Dashboard::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
|
||||
<div class="gh-dashboard-column">
|
||||
<Dashboard::Parts::Metric
|
||||
@label={{gh-pluralize this.freeMembers "Free member" without-count=true}}
|
||||
@value={{this.freeMembersFormatted}}
|
||||
@trends={{this.hasTrends}}
|
|
@ -1,11 +1,10 @@
|
|||
import Component from '@glimmer/component';
|
||||
import {action} from '@ember/object';
|
||||
import {formatNumber} from '../../../../helpers/format-number';
|
||||
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() {
|
|
@ -0,0 +1,43 @@
|
|||
<div class="gh-dashboard-minichart gh-dashboard-breakdown">
|
||||
<div class="gh-dashboard-content">
|
||||
<div class="gh-dashboard-data">
|
||||
<Dashboard::Parts::Metric
|
||||
@label={{this.chartTitle}} />
|
||||
|
||||
<div class="gh-dashboard-legend">
|
||||
<div class="gh-dashboard-legend-item">New</div>
|
||||
<div class="gh-dashboard-legend-item">Canceled</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-dashboard-chart" {{did-insert this.loadCharts}}>
|
||||
{{#if this.loading}}
|
||||
<div class="gh-dashboard-chart-loading">
|
||||
<div class="gh-loading-spinner"></div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="gh-dashboard-chart-container">
|
||||
<div class="gh-dashboard-chart-box">
|
||||
<EmberChart
|
||||
@type={{this.chartType}}
|
||||
@data={{this.chartData}}
|
||||
@options={{this.chartOptions}}
|
||||
@height={{110}} />
|
||||
</div>
|
||||
|
||||
<div id="gh-dashboard-breakdown-tooltip" class="gh-dashboard-tooltip">
|
||||
<div class="gh-dashboard-tooltip-label">
|
||||
-
|
||||
</div>
|
||||
<div class="gh-dashboard-tooltip-value">
|
||||
<div class="gh-dashboard-tooltip-value-1"><span class="indicator solid"></span><span class="value"></span></div>
|
||||
<div class="gh-dashboard-tooltip-value-1"><span class="metric">New</span></div>
|
||||
<div class="gh-dashboard-tooltip-value-2"><span class="indicator solid"></span><span class="value"></span></div>
|
||||
<div class="gh-dashboard-tooltip-value-2"><span class="metric">Canceled</span></div>
|
||||
<div class="gh-dashboard-tooltip-value-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -262,7 +262,7 @@ export default class PaidBreakdown extends Component {
|
|||
mode: 'index',
|
||||
custom: function (tooltip) {
|
||||
// get tooltip element
|
||||
const tooltipEl = document.getElementById('gh-dashboard5-breakdown-tooltip');
|
||||
const tooltipEl = document.getElementById('gh-dashboard-breakdown-tooltip');
|
||||
const chartContainerEl = tooltipEl.parentElement;
|
||||
const chartWidth = chartContainerEl.offsetWidth;
|
||||
const tooltipWidth = tooltipEl.offsetWidth;
|
||||
|
@ -290,15 +290,15 @@ export default class PaidBreakdown extends Component {
|
|||
label: (tooltipItems, data) => {
|
||||
// new data
|
||||
let newValue = parseInt(data.datasets[0].data[tooltipItems.index].toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','));
|
||||
document.querySelector('#gh-dashboard5-breakdown-tooltip .gh-dashboard5-tooltip-value-1 .value').innerHTML = `${newValue}`;
|
||||
document.querySelector('#gh-dashboard-breakdown-tooltip .gh-dashboard-tooltip-value-1 .value').innerHTML = `${newValue}`;
|
||||
|
||||
// canceld data
|
||||
let canceledValue = Math.abs(parseInt(data.datasets[1].data[tooltipItems.index].toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')));
|
||||
document.querySelector('#gh-dashboard5-breakdown-tooltip .gh-dashboard5-tooltip-value-2 .value').innerHTML = `${canceledValue}`;
|
||||
document.querySelector('#gh-dashboard-breakdown-tooltip .gh-dashboard-tooltip-value-2 .value').innerHTML = `${canceledValue}`;
|
||||
},
|
||||
title: (tooltipItems) => {
|
||||
const value = moment(tooltipItems[0].xLabel).format(DATE_FORMAT);
|
||||
document.querySelector('#gh-dashboard5-breakdown-tooltip .gh-dashboard5-tooltip-label').innerHTML = value;
|
||||
document.querySelector('#gh-dashboard-breakdown-tooltip .gh-dashboard-tooltip-label').innerHTML = value;
|
||||
}
|
||||
}
|
||||
},
|
|
@ -1,24 +1,24 @@
|
|||
<div class="gh-dashboard5-minichart gh-dashboard5-mix {{if this.isChartTiers 'is-tiers'}}">
|
||||
<div class="gh-dashboard5-content">
|
||||
<div class="gh-dashboard5-data">
|
||||
<Dashboard::v5::Parts::Metric
|
||||
<div class="gh-dashboard-minichart gh-dashboard-mix {{if this.isChartTiers 'is-tiers'}}">
|
||||
<div class="gh-dashboard-content">
|
||||
<div class="gh-dashboard-data">
|
||||
<Dashboard::Parts::Metric
|
||||
@label="Paid mix" />
|
||||
|
||||
{{#if this.isChartCadence}}
|
||||
<div class="gh-dashboard5-legend">
|
||||
<div class="gh-dashboard5-legend-item">Monthly</div>
|
||||
<div class="gh-dashboard5-legend-item">Annual</div>
|
||||
<div class="gh-dashboard-legend">
|
||||
<div class="gh-dashboard-legend-item">Monthly</div>
|
||||
<div class="gh-dashboard-legend-item">Annual</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="gh-dashboard5-chart {{if this.isChartCadence "narrow"}}" {{did-insert this.loadCharts}}>
|
||||
<div class="gh-dashboard-chart {{if this.isChartCadence "narrow"}}" {{did-insert this.loadCharts}}>
|
||||
{{#if this.loading}}
|
||||
<div class="gh-dashboard5-chart-loading">
|
||||
<div class="gh-dashboard-chart-loading">
|
||||
<div class="gh-loading-spinner"></div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="gh-dashboard5-chart-container">
|
||||
<div class="gh-dashboard5-chart-box">
|
||||
<div class="gh-dashboard-chart-container">
|
||||
<div class="gh-dashboard-chart-box">
|
||||
<EmberChart
|
||||
@type={{this.chartType}}
|
||||
@data={{this.chartData}}
|
||||
|
@ -26,8 +26,8 @@
|
|||
@height={{110}} />
|
||||
</div>
|
||||
|
||||
<div id="gh-dashboard5-mix-tooltip" class="gh-dashboard5-tooltip">
|
||||
<div class="gh-dashboard5-tooltip-value">
|
||||
<div id="gh-dashboard-mix-tooltip" class="gh-dashboard-tooltip">
|
||||
<div class="gh-dashboard-tooltip-value">
|
||||
-
|
||||
</div>
|
||||
</div>
|
||||
|
@ -37,7 +37,7 @@
|
|||
</div>
|
||||
|
||||
{{#if this.hasMultipleTiers }}
|
||||
<div class="gh-dashboard5-select">
|
||||
<div class="gh-dashboard-select">
|
||||
<PowerSelect
|
||||
@selected={{this.selectedModeOption}}
|
||||
@options={{this.modeOptions}}
|
|
@ -436,7 +436,7 @@ export default class PaidMix extends Component {
|
|||
mode: 'single',
|
||||
custom: function (tooltip) {
|
||||
// get tooltip element
|
||||
const tooltipEl = document.getElementById('gh-dashboard5-mix-tooltip');
|
||||
const tooltipEl = document.getElementById('gh-dashboard-mix-tooltip');
|
||||
const chartContainerEl = tooltipEl.parentElement;
|
||||
const chartWidth = chartContainerEl.offsetWidth;
|
||||
const tooltipWidth = tooltipEl.offsetWidth;
|
||||
|
@ -460,7 +460,7 @@ export default class PaidMix extends Component {
|
|||
},
|
||||
callbacks: {
|
||||
label: (tooltipItems, data) => {
|
||||
const tooltipTextEl = document.querySelector('#gh-dashboard5-mix-tooltip .gh-dashboard5-tooltip-value');
|
||||
const tooltipTextEl = document.querySelector('#gh-dashboard-mix-tooltip .gh-dashboard-tooltip-value');
|
||||
const label = data.datasets[tooltipItems.datasetIndex].label || '';
|
||||
var value = data.datasets[tooltipItems.datasetIndex].data[tooltipItems.index] || 0;
|
||||
if (value < 0) {
|
|
@ -1,20 +1,20 @@
|
|||
<div class="gh-dashboard5-minichart gh-dashboard5-mrr">
|
||||
<div class="gh-dashboard5-content">
|
||||
<div class="gh-dashboard5-data">
|
||||
<Dashboard::v5::Parts::Metric
|
||||
<div class="gh-dashboard-minichart gh-dashboard-mrr">
|
||||
<div class="gh-dashboard-content">
|
||||
<div class="gh-dashboard-data">
|
||||
<Dashboard::Parts::Metric
|
||||
@label={{this.chartTitle}}
|
||||
@value="{{this.currentMRRFormatted}}"
|
||||
@trends={{this.hasTrends}}
|
||||
@percentage={{this.mrrTrend}} />
|
||||
</div>
|
||||
<div class="gh-dashboard5-chart">
|
||||
<div class="gh-dashboard-chart">
|
||||
{{#if this.loading}}
|
||||
<div class="gh-dashboard5-chart-loading">
|
||||
<div class="gh-dashboard-chart-loading">
|
||||
<div class="gh-loading-spinner"></div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="gh-dashboard5-chart-container">
|
||||
<div class="gh-dashboard5-chart-box">
|
||||
<div class="gh-dashboard-chart-container">
|
||||
<div class="gh-dashboard-chart-box">
|
||||
<EmberChart
|
||||
@type={{this.chartType}}
|
||||
@data={{this.chartData}}
|
||||
|
@ -22,11 +22,11 @@
|
|||
@height={{110}} />
|
||||
</div>
|
||||
|
||||
<div id="gh-dashboard5-mrr-tooltip" class="gh-dashboard5-tooltip">
|
||||
<div class="gh-dashboard5-tooltip-label">
|
||||
<div id="gh-dashboard-mrr-tooltip" class="gh-dashboard-tooltip">
|
||||
<div class="gh-dashboard-tooltip-label">
|
||||
-
|
||||
</div>
|
||||
<div class="gh-dashboard5-tooltip-value">
|
||||
<div class="gh-dashboard-tooltip-value">
|
||||
<span class="indicator line"></span>
|
||||
<span class="value">-</span>
|
||||
<span class="metric">MRR</span>
|
|
@ -3,7 +3,7 @@
|
|||
import Component from '@glimmer/component';
|
||||
import moment from 'moment';
|
||||
import {getSymbol} from 'ghost-admin/utils/currency';
|
||||
import {ghPriceAmount} from '../../../../helpers/gh-price-amount';
|
||||
import {ghPriceAmount} from '../../../helpers/gh-price-amount';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
const DATE_FORMAT = 'D MMM, YYYY';
|
||||
|
@ -170,7 +170,7 @@ export default class PaidMrr extends Component {
|
|||
mode: 'index',
|
||||
custom: function (tooltip) {
|
||||
// get tooltip element
|
||||
const tooltipEl = document.getElementById('gh-dashboard5-mrr-tooltip');
|
||||
const tooltipEl = document.getElementById('gh-dashboard-mrr-tooltip');
|
||||
const chartContainerEl = tooltipEl.parentElement;
|
||||
const chartWidth = chartContainerEl.offsetWidth;
|
||||
const tooltipWidth = tooltipEl.offsetWidth;
|
||||
|
@ -195,11 +195,11 @@ export default class PaidMrr extends Component {
|
|||
callbacks: {
|
||||
label: (tooltipItems, data) => {
|
||||
const value = `${that.mrrCurrencySymbol}${ghPriceAmount(data.datasets[tooltipItems.datasetIndex].data[tooltipItems.index], {cents: false})}`;
|
||||
document.querySelector('#gh-dashboard5-mrr-tooltip .gh-dashboard5-tooltip-value .value').innerHTML = value;
|
||||
document.querySelector('#gh-dashboard-mrr-tooltip .gh-dashboard-tooltip-value .value').innerHTML = value;
|
||||
},
|
||||
title: (tooltipItems) => {
|
||||
const value = moment(tooltipItems[0].xLabel).format(DATE_FORMAT);
|
||||
document.querySelector('#gh-dashboard5-mrr-tooltip .gh-dashboard5-tooltip-label').innerHTML = value;
|
||||
document.querySelector('#gh-dashboard-mrr-tooltip .gh-dashboard-tooltip-label').innerHTML = value;
|
||||
}
|
||||
}
|
||||
},
|
|
@ -1,37 +1,37 @@
|
|||
<section class="gh-dashboard5-section gh-dashboard5-recents" {{did-insert this.loadPosts}}>
|
||||
<article class="gh-dashboard5-box">
|
||||
<div class="gh-dashboard5-tabs">
|
||||
<button type="button" class="gh-dashboard5-tab {{if this.postsTabSelected 'is-selected'}}" {{on "click" this.changeTabToPosts}}>
|
||||
<Dashboard::v5::Parts::Metric
|
||||
<section class="gh-dashboard-section gh-dashboard-recents" {{did-insert this.loadPosts}}>
|
||||
<article class="gh-dashboard-box">
|
||||
<div class="gh-dashboard-tabs">
|
||||
<button type="button" class="gh-dashboard-tab {{if this.postsTabSelected 'is-selected'}}" {{on "click" this.changeTabToPosts}}>
|
||||
<Dashboard::Parts::Metric
|
||||
@label="Recent posts" />
|
||||
</button>
|
||||
{{#if this.areMembersEnabled}}
|
||||
<button type="button" class="gh-dashboard5-tab {{if this.activityTabSelected 'is-selected'}}" {{on "click" this.changeTabToActivity}}>
|
||||
<Dashboard::v5::Parts::Metric
|
||||
<button type="button" class="gh-dashboard-tab {{if this.activityTabSelected 'is-selected'}}" {{on "click" this.changeTabToActivity}}>
|
||||
<Dashboard::Parts::Metric
|
||||
@label="Member activity" />
|
||||
</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{#if this.postsTabSelected}}
|
||||
<div class="gh-dashboard5-recents-posts gh-dashboard5-list {{unless this.areNewslettersEnabled 'is-single'}}">
|
||||
<div class="gh-dashboard5-list-header">
|
||||
<div class="gh-dashboard5-list-title">Title</div>
|
||||
<div class="gh-dashboard-recents-posts gh-dashboard-list {{unless this.areNewslettersEnabled 'is-single'}}">
|
||||
<div class="gh-dashboard-list-header">
|
||||
<div class="gh-dashboard-list-title">Title</div>
|
||||
{{#if this.areNewslettersEnabled}}
|
||||
<div class="gh-dashboard5-list-title">Sends</div>
|
||||
<div class="gh-dashboard5-list-title">Open rate</div>
|
||||
<div class="gh-dashboard-list-title">Sends</div>
|
||||
<div class="gh-dashboard-list-title">Open rate</div>
|
||||
{{else}}
|
||||
<div class="gh-dashboard5-list-title">Published</div>
|
||||
<div class="gh-dashboard-list-title">Published</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="gh-dashboard5-list-body">
|
||||
<div class="gh-dashboard-list-body">
|
||||
{{#each this.posts as |post|}}
|
||||
<LinkTo class="gh-dashboard5-list-item permalink" @route="editor.edit" @models={{array post.displayName post.id}}>
|
||||
<div class="gh-dashboard5-list-item-sub">
|
||||
<span class="gh-dashboard5-list-text">{{post.title}}</span>
|
||||
<LinkTo class="gh-dashboard-list-item permalink" @route="editor.edit" @models={{array post.displayName post.id}}>
|
||||
<div class="gh-dashboard-list-item-sub">
|
||||
<span class="gh-dashboard-list-text">{{post.title}}</span>
|
||||
</div>
|
||||
{{#if this.areNewslettersEnabled}}
|
||||
<div class="gh-dashboard5-list-item-sub">
|
||||
<span class="gh-dashboard5-metric-minivalue {{unless post.email "na"}}">
|
||||
<div class="gh-dashboard-list-item-sub">
|
||||
<span class="gh-dashboard-metric-minivalue {{unless post.email "na"}}">
|
||||
{{#if post.email}}
|
||||
{{format-number post.email.emailCount}}
|
||||
{{else}}
|
||||
|
@ -39,43 +39,43 @@
|
|||
{{/if}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="gh-dashboard5-list-item-sub">
|
||||
<span class="gh-dashboard5-rate-bar">
|
||||
<div class="gh-dashboard-list-item-sub">
|
||||
<span class="gh-dashboard-rate-bar">
|
||||
{{#if post.email}}
|
||||
<span class="gh-dashboard5-metric-minivalue">{{post.email.openRate}}%</span>
|
||||
<span class="gh-dashboard5-rate-amount"><span style={{html-safe (concat "width: " post.email.openRate "%;")}}/></span>
|
||||
<span class="gh-dashboard-metric-minivalue">{{post.email.openRate}}%</span>
|
||||
<span class="gh-dashboard-rate-amount"><span style={{html-safe (concat "width: " post.email.openRate "%;")}}/></span>
|
||||
{{else}}
|
||||
<span class="gh-dashboard5-metric-minivalue na">—</span>
|
||||
<span class="gh-dashboard-metric-minivalue na">—</span>
|
||||
{{/if}}
|
||||
</span>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="gh-dashboard5-list-item-sub">
|
||||
<span class="gh-dashboard5-list-subtext">{{moment-format post.published_at "D MMM YYYY HH:mm"}}</span>
|
||||
<div class="gh-dashboard-list-item-sub">
|
||||
<span class="gh-dashboard-list-subtext">{{moment-format post.published_at "D MMM YYYY HH:mm"}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
</LinkTo>
|
||||
{{else}}
|
||||
<div class="gh-dashboard5-list-empty">
|
||||
<div class="gh-dashboard-list-empty">
|
||||
<p>No published posts yet.</p>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
<div class="gh-dashboard5-list-footer">
|
||||
<div class="gh-dashboard-list-footer">
|
||||
<LinkTo @route="posts" @query={{reset-query-params "posts"}}>See all posts →</LinkTo>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="gh-dashboard5-recents-activity gh-dashboard5-list" data-test-dashboard-member-activity>
|
||||
<div class="gh-dashboard5-list-header">
|
||||
<div class="gh-dashboard5-list-title">Member</div>
|
||||
<div class="gh-dashboard5-list-title">Event</div>
|
||||
<div class="gh-dashboard5-list-title">Time</div>
|
||||
<div class="gh-dashboard-recents-activity gh-dashboard-list" data-test-dashboard-member-activity>
|
||||
<div class="gh-dashboard-list-header">
|
||||
<div class="gh-dashboard-list-title">Member</div>
|
||||
<div class="gh-dashboard-list-title">Event</div>
|
||||
<div class="gh-dashboard-list-title">Time</div>
|
||||
</div>
|
||||
<div class="gh-dashboard5-list-body">
|
||||
<div class="gh-dashboard-list-body">
|
||||
{{#let (members-event-fetcher filter=(members-event-filter excludeEmailEvents=true) pageSize=5) as |eventsFetcher|}}
|
||||
{{#if eventsFetcher.isError}}
|
||||
<div class="gh-dashboard5-list-error">
|
||||
<div class="gh-dashboard-list-error">
|
||||
<p>There was an error loading events</p>
|
||||
{{#if eventsFetcher.errorMessage}}
|
||||
<code>{{eventsFetcher.errorMessage}}</code>
|
||||
|
@ -84,41 +84,41 @@
|
|||
{{/if}}
|
||||
|
||||
{{#if eventsFetcher.isLoading}}
|
||||
<div class="gh-dashboard5-list-loading">
|
||||
<div class="gh-dashboard-list-loading">
|
||||
<div class="gh-loading-spinner"></div>
|
||||
</div>
|
||||
{{else}}
|
||||
{{#if eventsFetcher.data}}
|
||||
{{#each eventsFetcher.data as |event|}}
|
||||
{{#let (parse-member-event event eventsFetcher.hasMultipleNewsletters) as |parsedEvent|}}
|
||||
<LinkTo class="gh-dashboard5-list-item member-details" @route="member" @model="{{parsedEvent.memberId}}" data-test-dashboard-member-activity-item>
|
||||
<div class="gh-dashboard5-list-item-sub">
|
||||
<LinkTo class="gh-dashboard-list-item member-details" @route="member" @model="{{parsedEvent.memberId}}" data-test-dashboard-member-activity-item>
|
||||
<div class="gh-dashboard-list-item-sub">
|
||||
<GhMemberAvatar @member={{parsedEvent.member}} @containerClass="w8 h8 mr3 flex-shrink-0" />
|
||||
<span class="gh-dashboard5-list-text">{{parsedEvent.subject}}</span>
|
||||
<span class="gh-dashboard-list-text">{{parsedEvent.subject}}</span>
|
||||
</div>
|
||||
<div class="gh-dashboard5-list-item-sub">
|
||||
<div class="gh-dashboard-list-item-sub">
|
||||
{{svg-jar parsedEvent.icon}}
|
||||
<span class="gh-dashboard5-list-subtext">
|
||||
<span class="gh-dashboard-list-subtext">
|
||||
{{capitalize-first-letter parsedEvent.action}}
|
||||
{{parsedEvent.object}}
|
||||
{{parsedEvent.info}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="gh-dashboard5-list-item-sub">
|
||||
<span class="gh-dashboard5-list-subtext">{{moment-format event.timestamp "D MMM YYYY HH:mm"}}</span>
|
||||
<div class="gh-dashboard-list-item-sub">
|
||||
<span class="gh-dashboard-list-subtext">{{moment-format event.timestamp "D MMM YYYY HH:mm"}}</span>
|
||||
</div>
|
||||
</LinkTo>
|
||||
{{/let}}
|
||||
{{/each}}
|
||||
{{else}}
|
||||
<div class="gh-dashboard5-list-empty" data-test-no-member-activities>
|
||||
<div class="gh-dashboard-list-empty" data-test-no-member-activities>
|
||||
<p>No activity yet.</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/let}}
|
||||
</div>
|
||||
<div class="gh-dashboard5-list-footer">
|
||||
<div class="gh-dashboard-list-footer">
|
||||
<LinkTo @route="members-activity" @query={{reset-query-params "members-activity"}}>See all activity →</LinkTo>
|
||||
</div>
|
||||
</div>
|
|
@ -5,13 +5,9 @@ import {tracked} from '@glimmer/tracking';
|
|||
|
||||
export default class Recents extends Component {
|
||||
@service store;
|
||||
@service feature;
|
||||
@service session;
|
||||
@service settings;
|
||||
@service dashboardStats;
|
||||
|
||||
@tracked selected = 'posts';
|
||||
|
||||
@tracked posts = [];
|
||||
|
||||
@action
|
||||
|
@ -37,21 +33,6 @@ export default class Recents extends Component {
|
|||
return (this.selected === 'activity');
|
||||
}
|
||||
|
||||
get shouldDisplay() {
|
||||
if (this.feature.improvedOnboarding) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const isOwner = this.session.user?.isOwnerOnly;
|
||||
const hasCompletedLaunchWizard = this.settings.get('editorIsLaunchComplete');
|
||||
|
||||
if (isOwner && !hasCompletedLaunchWizard) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
get areMembersEnabled() {
|
||||
return this.dashboardStats.siteStatus?.membersEnabled;
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
<section class="gh-dashboard5-layout" {{did-insert this.onInsert}}>
|
||||
{{#if this.isLoading }}
|
||||
<GhLoadingSpinner />
|
||||
{{else}}
|
||||
{{#if this.areMembersEnabled}}
|
||||
{{#if this.hasPaidTiers}}
|
||||
<Dashboard::V5::Charts::Overview />
|
||||
{{/if}}
|
||||
|
||||
<div class="gh-dashboard5-group {{if this.isTotalMembersZero 'is-zero'}}">
|
||||
<Dashboard::V5::Charts::Anchor />
|
||||
|
||||
{{#if this.areNewslettersEnabled}}
|
||||
<Dashboard::V5::Charts::Engagement />
|
||||
{{/if}}
|
||||
|
||||
{{#if this.isTotalMembersZero}}
|
||||
<Dashboard::V5::Parts::Zero />
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<Dashboard::V5::Charts::Recents />
|
||||
|
||||
<div class="gh-dashboard5-split gh-dashboard5-box is-secondary">
|
||||
<Dashboard::V5::Resources::Resources />
|
||||
<Dashboard::V5::Resources::Newsletter />
|
||||
</div>
|
||||
<div class="gh-dashboard5-split">
|
||||
<Dashboard::V5::Resources::StaffPicks />
|
||||
<Dashboard::V5::Resources::WhatsNew />
|
||||
<Dashboard::V5::Resources::Community />
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#unless this.isTotalMembersZero}}
|
||||
<div class="gh-dashboard5-select">
|
||||
<PowerSelect
|
||||
@selected={{this.selectedDaysOption}}
|
||||
@options={{this.daysOptions}}
|
||||
@searchEnabled={{false}}
|
||||
@onChange={{this.onDaysChange}}
|
||||
@triggerComponent="gh-power-select/trigger"
|
||||
@triggerClass="gh-contentfilter-menu-trigger"
|
||||
@dropdownClass="gh-contentfilter-menu-dropdown is-narrow"
|
||||
@matchTriggerWidth={{false}}
|
||||
as |option|
|
||||
>
|
||||
{{#if option.name}}{{option.name}}{{else}}<span class="red">Unknown option</span>{{/if}}
|
||||
</PowerSelect>
|
||||
</div>
|
||||
{{/unless}}
|
||||
</section>
|
||||
|
||||
{{#if (enable-developer-experiments)}}
|
||||
<Dashboard::V5::Prototype::ControlPanel />
|
||||
{{/if}}
|
|
@ -1,67 +0,0 @@
|
|||
import Component from '@glimmer/component';
|
||||
import {action} from '@ember/object';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
// Options 30 and 90 need an extra day to be able to distribute ticks/gridlines evenly
|
||||
const DAYS_OPTIONS = [{
|
||||
name: '7 Days',
|
||||
value: 7
|
||||
}, {
|
||||
name: '30 Days',
|
||||
value: 30 + 1
|
||||
}, {
|
||||
name: '90 Days',
|
||||
value: 90 + 1
|
||||
}];
|
||||
|
||||
export default class DashboardDashboardV5Component extends Component {
|
||||
@service dashboardStats;
|
||||
|
||||
daysOptions = DAYS_OPTIONS;
|
||||
|
||||
@action
|
||||
onInsert() {
|
||||
this.dashboardStats.loadSiteStatus();
|
||||
}
|
||||
|
||||
@action
|
||||
onDaysChange(selected) {
|
||||
this.days = selected.value;
|
||||
}
|
||||
|
||||
get days() {
|
||||
return this.dashboardStats.chartDays;
|
||||
}
|
||||
|
||||
set days(days) {
|
||||
this.dashboardStats.chartDays = days;
|
||||
}
|
||||
|
||||
get selectedDaysOption() {
|
||||
return this.daysOptions.find(d => d.value === this.days);
|
||||
}
|
||||
|
||||
get isLoading() {
|
||||
return this.dashboardStats.siteStatus === null;
|
||||
}
|
||||
|
||||
get totalMembers() {
|
||||
return this.dashboardStats.memberCounts?.total ?? 0;
|
||||
}
|
||||
|
||||
get isTotalMembersZero() {
|
||||
return this.dashboardStats.memberCounts && this.totalMembers === 0;
|
||||
}
|
||||
|
||||
get hasPaidTiers() {
|
||||
return this.dashboardStats.siteStatus?.hasPaidTiers;
|
||||
}
|
||||
|
||||
get areNewslettersEnabled() {
|
||||
return this.dashboardStats.siteStatus?.newslettersEnabled;
|
||||
}
|
||||
|
||||
get areMembersEnabled() {
|
||||
return this.dashboardStats.siteStatus?.membersEnabled;
|
||||
}
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
{{#if this.shouldDisplay}}
|
||||
<div class="gh-dashboard-box activity" data-test-dashboard-member-activity>
|
||||
<h4 class="gh-dashboard-header-container">
|
||||
<h4 class="gh-dashboard-header">
|
||||
Activity
|
||||
</h4>
|
||||
</h4>
|
||||
<div class="content">
|
||||
{{#let (members-event-fetcher filter=(members-event-filter excludeEmailEvents=true) pageSize=5) as |eventsFetcher|}}
|
||||
{{#if eventsFetcher.isLoading}}
|
||||
Loading...
|
||||
{{/if}}
|
||||
|
||||
{{#if eventsFetcher.isError}}
|
||||
<p class="error">
|
||||
There was an error loading events
|
||||
{{#if eventsFetcher.errorMessage}}
|
||||
<code>{{eventsFetcher.errorMessage}}</code>
|
||||
{{/if}}
|
||||
</p>
|
||||
{{/if}}
|
||||
|
||||
{{#unless (or eventsFetcher.isLoading eventsFetcher.isError)}}
|
||||
<div class="gh-event-timeline">
|
||||
{{#if eventsFetcher.data}}
|
||||
<ul class="gh-dashboard-activity-list">
|
||||
{{#each eventsFetcher.data as |event|}}
|
||||
{{#let (parse-member-event event eventsFetcher.hasMultipleNewsletters) as |parsedEvent|}}
|
||||
<li class="gh-dashboard-activity-item" data-test-dashboard-member-activity-item>
|
||||
<LinkTo class="member-details" @route="member" @model="{{parsedEvent.memberId}}">
|
||||
<div class="gh-dashboard-activity-container">
|
||||
{{svg-jar parsedEvent.icon}}
|
||||
<div class="gh-dashboard-activity-detail">
|
||||
<div class="gh-dashboard-activity-name">
|
||||
{{parsedEvent.subject}}
|
||||
</div>
|
||||
<div class="gh-dashboard-activity-event">
|
||||
{{capitalize-first-letter parsedEvent.action}}
|
||||
{{parsedEvent.object}}
|
||||
{{parsedEvent.info}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</LinkTo>
|
||||
<span class="gh-dashboard-activity-time">{{moment-from-now parsedEvent.timestamp}}</span>
|
||||
</li>
|
||||
{{/let}}
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{else}}
|
||||
<div class="gh-no-data-list" data-test-no-member-activities>
|
||||
{{svg-jar "no-data-list"}}
|
||||
<span>No member activity available.</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<LinkTo @route="members-activity" @query={{reset-query-params "members-activity"}}>See all activity →</LinkTo>
|
||||
</div>
|
||||
{{/unless}}
|
||||
{{/let}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
|
@ -1,23 +0,0 @@
|
|||
import Component from '@glimmer/component';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default class DashboardLatestMemberActivityComponent extends Component {
|
||||
@service feature;
|
||||
@service session;
|
||||
@service settings;
|
||||
|
||||
get shouldDisplay() {
|
||||
if (this.feature.improvedOnboarding) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const isOwner = this.session.user?.isOwnerOnly;
|
||||
const hasCompletedLaunchWizard = this.settings.get('editorIsLaunchComplete');
|
||||
|
||||
if (isOwner && !hasCompletedLaunchWizard) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -1,106 +0,0 @@
|
|||
<section class="gh-dashboard-area charts" data-test-dashboard-members-graphs>
|
||||
<div class="gh-dashboard-box mrr">
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="gh-dashboard-header">MRR</h4>
|
||||
<h4 class="gh-dashboard-header secondary">30 days</h4>
|
||||
</div>
|
||||
<div class="gh-dashboard-chart-container">
|
||||
{{#if this.mrrStatsLoading}}
|
||||
Loading...
|
||||
{{else}}
|
||||
{{#if this.mrrStatsError}}
|
||||
<p class="error">
|
||||
There was an error loading MRR
|
||||
<code>{{this.mrrStatsError.message}}</code>
|
||||
</p>
|
||||
{{else}}
|
||||
<div class="gh-dashboard-summary">
|
||||
<div class="data"><span class="currency">{{this.mrrStatsData.currency}}</span>{{format-number this.mrrStatsData.currentAmount}}</div>
|
||||
<div class="growth {{this.mrrStatsData.percentClass}}">{{this.mrrStatsData.percentGrowth}}%</div>
|
||||
</div>
|
||||
{{#if this.mrrStatsData}}
|
||||
<div class="gh-dashboard-chart">
|
||||
<GhMembersChart @type="LineWithLine" @nightShift={{feature "nightShift"}} @showSummary={{false}} @showRange={{false}} @chartType="mrr" @chartStats={{this.mrrStatsData}} />
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-dashboard-box total-members">
|
||||
<div class="gh-dashboard-chart-container">
|
||||
{{#if this.memberCountStatsLoading}}
|
||||
Loading...
|
||||
{{else}}
|
||||
{{#if this.memberCountStatsError}}
|
||||
<p class="error">
|
||||
There was an error loading total members
|
||||
<code>{{this.memberCountStatsData.message}}</code>
|
||||
</p>
|
||||
{{else}}
|
||||
<div class="gh-dashboard-summary small">
|
||||
<h4 class="gh-dashboard-header">Total members</h4>
|
||||
<div class="data-container">
|
||||
<div class="data">{{format-number this.memberCountStatsData.all.total}}</div>
|
||||
<div class="growth {{this.memberCountStatsData.all.percentClass}}">{{this.memberCountStatsData.all.percentGrowth}}%</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-dashboard-chart small">
|
||||
<GhMembersChart @type="LineWithLine" @nightShift={{feature "nightShift"}} @chartSize="small" @showSummary={{false}} @chartType="all-members" @showRange={{false}} @chartStats={{this.memberCountStatsData.all}} />
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-dashboard-box paid-members">
|
||||
<div class="gh-dashboard-chart-container">
|
||||
{{#if this.memberCountStatsLoading}}
|
||||
Loading...
|
||||
{{else}}
|
||||
{{#if this.memberCountStatsError}}
|
||||
<p class="error">
|
||||
There was an error loading paid members
|
||||
<code>{{this.memberCountStatsData.message}}</code>
|
||||
</p>
|
||||
{{else}}
|
||||
<div class="gh-dashboard-summary small">
|
||||
<h4 class="gh-dashboard-header">Paid members</h4>
|
||||
<div class="data-container">
|
||||
<div class="data">{{format-number this.memberCountStatsData.paid.total}}</div>
|
||||
<div class="growth {{this.memberCountStatsData.paid.percentClass}}">{{this.memberCountStatsData.paid.percentGrowth}}%</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-dashboard-chart small">
|
||||
<GhMembersChart @type="LineWithLine" @nightShift={{feature "nightShift"}} @chartSize="small" @showSummary={{false}} @chartType="paid-members" @showRange={{false}} @chartStats={{this.memberCountStatsData.paid}} />
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-dashboard-box newsletter-open-rate">
|
||||
<div class="gh-dashboard-chart-container">
|
||||
{{#if this.newsletterOpenRatesLoading}}
|
||||
Loading...
|
||||
{{else}}
|
||||
{{#if this.newsletterOpenRatesError}}
|
||||
<p class="error">
|
||||
There was an error loading newsletter open rates
|
||||
<code>{{this.memberCountStatsData.message}}</code>
|
||||
</p>
|
||||
{{else}}
|
||||
<div class="gh-dashboard-summary small">
|
||||
<h4 class="gh-dashboard-header">Email open rate</h4>
|
||||
|
||||
<div class="data-container">
|
||||
<div class="data">{{this.newsletterOpenRatesData.current}}%</div>
|
||||
<div class="growth {{this.newsletterOpenRatesData.percentClass}}">{{this.newsletterOpenRatesData.percentGrowth}}%</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-dashboard-chart small">
|
||||
<GhMembersChart @type="bar" @nightShift={{feature "nightShift"}} @chartSize="small" @showSummary={{false}} @chartType="open-rate" @showRange={{false}} @chartStats={{this.newsletterOpenRatesData}} />
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
|
@ -1,161 +0,0 @@
|
|||
import Component from '@glimmer/component';
|
||||
import {getSymbol} from 'ghost-admin/utils/currency';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
|
||||
export default class DashboardMembersGraphs extends Component {
|
||||
@service membersStats;
|
||||
@service store;
|
||||
|
||||
@tracked mrrStatsData = null;
|
||||
@tracked mrrStatsError = null;
|
||||
@tracked mrrStatsLoading = false;
|
||||
|
||||
@tracked memberCountStatsData = null;
|
||||
@tracked memberCountStatsError = null;
|
||||
@tracked memberCountStatsLoading = false;
|
||||
|
||||
@tracked newsletterOpenRatesData = null;
|
||||
@tracked newsletterOpenRatesError = null;
|
||||
@tracked newsletterOpenRatesLoading = false;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.loadCharts();
|
||||
}
|
||||
|
||||
loadCharts() {
|
||||
this.loadMRRStats();
|
||||
this.loadMemberCountStats();
|
||||
this.loadNewsletterOpenRates();
|
||||
}
|
||||
|
||||
async loadMRRStats() {
|
||||
const tiers = await this.store.query('tier', {
|
||||
filter: 'type:paid', include: 'monthly_price,yearly_price', limit: 'all'
|
||||
});
|
||||
const defaultTier = tiers?.firstObject;
|
||||
|
||||
this.mrrStatsLoading = true;
|
||||
this.membersStats.fetchMRR().then((stats) => {
|
||||
this.mrrStatsLoading = false;
|
||||
const statsData = stats.data || [];
|
||||
const defaultCurrency = defaultTier?.monthlyPrice?.currency || 'usd';
|
||||
let currencyStats = statsData.find((stat) => {
|
||||
return stat.currency === defaultCurrency;
|
||||
});
|
||||
currencyStats = currencyStats || {
|
||||
data: [],
|
||||
currency: defaultCurrency
|
||||
};
|
||||
if (currencyStats) {
|
||||
const currencyStatsData = this.membersStats.fillDates(currencyStats.data) || {};
|
||||
const dateValues = Object.values(currencyStatsData).map(val => Math.round((val / 100)));
|
||||
const currentMRR = dateValues.length ? dateValues[dateValues.length - 1] : 0;
|
||||
const rangeStartMRR = dateValues.length ? dateValues[0] : 0;
|
||||
const percentGrowth = rangeStartMRR !== 0 ? ((currentMRR - rangeStartMRR) / rangeStartMRR) * 100 : 0;
|
||||
this.mrrStatsData = {
|
||||
currentAmount: currentMRR,
|
||||
currency: getSymbol(currencyStats.currency),
|
||||
percentGrowth: percentGrowth.toFixed(1),
|
||||
percentClass: (percentGrowth > 0 ? 'positive' : (percentGrowth < 0 ? 'negative' : '')),
|
||||
options: {
|
||||
rangeInDays: 30
|
||||
},
|
||||
data: {
|
||||
label: 'MRR',
|
||||
dateLabels: Object.keys(currencyStatsData),
|
||||
dateValues
|
||||
},
|
||||
title: 'MRR',
|
||||
stats: currencyStats
|
||||
};
|
||||
}
|
||||
}, (error) => {
|
||||
this.mrrStatsError = error;
|
||||
this.mrrStatsLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
loadMemberCountStats() {
|
||||
this.memberCountStatsLoading = true;
|
||||
this.membersStats.fetchCounts().then((stats) => {
|
||||
this.memberCountStatsLoading = false;
|
||||
|
||||
if (stats) {
|
||||
const statsDateObj = this.membersStats.fillCountDates(stats.data) || {};
|
||||
const dateValues = Object.values(statsDateObj);
|
||||
const currentAllCount = dateValues.length ? dateValues[dateValues.length - 1].total : 0;
|
||||
const currentPaidCount = dateValues.length ? dateValues[dateValues.length - 1].paid : 0;
|
||||
const rangeStartAllCount = dateValues.length ? dateValues[0].total : 0;
|
||||
const rangeStartPaidCount = dateValues.length ? dateValues[0].paid : 0;
|
||||
const allCountPercentGrowth = rangeStartAllCount !== 0 ? ((currentAllCount - rangeStartAllCount) / rangeStartAllCount) * 100 : 0;
|
||||
const paidCountPercentGrowth = rangeStartPaidCount !== 0 ? ((currentPaidCount - rangeStartPaidCount) / rangeStartPaidCount) * 100 : 0;
|
||||
|
||||
this.memberCountStatsData = {
|
||||
all: {
|
||||
percentGrowth: allCountPercentGrowth.toFixed(1),
|
||||
percentClass: (allCountPercentGrowth > 0 ? 'positive' : (allCountPercentGrowth < 0 ? 'negative' : '')),
|
||||
total: dateValues.length ? dateValues[dateValues.length - 1].total : 0,
|
||||
options: {
|
||||
rangeInDays: 30
|
||||
},
|
||||
data: {
|
||||
label: 'Members',
|
||||
dateLabels: Object.keys(statsDateObj),
|
||||
dateValues: dateValues.map(d => d.total)
|
||||
},
|
||||
title: 'Total Members',
|
||||
stats: stats
|
||||
},
|
||||
paid: {
|
||||
percentGrowth: paidCountPercentGrowth.toFixed(1),
|
||||
percentClass: (paidCountPercentGrowth > 0 ? 'positive' : (paidCountPercentGrowth < 0 ? 'negative' : '')),
|
||||
total: dateValues.length ? dateValues[dateValues.length - 1].paid : 0,
|
||||
options: {
|
||||
rangeInDays: 30
|
||||
},
|
||||
data: {
|
||||
label: 'Members',
|
||||
dateLabels: Object.keys(statsDateObj),
|
||||
dateValues: dateValues.map(d => d.paid)
|
||||
},
|
||||
title: 'Paid Members',
|
||||
stats: stats
|
||||
}
|
||||
};
|
||||
}
|
||||
}, (error) => {
|
||||
this.memberCountStatsError = error;
|
||||
this.memberCountStatsLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
loadNewsletterOpenRates() {
|
||||
this.newsletterOpenRatesLoading = true;
|
||||
this.membersStats.fetchNewsletterStats().then((results) => {
|
||||
const rangeStartOpenRate = results.length > 1 ? results[results.length - 2].openRate : 0;
|
||||
const rangeEndOpenRate = results.length > 0 ? results[results.length - 1].openRate : 0;
|
||||
const percentGrowth = rangeStartOpenRate !== 0 ? ((rangeEndOpenRate - rangeStartOpenRate) / rangeStartOpenRate) * 100 : 0;
|
||||
this.newsletterOpenRatesData = {
|
||||
percentGrowth: percentGrowth.toFixed(1),
|
||||
percentClass: (percentGrowth > 0 ? 'positive' : (percentGrowth < 0 ? 'negative' : '')),
|
||||
current: rangeEndOpenRate,
|
||||
options: {
|
||||
rangeInDays: 30
|
||||
},
|
||||
data: {
|
||||
label: 'Open rate',
|
||||
dateLabels: results.map(d => d.subject),
|
||||
dateValues: results.map(d => d.openRate)
|
||||
},
|
||||
title: 'Open rate',
|
||||
stats: results
|
||||
};
|
||||
this.newsletterOpenRatesLoading = false;
|
||||
}, (error) => {
|
||||
this.newsletterOpenRatesError = error;
|
||||
this.newsletterOpenRatesLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,39 +1,39 @@
|
|||
<div class="gh-dashboard5-metric {{if @center "is-center"}} {{if @reverse "is-reverse"}} {{if @large "is-large"}}">
|
||||
<div class="gh-dashboard5-metric-data">
|
||||
<div class="gh-dashboard-metric {{if @center "is-center"}} {{if @reverse "is-reverse"}} {{if @large "is-large"}}">
|
||||
<div class="gh-dashboard-metric-data">
|
||||
{{#if @secondary}}
|
||||
{{#if @value}}
|
||||
<div class="gh-dashboard5-metric-value {{if @secondary 'is-secondary'}}">
|
||||
<div class="gh-dashboard-metric-value {{if @secondary 'is-secondary'}}">
|
||||
<span class="value">{{@value}}</span>
|
||||
{{#if @trends}}
|
||||
<Dashboard::v5::Parts::Percentage @percentage={{@percentage}}/>
|
||||
<Dashboard::Parts::Percentage @percentage={{@percentage}}/>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
<h5 class="gh-dashboard5-metric-label {{if @secondary 'is-secondary'}}">
|
||||
<h5 class="gh-dashboard-metric-label {{if @secondary 'is-secondary'}}">
|
||||
{{@label}}
|
||||
</h5>
|
||||
{{else}}
|
||||
{{#if @reverse}}
|
||||
{{#if @value}}
|
||||
<div class="gh-dashboard5-metric-value">
|
||||
<div class="gh-dashboard-metric-value">
|
||||
<span class="value">{{@value}}</span>
|
||||
{{#if @trends}}
|
||||
<Dashboard::v5::Parts::Percentage @percentage={{@percentage}}/>
|
||||
<Dashboard::Parts::Percentage @percentage={{@percentage}}/>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
<h5 class="gh-dashboard5-metric-label">
|
||||
<h5 class="gh-dashboard-metric-label">
|
||||
{{@label}}
|
||||
</h5>
|
||||
{{else}}
|
||||
<h5 class="gh-dashboard5-metric-label">
|
||||
<h5 class="gh-dashboard-metric-label">
|
||||
{{@label}}
|
||||
</h5>
|
||||
{{#if @value}}
|
||||
<div class="gh-dashboard5-metric-value">
|
||||
<div class="gh-dashboard-metric-value">
|
||||
<span class="value">{{@value}}</span>
|
||||
{{#if @trends}}
|
||||
<Dashboard::v5::Parts::Percentage @percentage={{@percentage}}/>
|
||||
<Dashboard::Parts::Percentage @percentage={{@percentage}}/>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
@ -41,7 +41,7 @@
|
|||
{{/if}}
|
||||
</div>
|
||||
{{#if @extra}}
|
||||
<div class="gh-dashboard5-metric-extra">
|
||||
<div class="gh-dashboard-metric-extra">
|
||||
{{@extra}}
|
||||
</div>
|
||||
{{/if}}
|
|
@ -0,0 +1,7 @@
|
|||
{{#if (gt @percentage 0) }}
|
||||
<div class="gh-dashboard-percentage is-positive">+{{ @percentage }}%</div>
|
||||
{{else if (lt @percentage 0)}}
|
||||
<div class="gh-dashboard-percentage is-negative">{{ @percentage }}%</div>
|
||||
{{else}}
|
||||
<div class="gh-dashboard-percentage">0%</div>
|
||||
{{/if}}
|
|
@ -1,5 +1,5 @@
|
|||
<div class="gh-dashboard5-zero">
|
||||
<div class="gh-dashboard5-zero-message">
|
||||
<div class="gh-dashboard-zero">
|
||||
<div class="gh-dashboard-zero-message">
|
||||
<h4>Welcome to your Dashboard</h4>
|
||||
<p>You'll find member analytics here once<br />someone signs up.</p>
|
||||
<p><LinkTo @route="members">Add or import members →</LinkTo></p>
|
17
ghost/admin/app/components/dashboard/resources/community.hbs
Normal file
17
ghost/admin/app/components/dashboard/resources/community.hbs
Normal file
|
@ -0,0 +1,17 @@
|
|||
<section class="gh-dashboard-resource gh-dashboard-community">
|
||||
<a href="https://ghost.org/resources/community/" target="_blank" rel="noopener noreferrer" class="gh-dashboard-resource-box">
|
||||
<div class="gh-dashboard-resource-title">
|
||||
<h4>Ghost Creator Community</h4>
|
||||
</div>
|
||||
<div class="gh-dashboard-resource-body">
|
||||
<div class="gh-dashboard-list">
|
||||
<div class="gh-dashboard-list-body">
|
||||
<p>Talk strategy.<br />Get advice.<br />Or just hang out.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-dashboard-resource-footer">
|
||||
Share the journey →
|
||||
</div>
|
||||
</a>
|
||||
</section>
|
|
@ -0,0 +1,24 @@
|
|||
<section class="gh-dashboard-resource gh-dashboard-newsletter" {{did-insert this.load}}>
|
||||
<article class="gh-dashboard-resource-box">
|
||||
<div class="gh-dashboard-resource-title">
|
||||
<h4>Latest from the newsletter</h4>
|
||||
</div>
|
||||
<div class="gh-dashboard-resource-body">
|
||||
{{#if (not (or this.loading this.error))}}
|
||||
<div class="gh-dashboard-resource-bigarticle">
|
||||
{{#each this.newsletters as |entry|}}
|
||||
<a class="gh-dashboard-resource-smallarticle" href={{set-query-params entry.url utm_source='admin'}} target="_blank" rel="noopener noreferrer">
|
||||
<div class="gh-dashboard-resource-text">
|
||||
<h3>{{entry.title}}</h3>
|
||||
<p>{{entry.excerpt}}</p>
|
||||
</div>
|
||||
</a>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="gh-dashboard-resource-footer">
|
||||
<a href="https://ghost.org/resources/newsletter/" target="_blank" class="gh-dashboard-subscribe-button" rel="noopener noreferrer">Subscribe <span>to the newsletter </span>→</a>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
23
ghost/admin/app/components/dashboard/resources/resources.hbs
Normal file
23
ghost/admin/app/components/dashboard/resources/resources.hbs
Normal file
|
@ -0,0 +1,23 @@
|
|||
<section class="gh-dashboard-resource gh-dashboard-resources" {{did-insert this.load}}>
|
||||
<article class="gh-dashboard-resource-box">
|
||||
{{#if (not (or this.loading this.error))}}
|
||||
<a href="{{this.resource.url}}" target="_blank" class="gh-dashboard-resource-thumbnail" rel="noopener noreferrer" style={{html-safe (concat "background-image: url(" this.resource.feature_image ")")}} aria-label="Resource link"></a>
|
||||
<div class="gh-dashboard-resource-contents">
|
||||
<div class="gh-dashboard-resource-title">
|
||||
<h4>Resources</h4>
|
||||
</div>
|
||||
<div class="gh-dashboard-resource-body">
|
||||
<a href="{{this.resource.url}}" target="_blank" class="gh-dashboard-resource-bigarticle" rel="noopener noreferrer">
|
||||
<div class="gh-dashboard-resource-text">
|
||||
<h3>{{this.resource.title}}</h3>
|
||||
<p>{{this.resource.excerpt}}</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="gh-dashboard-resource-footer">
|
||||
<a href="https://ghost.org/resources/" target="_blank" rel="noopener noreferrer">Learn more →</a>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</article>
|
||||
</section>
|
|
@ -0,0 +1,33 @@
|
|||
<section class="gh-dashboard-resource gh-dashboard-staff-picks" {{did-insert this.load}}>
|
||||
<article class="gh-dashboard-resource-box">
|
||||
<div class="gh-dashboard-resource-title is-large has-border">
|
||||
<h4>Staff picks</h4>
|
||||
<p>Hand picked stories from around the web, published with Ghost.</p>
|
||||
</div>
|
||||
<div class="gh-dashboard-resource-body">
|
||||
<div class="gh-dashboard-list">
|
||||
{{#if (not (or this.loading this.error))}}
|
||||
<div class="gh-dashboard-list-body">
|
||||
{{#each this.staffPicks as |entry|}}
|
||||
<div class="gh-dashboard-list-item">
|
||||
<a class="gh-dashboard-list-post permalink" href={{set-query-params entry.link utm_source='ghost'}} target="_blank" rel="noopener noreferrer">
|
||||
<span class="gh-dashboard-list-link">
|
||||
<span>{{entry.title}}</span>
|
||||
</span>
|
||||
<div class="gh-dashboard-resource-secondary">{{entry.creator}}</div>
|
||||
</a>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="gh-dashboard-list-empty">
|
||||
<p>No staff picks yet.</p>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-dashboard-resource-footer">
|
||||
<a href="https://twitter.com/ghoststaffpicks" target="_blank" rel="noopener noreferrer">{{svg-jar "twitter-logo"}} <span>Follow on Twitter</span></a>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
33
ghost/admin/app/components/dashboard/resources/whats-new.hbs
Normal file
33
ghost/admin/app/components/dashboard/resources/whats-new.hbs
Normal file
|
@ -0,0 +1,33 @@
|
|||
<section class="gh-dashboard-resource gh-dashboard-whats-new" {{did-insert this.load}}>
|
||||
<article class="gh-dashboard-resource-box">
|
||||
<div class="gh-dashboard-resource-title is-large has-border">
|
||||
<h4>What's new</h4>
|
||||
<p>All the latest improvements.</p>
|
||||
</div>
|
||||
<div class="gh-dashboard-resource-body">
|
||||
<div class="gh-dashboard-list {{if this.whatsNew.hasNew "has-new"}}">
|
||||
{{#if (not (or this.loading this.error))}}
|
||||
<div class="gh-dashboard-list-body">
|
||||
{{#each this.entries as |entry|}}
|
||||
<div class="gh-dashboard-list-item">
|
||||
<LinkTo class="gh-dashboard-list-post" @route="whatsnew" @query={{hash entry=entry.slug}}>
|
||||
<span class="gh-dashboard-list-link">
|
||||
<span>{{entry.title}}</span>
|
||||
</span>
|
||||
<div class="gh-dashboard-resource-secondary">{{moment-format entry.published_at "D MMM YYYY"}}</div>
|
||||
</LinkTo>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="gh-dashboard-list-empty">
|
||||
<p>No new features yet.</p>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-dashboard-resource-footer">
|
||||
<LinkTo @route="whatsnew" @query={{hash entry=null}} class="green">See more features →</LinkTo>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
|
@ -1,43 +0,0 @@
|
|||
<div class="gh-dashboard5-minichart gh-dashboard5-breakdown">
|
||||
<div class="gh-dashboard5-content">
|
||||
<div class="gh-dashboard5-data">
|
||||
<Dashboard::v5::Parts::Metric
|
||||
@label={{this.chartTitle}} />
|
||||
|
||||
<div class="gh-dashboard5-legend">
|
||||
<div class="gh-dashboard5-legend-item">New</div>
|
||||
<div class="gh-dashboard5-legend-item">Canceled</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-dashboard5-chart" {{did-insert this.loadCharts}}>
|
||||
{{#if this.loading}}
|
||||
<div class="gh-dashboard5-chart-loading">
|
||||
<div class="gh-loading-spinner"></div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="gh-dashboard5-chart-container">
|
||||
<div class="gh-dashboard5-chart-box">
|
||||
<EmberChart
|
||||
@type={{this.chartType}}
|
||||
@data={{this.chartData}}
|
||||
@options={{this.chartOptions}}
|
||||
@height={{110}} />
|
||||
</div>
|
||||
|
||||
<div id="gh-dashboard5-breakdown-tooltip" class="gh-dashboard5-tooltip">
|
||||
<div class="gh-dashboard5-tooltip-label">
|
||||
-
|
||||
</div>
|
||||
<div class="gh-dashboard5-tooltip-value">
|
||||
<div class="gh-dashboard5-tooltip-value-1"><span class="indicator solid"></span><span class="value"></span></div>
|
||||
<div class="gh-dashboard5-tooltip-value-1"><span class="metric">New</span></div>
|
||||
<div class="gh-dashboard5-tooltip-value-2"><span class="indicator solid"></span><span class="value"></span></div>
|
||||
<div class="gh-dashboard5-tooltip-value-2"><span class="metric">Canceled</span></div>
|
||||
<div class="gh-dashboard5-tooltip-value-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,3 +0,0 @@
|
|||
<div class="gh-dashboard5-zero">
|
||||
|
||||
</div>
|
|
@ -1,7 +0,0 @@
|
|||
{{#if (gt @percentage 0) }}
|
||||
<div class="gh-dashboard5-percentage is-positive">+{{ @percentage }}%</div>
|
||||
{{else if (lt @percentage 0)}}
|
||||
<div class="gh-dashboard5-percentage is-negative">{{ @percentage }}%</div>
|
||||
{{else}}
|
||||
<div class="gh-dashboard5-percentage">0%</div>
|
||||
{{/if}}
|
|
@ -1,17 +0,0 @@
|
|||
<section class="gh-dashboard5-resource gh-dashboard5-community">
|
||||
<a href="https://ghost.org/resources/community/" target="_blank" rel="noopener noreferrer" class="gh-dashboard5-resource-box">
|
||||
<div class="gh-dashboard5-resource-title">
|
||||
<h4>Ghost Creator Community</h4>
|
||||
</div>
|
||||
<div class="gh-dashboard5-resource-body">
|
||||
<div class="gh-dashboard5-list">
|
||||
<div class="gh-dashboard5-list-body">
|
||||
<p>Talk strategy.<br />Get advice.<br />Or just hang out.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-dashboard5-resource-footer">
|
||||
Share the journey →
|
||||
</div>
|
||||
</a>
|
||||
</section>
|
|
@ -1,24 +0,0 @@
|
|||
<section class="gh-dashboard5-resource gh-dashboard5-newsletter" {{did-insert this.load}}>
|
||||
<article class="gh-dashboard5-resource-box">
|
||||
<div class="gh-dashboard5-resource-title">
|
||||
<h4>Latest from the newsletter</h4>
|
||||
</div>
|
||||
<div class="gh-dashboard5-resource-body">
|
||||
{{#if (not (or this.loading this.error))}}
|
||||
<div class="gh-dashboard5-resource-bigarticle">
|
||||
{{#each this.newsletters as |entry|}}
|
||||
<a class="gh-dashboard5-resource-smallarticle" href={{set-query-params entry.url utm_source='admin'}} target="_blank" rel="noopener noreferrer">
|
||||
<div class="gh-dashboard5-resource-text">
|
||||
<h3>{{entry.title}}</h3>
|
||||
<p>{{entry.excerpt}}</p>
|
||||
</div>
|
||||
</a>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="gh-dashboard5-resource-footer">
|
||||
<a href="https://ghost.org/resources/newsletter/" target="_blank" class="gh-dashboard5-subscribe-button" rel="noopener noreferrer">Subscribe <span>to the newsletter </span>→</a>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
|
@ -1,23 +0,0 @@
|
|||
<section class="gh-dashboard5-resource gh-dashboard5-resources" {{did-insert this.load}}>
|
||||
<article class="gh-dashboard5-resource-box">
|
||||
{{#if (not (or this.loading this.error))}}
|
||||
<a href="{{this.resource.url}}" target="_blank" class="gh-dashboard5-resource-thumbnail" rel="noopener noreferrer" style={{html-safe (concat "background-image: url(" this.resource.feature_image ")")}} aria-label="Resource link"></a>
|
||||
<div class="gh-dashboard5-resource-contents">
|
||||
<div class="gh-dashboard5-resource-title">
|
||||
<h4>Resources</h4>
|
||||
</div>
|
||||
<div class="gh-dashboard5-resource-body">
|
||||
<a href="{{this.resource.url}}" target="_blank" class="gh-dashboard5-resource-bigarticle" rel="noopener noreferrer">
|
||||
<div class="gh-dashboard5-resource-text">
|
||||
<h3>{{this.resource.title}}</h3>
|
||||
<p>{{this.resource.excerpt}}</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="gh-dashboard5-resource-footer">
|
||||
<a href="https://ghost.org/resources/" target="_blank" rel="noopener noreferrer">Learn more →</a>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</article>
|
||||
</section>
|
|
@ -1,33 +0,0 @@
|
|||
<section class="gh-dashboard5-resource gh-dashboard5-staff-picks" {{did-insert this.load}}>
|
||||
<article class="gh-dashboard5-resource-box">
|
||||
<div class="gh-dashboard5-resource-title is-large has-border">
|
||||
<h4>Staff picks</h4>
|
||||
<p>Hand picked stories from around the web, published with Ghost.</p>
|
||||
</div>
|
||||
<div class="gh-dashboard5-resource-body">
|
||||
<div class="gh-dashboard5-list">
|
||||
{{#if (not (or this.loading this.error))}}
|
||||
<div class="gh-dashboard5-list-body">
|
||||
{{#each this.staffPicks as |entry|}}
|
||||
<div class="gh-dashboard5-list-item">
|
||||
<a class="gh-dashboard5-list-post permalink" href={{set-query-params entry.link utm_source='ghost'}} target="_blank" rel="noopener noreferrer">
|
||||
<span class="gh-dashboard5-list-link">
|
||||
<span>{{entry.title}}</span>
|
||||
</span>
|
||||
<div class="gh-dashboard5-resource-secondary">{{entry.creator}}</div>
|
||||
</a>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="gh-dashboard5-list-empty">
|
||||
<p>No staff picks yet.</p>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-dashboard5-resource-footer">
|
||||
<a href="https://twitter.com/ghoststaffpicks" target="_blank" rel="noopener noreferrer">{{svg-jar "twitter-logo"}} <span>Follow on Twitter</span></a>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
|
@ -1,33 +0,0 @@
|
|||
<section class="gh-dashboard5-resource gh-dashboard5-whats-new" {{did-insert this.load}}>
|
||||
<article class="gh-dashboard5-resource-box">
|
||||
<div class="gh-dashboard5-resource-title is-large has-border">
|
||||
<h4>What's new</h4>
|
||||
<p>All the latest improvements.</p>
|
||||
</div>
|
||||
<div class="gh-dashboard5-resource-body">
|
||||
<div class="gh-dashboard5-list {{if this.whatsNew.hasNew "has-new"}}">
|
||||
{{#if (not (or this.loading this.error))}}
|
||||
<div class="gh-dashboard5-list-body">
|
||||
{{#each this.entries as |entry|}}
|
||||
<div class="gh-dashboard5-list-item">
|
||||
<LinkTo class="gh-dashboard5-list-post" @route="whatsnew" @query={{hash entry=entry.slug}}>
|
||||
<span class="gh-dashboard5-list-link">
|
||||
<span>{{entry.title}}</span>
|
||||
</span>
|
||||
<div class="gh-dashboard5-resource-secondary">{{moment-format entry.published_at "D MMM YYYY"}}</div>
|
||||
</LinkTo>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="gh-dashboard5-list-empty">
|
||||
<p>No new features yet.</p>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-dashboard5-resource-footer">
|
||||
<LinkTo @route="whatsnew" @query={{hash entry=null}} class="green">See more features →</LinkTo>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
|
@ -1,11 +0,0 @@
|
|||
<div class="gh-dashboard-chart-box {{if this.isSmall "small"}}" {{did-insert (perform this.fetchStatsTask)}}>
|
||||
{{#if this.stats}}
|
||||
<EmberChart
|
||||
@type={{this.type}}
|
||||
@options={{this.chartOptions}}
|
||||
@data={{this.chartData}}
|
||||
@height={{300}} />
|
||||
{{else}}
|
||||
<GhLoadingSpinner />
|
||||
{{/if}}
|
||||
</div>
|
|
@ -1,363 +0,0 @@
|
|||
/* global Chart */
|
||||
import Component from '@ember/component';
|
||||
import moment from 'moment';
|
||||
import {action, computed, get} from '@ember/object';
|
||||
import {getSymbol} from 'ghost-admin/utils/currency';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {task} from 'ember-concurrency';
|
||||
|
||||
const DATE_FORMAT = 'D MMM YYYY';
|
||||
|
||||
export default Component.extend({
|
||||
ajax: service(),
|
||||
membersStats: service(),
|
||||
|
||||
// public attrs
|
||||
nightShift: false,
|
||||
lineColor: '#14b8ff',
|
||||
|
||||
stats: null,
|
||||
tagName: '',
|
||||
chartStats: null,
|
||||
chartData: null,
|
||||
chartOptions: null,
|
||||
showSummary: true,
|
||||
showRange: true,
|
||||
chartType: '',
|
||||
chartSize: '',
|
||||
chartHeading: 'Total members',
|
||||
|
||||
isSmall: computed('chartSize', function () {
|
||||
if (this.chartSize === 'small') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
|
||||
startDateLabel: computed('membersStats.stats', function () {
|
||||
if (!this.membersStats?.stats?.total_on_date) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let firstDate = Object.keys(this.membersStats.stats.total_on_date)[0];
|
||||
return moment(firstDate).format(DATE_FORMAT);
|
||||
}),
|
||||
|
||||
selectedRange: computed('membersStats.days', function () {
|
||||
const availableRanges = this.availableRanges;
|
||||
return availableRanges.findBy('days', this.membersStats.days);
|
||||
}),
|
||||
|
||||
availableRanges: computed(function () {
|
||||
return [{
|
||||
name: '30 days',
|
||||
days: '30'
|
||||
}, {
|
||||
name: '90 days',
|
||||
days: '90'
|
||||
}, {
|
||||
name: '365 days',
|
||||
days: '365'
|
||||
}, {
|
||||
name: 'All time',
|
||||
days: 'all-time'
|
||||
}];
|
||||
}),
|
||||
|
||||
// Lifecycle ---------------------------------------------------------------
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.setChartJSDefaults();
|
||||
},
|
||||
|
||||
didReceiveAttrs() {
|
||||
this._super(...arguments);
|
||||
|
||||
if (this.chartStats) {
|
||||
const {options, data, title, stats} = this.chartStats;
|
||||
|
||||
this.set('stats', stats);
|
||||
this.set('chartHeading', title);
|
||||
this.setChartData(data);
|
||||
this.setChartOptions(options);
|
||||
}
|
||||
|
||||
if (this._lastNightShift !== undefined && this.nightShift !== this._lastNightShift) {
|
||||
const {options = {}} = this.chartStats;
|
||||
|
||||
this.setChartOptions(options);
|
||||
}
|
||||
this._lastNightShift = this.nightShift;
|
||||
},
|
||||
|
||||
// Actions -----------------------------------------------------------------
|
||||
|
||||
changeDateRange: action(function (range) {
|
||||
this.membersStats.days = get(range, 'days');
|
||||
this.fetchStatsTask.perform();
|
||||
}),
|
||||
|
||||
// Tasks -------------------------------------------------------------------
|
||||
|
||||
fetchStatsTask: task(function* () {
|
||||
let stats;
|
||||
if (!this.chartType) {
|
||||
this.set('stats', null);
|
||||
stats = yield this.membersStats.fetch();
|
||||
this.setOriginalChartData(stats);
|
||||
}
|
||||
}),
|
||||
|
||||
setOriginalChartData(stats) {
|
||||
if (stats) {
|
||||
this.set('stats', stats);
|
||||
|
||||
this.setChartOptions({
|
||||
rangeInDays: Object.keys(stats.total_on_date).length
|
||||
});
|
||||
|
||||
this.setChartData({
|
||||
dateLabels: Object.keys(stats.total_on_date),
|
||||
dateValues: Object.values(stats.total_on_date)
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Internal ----------------------------------------------------------------
|
||||
|
||||
setChartData({dateLabels, dateValues, label = 'Total Members'}) {
|
||||
let backgroundColors = this.lineColor;
|
||||
|
||||
if (this.chartType === 'open-rate') {
|
||||
backgroundColors = dateLabels.map((val) => {
|
||||
if (val) {
|
||||
return this.lineColor;
|
||||
} else {
|
||||
return (this.nightShift ? '#7C8B9A' : '#CED4D9');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.set('chartData', {
|
||||
labels: dateLabels,
|
||||
datasets: [{
|
||||
label: label,
|
||||
cubicInterpolationMode: 'monotone',
|
||||
data: dateValues,
|
||||
fill: false,
|
||||
backgroundColor: backgroundColors,
|
||||
pointRadius: 0,
|
||||
pointHitRadius: 10,
|
||||
borderColor: this.lineColor,
|
||||
borderJoinStyle: 'miter',
|
||||
maxBarThickness: 20,
|
||||
minBarLength: 2
|
||||
}]
|
||||
});
|
||||
},
|
||||
|
||||
setChartOptions({rangeInDays}) {
|
||||
let maxTicksAllowed = this.isSmall ? 3 : this.getTicksForRange(rangeInDays);
|
||||
|
||||
if (this.chartType === 'open-rate') {
|
||||
maxTicksAllowed = 0;
|
||||
}
|
||||
|
||||
this.setChartJSDefaults();
|
||||
let options = {
|
||||
responsive: true,
|
||||
responsiveAnimationDuration: 5,
|
||||
maintainAspectRatio: false,
|
||||
layout: {
|
||||
padding: {
|
||||
top: (this.isSmall ? 20 : 5), // Needed otherwise the top dot is cut
|
||||
right: 10,
|
||||
bottom: (this.isSmall ? 20 : 5),
|
||||
left: 10
|
||||
}
|
||||
},
|
||||
title: {
|
||||
display: false
|
||||
},
|
||||
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,
|
||||
filter: (tooltipItems, data) => {
|
||||
if (this.chartType === 'open-rate') {
|
||||
let label = data.labels[tooltipItems.index];
|
||||
if (label === '') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
callbacks: {
|
||||
label: (tooltipItems, data) => {
|
||||
const labelText = data.datasets[tooltipItems.datasetIndex].label;
|
||||
let valueText = data.datasets[tooltipItems.datasetIndex].data[tooltipItems.index].toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
if (this.chartType === 'mrr') {
|
||||
const currency = getSymbol(this.stats.currency);
|
||||
valueText = `${currency}${valueText}`;
|
||||
}
|
||||
if (this.chartType === 'open-rate') {
|
||||
valueText = `${valueText}%`;
|
||||
}
|
||||
return `${labelText}: ${valueText}`;
|
||||
},
|
||||
title: (tooltipItems) => {
|
||||
if (this.chartType === 'open-rate') {
|
||||
if (tooltipItems.length) {
|
||||
return tooltipItems[0].xLabel;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
return moment(tooltipItems[0].xLabel).format(DATE_FORMAT);
|
||||
}
|
||||
}
|
||||
},
|
||||
hover: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
animationDuration: 120
|
||||
},
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
scales: {
|
||||
xAxes: [{
|
||||
labelString: 'Date',
|
||||
gridLines: {
|
||||
drawTicks: false,
|
||||
color: (this.nightShift ? '#333F44' : '#DDE1E5'),
|
||||
zeroLineColor: (this.nightShift ? '#333F44' : '#DDE1E5')
|
||||
},
|
||||
ticks: {
|
||||
display: false,
|
||||
maxRotation: 0,
|
||||
minRotation: 0,
|
||||
padding: 6,
|
||||
autoSkip: false,
|
||||
fontColor: '#626D79',
|
||||
maxTicksLimit: 10,
|
||||
callback: (value, index, values) => {
|
||||
let step = (values.length - 1) / (maxTicksAllowed);
|
||||
let steps = [];
|
||||
for (let i = 0; i < maxTicksAllowed; i++) {
|
||||
steps.push(Math.ceil(i * step));
|
||||
}
|
||||
|
||||
if (index === 0) {
|
||||
return value;
|
||||
}
|
||||
if (index === (values.length - 1) && this.chartType !== 'open-rate') {
|
||||
return 'Today';
|
||||
}
|
||||
|
||||
if (steps.includes(index)) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
}],
|
||||
yAxes: [{
|
||||
gridLines: {
|
||||
drawTicks: false,
|
||||
display: false,
|
||||
drawBorder: false
|
||||
},
|
||||
ticks: {
|
||||
maxTicksLimit: 5,
|
||||
fontColor: '#7C8B9A',
|
||||
padding: 8,
|
||||
precision: 0,
|
||||
callback: (value) => {
|
||||
const currency = this.chartType === 'mrr' ? getSymbol(this.stats.currency) : '';
|
||||
if (parseInt(value) >= 1000) {
|
||||
return currency + value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
} else {
|
||||
return currency + value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
if (this.chartType === 'mrr' || this.chartType === 'all-members') {
|
||||
const chartData = this.chartData.datasets[0].data;
|
||||
let allZeros = true;
|
||||
for (let i = 0; i < chartData.length; i++) {
|
||||
const element = chartData[i];
|
||||
if (element !== 0) {
|
||||
allZeros = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (allZeros) {
|
||||
options.scales.yAxes[0].ticks.suggestedMin = 0;
|
||||
options.scales.yAxes[0].ticks.suggestedMax = 100;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.chartType === 'open-rate') {
|
||||
options.scales.yAxes[0].ticks.suggestedMin = 0;
|
||||
}
|
||||
|
||||
if (this.isSmall) {
|
||||
options.scales.yAxes[0].ticks.display = false;
|
||||
options.scales.xAxes[0].gridLines.display = true;
|
||||
}
|
||||
this.set('chartOptions', options);
|
||||
},
|
||||
|
||||
getTicksForRange(rangeInDays) {
|
||||
if (rangeInDays <= 30) {
|
||||
return 5;
|
||||
} else if (rangeInDays <= 90) {
|
||||
return 10;
|
||||
} else {
|
||||
return 15;
|
||||
}
|
||||
},
|
||||
|
||||
setChartJSDefaults() {
|
||||
Chart.defaults.LineWithLine = Chart.defaults.line;
|
||||
Chart.controllers.LineWithLine = Chart.controllers.line.extend({
|
||||
draw: function (ease) {
|
||||
Chart.controllers.line.prototype.draw.call(this, ease);
|
||||
|
||||
if (this.chart.tooltip._active && this.chart.tooltip._active.length) {
|
||||
let activePoint = this.chart.tooltip._active[0];
|
||||
let ctx = this.chart.ctx;
|
||||
let x = activePoint.tooltipPosition().x;
|
||||
let topY = this.chart.scales['y-axis-0'].top;
|
||||
let bottomY = this.chart.scales['y-axis-0'].bottom;
|
||||
|
||||
// draw line
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, topY);
|
||||
ctx.lineTo(x, bottomY);
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeStyle = (this.nightShift ? 'rgba(62, 176, 239, 0.65)' : 'rgba(62, 176, 239, 0.1)');
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
|
@ -2,64 +2,68 @@ import Controller from '@ember/controller';
|
|||
import {action} from '@ember/object';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {task} from 'ember-concurrency';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
|
||||
// Options 30 and 90 need an extra day to be able to distribute ticks/gridlines evenly
|
||||
const DAYS_OPTIONS = [{
|
||||
name: '7 Days',
|
||||
value: 7
|
||||
}, {
|
||||
name: '30 Days',
|
||||
value: 30 + 1
|
||||
}, {
|
||||
name: '90 Days',
|
||||
value: 90 + 1
|
||||
}];
|
||||
|
||||
export default class DashboardController extends Controller {
|
||||
@service feature;
|
||||
@service session;
|
||||
@service membersStats;
|
||||
@service store;
|
||||
@service settings;
|
||||
@service whatsNew;
|
||||
@service dashboardStats;
|
||||
|
||||
@tracked whatsNewEntries = null;
|
||||
@tracked whatsNewEntriesLoading = null;
|
||||
@tracked whatsNewEntriesError = null;
|
||||
|
||||
get showMembersData() {
|
||||
return this.settings.get('membersSignupAccess') !== 'none';
|
||||
}
|
||||
|
||||
get showMembersGraphs() {
|
||||
if (!this.feature.improvedOnboarding) {
|
||||
return this.showMembersData;
|
||||
}
|
||||
|
||||
const hasMembers = this.store.peekAll('member').length > 0;
|
||||
|
||||
return this.showMembersData
|
||||
&& this.checkMemberCountTask.performCount > 0
|
||||
&& hasMembers;
|
||||
}
|
||||
|
||||
initialise() {
|
||||
if (!this.feature.get('dashboardV5')) {
|
||||
this.loadWhatsNew();
|
||||
this.checkMemberCountTask.perform();
|
||||
}
|
||||
}
|
||||
|
||||
loadWhatsNew() {
|
||||
this.whatsNewEntriesLoading = true;
|
||||
this.whatsNew.fetchLatest.perform().then(() => {
|
||||
this.whatsNewEntriesLoading = false;
|
||||
this.whatsNewEntries = this.whatsNew.entries.slice(0, 3);
|
||||
}, (error) => {
|
||||
this.whatsNewEntriesError = error;
|
||||
this.whatsNewEntriesLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
dismissLaunchBanner() {
|
||||
this.settings.set('editorIsLaunchComplete', true);
|
||||
this.settings.save();
|
||||
}
|
||||
daysOptions = DAYS_OPTIONS;
|
||||
|
||||
@task
|
||||
*checkMemberCountTask() {
|
||||
if (this.store.peekAll('member').length === 0) {
|
||||
yield this.store.query('member', {limit: 1});
|
||||
}
|
||||
*loadSiteStatusTask() {
|
||||
yield this.dashboardStats.loadSiteStatus();
|
||||
return {};
|
||||
}
|
||||
|
||||
@action
|
||||
onDaysChange(selected) {
|
||||
this.days = selected.value;
|
||||
}
|
||||
|
||||
get days() {
|
||||
return this.dashboardStats.chartDays;
|
||||
}
|
||||
|
||||
set days(days) {
|
||||
this.dashboardStats.chartDays = days;
|
||||
}
|
||||
|
||||
get selectedDaysOption() {
|
||||
return this.daysOptions.find(d => d.value === this.days);
|
||||
}
|
||||
|
||||
get isLoading() {
|
||||
return this.dashboardStats.siteStatus === null;
|
||||
}
|
||||
|
||||
get totalMembers() {
|
||||
return this.dashboardStats.memberCounts?.total ?? 0;
|
||||
}
|
||||
|
||||
get isTotalMembersZero() {
|
||||
return this.dashboardStats.memberCounts && this.totalMembers === 0;
|
||||
}
|
||||
|
||||
get hasPaidTiers() {
|
||||
return this.dashboardStats.siteStatus?.hasPaidTiers;
|
||||
}
|
||||
|
||||
get areNewslettersEnabled() {
|
||||
return this.dashboardStats.siteStatus?.newslettersEnabled;
|
||||
}
|
||||
|
||||
get areMembersEnabled() {
|
||||
return this.dashboardStats.siteStatus?.membersEnabled;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
|
||||
|
||||
export default class DashboardRoute extends AuthenticatedRoute {
|
||||
beforeModel() {
|
||||
async beforeModel() {
|
||||
super.beforeModel(...arguments);
|
||||
|
||||
if (this.session.user.isContributor) {
|
||||
|
@ -17,7 +17,12 @@ export default class DashboardRoute extends AuthenticatedRoute {
|
|||
};
|
||||
}
|
||||
|
||||
// trigger a background load of members plus labels for filter dropdown
|
||||
setupController() {
|
||||
this.controller.initialise();
|
||||
super.setupController(...arguments);
|
||||
}
|
||||
|
||||
model() {
|
||||
return this.controllerFor('dashboard').loadSiteStatusTask.perform();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,7 +56,6 @@ export default class FeatureService extends Service {
|
|||
nightShift;
|
||||
|
||||
// labs flags
|
||||
@feature('dashboardV5') dashboardV5;
|
||||
@feature('membersActivity') membersActivity;
|
||||
@feature('urlCache') urlCache;
|
||||
@feature('beforeAfterCard') beforeAfterCard;
|
||||
|
|
|
@ -69,7 +69,6 @@
|
|||
@import "layouts/fullscreen-wizard.css";
|
||||
@import "layouts/post-preview.css";
|
||||
@import "layouts/dashboard.css";
|
||||
@import "layouts/dashboard-v5.css";
|
||||
@import "layouts/tiers.css";
|
||||
@import "layouts/offers.css";
|
||||
|
||||
|
@ -975,7 +974,6 @@ input:focus,
|
|||
|
||||
/* Members activity */
|
||||
.gh-members-activity-icon svg path,
|
||||
.gh-dashboard-activity-container svg path,
|
||||
.gh-member-feed-icon svg path {
|
||||
stroke: #fff;
|
||||
}
|
||||
|
@ -1049,22 +1047,6 @@ input:focus,
|
|||
}
|
||||
|
||||
.gh-dashboard-box {
|
||||
border-color: var(--lightgrey);
|
||||
}
|
||||
|
||||
.gh-dashboard-btn {
|
||||
border: none;
|
||||
background: var(--black) !important;
|
||||
color: #394047 !important;
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.01), 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.gh-dashboard-btn:hover {
|
||||
background: var(--white);
|
||||
}
|
||||
|
||||
.gh-dashboard5-box {
|
||||
flex: 1;
|
||||
border: 1px solid var(--lightgrey);
|
||||
}
|
||||
|
@ -1121,77 +1103,77 @@ kbd {
|
|||
|
||||
/* Dashboard v5 */
|
||||
|
||||
.gh-dashboard5 .gh-dashboard5-anchor .gh-dashboard5-stats {
|
||||
.gh-dashboard .gh-dashboard-anchor .gh-dashboard-stats {
|
||||
background: transparent;
|
||||
border-top-color: #2b2d31;
|
||||
}
|
||||
|
||||
.gh-dashboard5 .gh-dashboard5-anchor .gh-dashboard5-stats-button {
|
||||
.gh-dashboard .gh-dashboard-anchor .gh-dashboard-stats-button {
|
||||
border-color: #202429;
|
||||
}
|
||||
|
||||
.gh-dashboard5 .gh-dashboard5-anchor .gh-dashboard5-stats-button.is-selected {
|
||||
.gh-dashboard .gh-dashboard-anchor .gh-dashboard-stats-button.is-selected {
|
||||
border-color: #394047;
|
||||
}
|
||||
|
||||
.gh-dashboard5 .gh-dashboard5-list-header,
|
||||
.gh-dashboard5 .gh-dashboard5-list-item,
|
||||
.gh-dashboard5 .gh-dashboard5-list-footer {
|
||||
.gh-dashboard .gh-dashboard-list-header,
|
||||
.gh-dashboard .gh-dashboard-list-item,
|
||||
.gh-dashboard .gh-dashboard-list-footer {
|
||||
border-color: #394047;
|
||||
}
|
||||
|
||||
.gh-dashboard5 .gh-dashboard5-box.is-secondary {
|
||||
.gh-dashboard .gh-dashboard-box.is-secondary {
|
||||
border: 1px solid #2b2d31;
|
||||
}
|
||||
|
||||
.gh-dashboard5 .gh-dashboard5-box.is-secondary,
|
||||
.gh-dashboard5 .gh-dashboard5-box.is-faded {
|
||||
.gh-dashboard .gh-dashboard-box.is-secondary,
|
||||
.gh-dashboard .gh-dashboard-box.is-faded {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.gh-dashboard5 .gh-dashboard5-column {
|
||||
.gh-dashboard .gh-dashboard-column {
|
||||
border-color: #2b2d31;
|
||||
}
|
||||
|
||||
.gh-dashboard5 .gh-dashboard5-breakout {
|
||||
.gh-dashboard .gh-dashboard-breakout {
|
||||
background: #111213;
|
||||
}
|
||||
|
||||
.gh-dashboard5-community .gh-dashboard5-list-footer {
|
||||
.gh-dashboard-community .gh-dashboard-list-footer {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.gh-dashboard5-chart-gradient {
|
||||
.gh-dashboard-chart-gradient {
|
||||
background: rgb(21,23,25);
|
||||
background: linear-gradient(270deg, rgba(21,23,25,0) 0%, rgba(21,23,25,1) 100%);
|
||||
}
|
||||
|
||||
.gh-dashboard5-staff-picks .gh-dashboard5-resource-body,
|
||||
.gh-dashboard5-resource-box.is-secondary .gh-dashboard5-resource-footer,
|
||||
.gh-dashboard5-whats-new .gh-dashboard5-resource-body {
|
||||
.gh-dashboard-staff-picks .gh-dashboard-resource-body,
|
||||
.gh-dashboard-resource-box.is-secondary .gh-dashboard-resource-footer,
|
||||
.gh-dashboard-whats-new .gh-dashboard-resource-body {
|
||||
border-color: #2c2e32;
|
||||
}
|
||||
|
||||
.gh-dashboard5-list-item:hover {
|
||||
.gh-dashboard-list-item:hover {
|
||||
background: #1c1e21;
|
||||
}
|
||||
|
||||
.gh-dashboard5-recents .gh-dashboard5-list-item:hover {
|
||||
.gh-dashboard-recents .gh-dashboard-list-item:hover {
|
||||
background: rgba(28, 30, 33, 0.7);
|
||||
}
|
||||
|
||||
.gh-dashboard5-resource .gh-dashboard5-list-item:hover {
|
||||
.gh-dashboard-resource .gh-dashboard-list-item:hover {
|
||||
background: rgba(28, 30, 33, 0.3);
|
||||
}
|
||||
|
||||
.gh-dashboard5-community .gh-dashboard5-resource-footer {
|
||||
.gh-dashboard-community .gh-dashboard-resource-footer {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.gh-dashboard5-zero {
|
||||
.gh-dashboard-zero {
|
||||
background: rgb(21, 23, 25, 0.8);
|
||||
}
|
||||
|
||||
.gh-dashboard5-zero-message {
|
||||
.gh-dashboard-zero-message {
|
||||
background-color: #1c1e21;
|
||||
}
|
||||
|
|
|
@ -69,7 +69,6 @@
|
|||
@import "layouts/fullscreen-wizard.css";
|
||||
@import "layouts/post-preview.css";
|
||||
@import "layouts/dashboard.css";
|
||||
@import "layouts/dashboard-v5.css";
|
||||
@import "layouts/tiers.css";
|
||||
@import "layouts/offers.css"
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -1,166 +1,68 @@
|
|||
<section class="gh-canvas" {{scroll-top}}>
|
||||
{{#if (feature "dashboardV5")}}
|
||||
<div class="gh-dashboard5">
|
||||
<div class="gh-dashboard5-inner">
|
||||
<GhCanvasHeader class="gh-canvas-header">
|
||||
<h2 class="gh-canvas-title" data-test-screen-title>
|
||||
Dashboard
|
||||
</h2>
|
||||
</GhCanvasHeader>
|
||||
</div>
|
||||
<Dashboard::DashboardV5 />
|
||||
<div class="gh-dashboard">
|
||||
<div class="gh-dashboard-inner">
|
||||
<GhCanvasHeader class="gh-canvas-header">
|
||||
<h2 class="gh-canvas-title" data-test-screen-title>
|
||||
Dashboard
|
||||
</h2>
|
||||
</GhCanvasHeader>
|
||||
</div>
|
||||
{{else}}
|
||||
<GhCanvasHeader class="gh-canvas-header">
|
||||
<h2 class="gh-canvas-title" data-test-screen-title>
|
||||
Dashboard
|
||||
</h2>
|
||||
</GhCanvasHeader>
|
||||
<div class="view-container gh-dashboard">
|
||||
{{#if (and this.session.user.isOwnerOnly (not this.settings.editorIsLaunchComplete) (not (feature "improvedOnboarding")))}}
|
||||
<section class="gh-dashboard-area lw-banner">
|
||||
<div class="gh-lw-banner" style="background-image:url(assets/img/launch-wizard-bg.png);">
|
||||
<h1>Select your publication style</h1>
|
||||
<p>Customize your brand and connect to Stripe to get your membership site ready to be shown to the world.</p>
|
||||
<LinkTo class="gh-btn gh-btn-green" @route="launch"><span>Start setup guide</span></LinkTo>
|
||||
<div class="gh-dashboard-dismiss">
|
||||
<GhDropdownButton @dropdownName="launch-wizard-dismiss"
|
||||
@classNames="gh-btn gh-btn-icon icon-only gh-dashboard-dismissbutton dark">
|
||||
<span>
|
||||
{{svg-jar "dotdotdot"}}
|
||||
</span>
|
||||
</GhDropdownButton>
|
||||
<GhDropdown @name="launch-wizard-dismiss" @classNames="gh-dashboard-dismiss-dropdown dropdown-menu dropdown-triangle-top-right">
|
||||
<button class="gh-btn" type="button" {{on "click" this.dismissLaunchBanner}}><span>Dismiss</span></button>
|
||||
</GhDropdown>
|
||||
</div>
|
||||
<section class="gh-dashboard-layout">
|
||||
{{#if this.isLoading }}
|
||||
<GhLoadingSpinner />
|
||||
{{else}}
|
||||
{{#if this.areMembersEnabled}}
|
||||
{{#if this.hasPaidTiers}}
|
||||
<Dashboard::Charts::Overview />
|
||||
{{/if}}
|
||||
|
||||
<div class="gh-dashboard-group {{if this.isTotalMembersZero 'is-zero'}}">
|
||||
<Dashboard::Charts::Anchor />
|
||||
|
||||
{{#if this.areNewslettersEnabled}}
|
||||
<Dashboard::Charts::Engagement />
|
||||
{{/if}}
|
||||
|
||||
{{#if this.isTotalMembersZero}}
|
||||
<Dashboard::Parts::Zero />
|
||||
{{/if}}
|
||||
</div>
|
||||
</section>
|
||||
{{else if this.showMembersGraphs}}
|
||||
<Dashboard::MembersGraphs />
|
||||
{{/if}}
|
||||
|
||||
<Dashboard::Charts::Recents />
|
||||
|
||||
<div class="gh-dashboard-split gh-dashboard-box is-secondary">
|
||||
<Dashboard::Resources::Resources />
|
||||
<Dashboard::Resources::Newsletter />
|
||||
</div>
|
||||
<div class="gh-dashboard-split">
|
||||
<Dashboard::Resources::StaffPicks />
|
||||
<Dashboard::Resources::WhatsNew />
|
||||
<Dashboard::Resources::Community />
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<section class="gh-dashboard-area mixed">
|
||||
{{#unless this.settings.editorIsLaunchComplete}}
|
||||
<div class="gh-dashboard-container start-contents">
|
||||
<div class="gh-dashboard-box blogpost">
|
||||
<h2>Start creating content</h2>
|
||||
{{#if this.showMembersData}}
|
||||
<LinkTo @route="members">
|
||||
<span class="icon">{{svg-jar "members"}}</span>
|
||||
<div>
|
||||
<h4>Create your first member</h4>
|
||||
<p>Add yourself or import members from CSV</p>
|
||||
</div>
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
<LinkTo @route="editor.new" @model="post">
|
||||
<span class="icon green">{{svg-jar "posts"}}</span>
|
||||
<div>
|
||||
<h4>Publish a post</h4>
|
||||
<p>Get familiar with the Ghost editor and start creating</p>
|
||||
</div>
|
||||
</LinkTo>
|
||||
</div>
|
||||
{{#unless this.isTotalMembersZero}}
|
||||
<div class="gh-dashboard-select">
|
||||
<PowerSelect
|
||||
@selected={{this.selectedDaysOption}}
|
||||
@options={{this.daysOptions}}
|
||||
@searchEnabled={{false}}
|
||||
@onChange={{this.onDaysChange}}
|
||||
@triggerComponent="gh-power-select/trigger"
|
||||
@triggerClass="gh-contentfilter-menu-trigger"
|
||||
@dropdownClass="gh-contentfilter-menu-dropdown is-narrow"
|
||||
@matchTriggerWidth={{false}}
|
||||
as |option|
|
||||
>
|
||||
{{#if option.name}}{{option.name}}{{else}}<span class="red">Unknown option</span>{{/if}}
|
||||
</PowerSelect>
|
||||
</div>
|
||||
{{/unless}}
|
||||
{{/unless}}
|
||||
</section>
|
||||
|
||||
<div class="gh-dashboard-container col-2">
|
||||
<div class="gh-dashboard-box">
|
||||
<div class="content">
|
||||
<h2>Customize your site</h2>
|
||||
<p>Stand out from the crowd. Ghost lets you customize everything so you can create a publication that doesn’t just look the same as what everyone else has.</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<LinkTo class="gh-btn gh-btn-outline mt2 mr2" @route="settings.design"><span>Design</span></LinkTo>
|
||||
<LinkTo class="gh-btn gh-btn-outline mt2" @route="settings.newsletters"><span>Email</span></LinkTo>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-dashboard-box">
|
||||
<div class="content">
|
||||
<h2>Looking for help with Ghost features?</h2>
|
||||
<p>Our product knowledgebase is packed full of guides, tutorials, answers to frequently asked questions, tips for dealing with common errors, and much more. </p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<a class="gh-btn gh-btn-outline mt2" href="https://ghost.org/help/" target="_blank" rel="noopener noreferrer"><span>Visit the help center →</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a class="gh-dashboard-container" href="https://ghost.org/blog/types-of-newsletters/?utm_source=dashboard" target="_blank" rel="noopener noreferrer">
|
||||
<div class="gh-dashboard-box blogpost">
|
||||
<div class="content">
|
||||
<h2>6 types of newsletters you can start today</h2>
|
||||
<p>Choosing one of these newsletter types for your publication will help you create better content at a faster pace with less work.</p>
|
||||
<p class="green">Get some inspiration →</p>
|
||||
<div class="read-time">5 MIN READ</div>
|
||||
</div>
|
||||
<div class="thumbnail" style="background-image: url(assets/img/dashboard/bp1.jpg);"></div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a class="gh-dashboard-container" href="https://careers.ghost.org?utm_source=dashboard" target="_blank" rel="noopener noreferrer">
|
||||
<div class="gh-dashboard-box grey gh-dashboard-careers">
|
||||
<div class="summary">
|
||||
<h2>We're hiring! Join the team that makes Ghost.</h2>
|
||||
<p>The creator economy is growing faster than ever, and so are we! 📈 Join a team that's determined to make decentralised, open technology the heart and soul of new media 🌺</p>
|
||||
</div>
|
||||
<div class="gh-dashboard-careers-cta">
|
||||
<span class="gh-btn gh-btn-primary"><span>See open roles →</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a class="gh-dashboard-container reverse" href="https://ghost.org/blog/content-strategy-creator-funnel/?utm_source=dashboard" target="_blank" rel="noopener noreferrer">
|
||||
<div class="gh-dashboard-box blogpost">
|
||||
<div class="thumbnail" style="background-image: url(assets/img/dashboard/bp2.jpg);"></div>
|
||||
<div class="content">
|
||||
<h2>How to grow your audience, starting from 0</h2>
|
||||
<p>Starting from zero is hard. Thankfully, successful creators have given us clues on how to grow an audience by using something called a content funnel.</p>
|
||||
<p class="green">Here's how it works →</p>
|
||||
<div class="read-time">9 MIN READ</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div class="gh-dashboard-join-community" style="background-image: url(assets/img/dashboard/join-community.jpg)">
|
||||
<div>
|
||||
<h2>Join the Ghost creator community.</h2>
|
||||
<p>Meet other people building both free & paid publications with Ghost. Talk strategy, get advice, or just hang out.</p>
|
||||
<a class="gh-btn gh-btn-white gh-dashboard-btn" href="https://community.ghost.org" target="_blank" rel="noopener noreferrer"><span>Share the journey</span></a>
|
||||
</div>
|
||||
<a class="footer-link" href="https://community.ghost.org" target="_blank" rel="noopener noreferrer">community.ghost.org</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="gh-dashboard-area members-activity">
|
||||
{{#if this.showMembersData}}
|
||||
<Dashboard::LatestMemberActivity />
|
||||
{{/if}}
|
||||
|
||||
{{#if (not (or this.whatsNewEntriesLoading this.whatsNewEntriesError))}}
|
||||
<div class="gh-dashboard-box whats-new {{if this.whatsNew.hasNew "has-new"}}">
|
||||
<div class="gh-dashboard-header-container">
|
||||
<h4 class="gh-dashboard-header">What's new?</h4>
|
||||
{{svg-jar "gift"}}
|
||||
</div>
|
||||
<div class="content">
|
||||
{{#each this.whatsNewEntries as |entry|}}
|
||||
<LinkTo @route="whatsnew" @query={{hash entry=entry.slug}}>
|
||||
<h2>{{entry.title}}</h2>
|
||||
<span class="wn-date">{{moment-format entry.published_at "D MMM YYYY"}}</span>
|
||||
{{#if entry.custom_excerpt}}
|
||||
<p>{{entry.custom_excerpt}}</p>
|
||||
{{/if}}
|
||||
</LinkTo>
|
||||
{{/each}}
|
||||
</div>
|
||||
<div class="footer">
|
||||
<LinkTo @route="whatsnew" @query={{hash entry=null}} class="green">See more →</LinkTo>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</section>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if (enable-developer-experiments)}}
|
||||
<Dashboard::Prototype::ControlPanel />
|
||||
{{/if}}
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -278,19 +278,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-expandable-block">
|
||||
<div class="gh-expandable-header">
|
||||
<div>
|
||||
<h4 class="gh-expandable-title">Dashboard 5.0</h4>
|
||||
<p class="gh-expandable-description">
|
||||
Increased amount of analytics with more useful / dynamic resources
|
||||
</p>
|
||||
</div>
|
||||
<div class="for-switch">
|
||||
<GhFeatureFlag @flag="dashboardV5" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
|
|
@ -17,6 +17,7 @@ import mockSettings from './config/settings';
|
|||
import mockSite from './config/site';
|
||||
import mockSlugs from './config/slugs';
|
||||
import mockSnippets from './config/snippets';
|
||||
import mockStats from './config/stats';
|
||||
import mockTags from './config/tags';
|
||||
import mockThemes from './config/themes';
|
||||
import mockTiers from './config/tiers';
|
||||
|
@ -81,6 +82,7 @@ export function testConfig() {
|
|||
mockOffers(this);
|
||||
mockSnippets(this);
|
||||
mockNewsletters(this);
|
||||
mockStats(this);
|
||||
|
||||
/* Notifications -------------------------------------------------------- */
|
||||
|
||||
|
|
66
ghost/admin/mirage/config/stats.js
Normal file
66
ghost/admin/mirage/config/stats.js
Normal file
|
@ -0,0 +1,66 @@
|
|||
export default function mockStats(server) {
|
||||
server.get('/stats/subscriptions/', function () {
|
||||
return {
|
||||
stats: [],
|
||||
meta: {
|
||||
cadences: [],
|
||||
tiers: [],
|
||||
totals: []
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
server.get('/stats/member_count/', function () {
|
||||
return {
|
||||
stats: [
|
||||
{
|
||||
date: '2022-04-18',
|
||||
paid: 0,
|
||||
free: 2,
|
||||
comped: 0,
|
||||
paid_subscribed: 0,
|
||||
paid_canceled: 0
|
||||
},
|
||||
{
|
||||
date: '2022-04-19',
|
||||
paid: 0,
|
||||
free: 2,
|
||||
comped: 0,
|
||||
paid_subscribed: 2,
|
||||
paid_canceled: 0
|
||||
},
|
||||
{
|
||||
date: '2022-04-28',
|
||||
paid: 0,
|
||||
free: 12,
|
||||
comped: 0,
|
||||
paid_subscribed: 0,
|
||||
paid_canceled: 0
|
||||
},
|
||||
{
|
||||
date: '2022-05-02',
|
||||
paid: 0,
|
||||
free: 35,
|
||||
comped: 0,
|
||||
paid_subscribed: 0,
|
||||
paid_canceled: 0
|
||||
},
|
||||
{
|
||||
date: '2022-05-10',
|
||||
paid: 0,
|
||||
free: 38,
|
||||
comped: 1,
|
||||
paid_subscribed: 0,
|
||||
paid_canceled: 0
|
||||
}
|
||||
],
|
||||
meta: {
|
||||
totals: {
|
||||
paid: 0,
|
||||
free: 38,
|
||||
comped: 1
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';
|
||||
import {currentURL, find, visit} from '@ember/test-helpers';
|
||||
import {currentURL, visit} from '@ember/test-helpers';
|
||||
import {describe, it} from 'mocha';
|
||||
import {enableLabsFlag} from '../helpers/labs-flag';
|
||||
import {expect} from 'chai';
|
||||
|
@ -32,28 +32,6 @@ describe('Acceptance: Dashboard', function () {
|
|||
expect(currentURL()).to.equal('/dashboard');
|
||||
});
|
||||
|
||||
describe('members graphs', function () {
|
||||
it('is shown when members exist', async function () {
|
||||
this.server.createList('member', 5);
|
||||
await visit('/dashboard');
|
||||
expect(find('[data-test-dashboard-members-graphs]'), 'members graphs block').to.exist;
|
||||
});
|
||||
|
||||
it('is hidden when no members exist', async function () {
|
||||
this.server.db.members.remove();
|
||||
await visit('/dashboard');
|
||||
expect(find('[data-test-dashboard-members-graphs]'), 'members graphs block').to.not.exist;
|
||||
});
|
||||
|
||||
it('is hidden when members is disabled', async function () {
|
||||
this.server.createList('member', 5);
|
||||
this.server.db.settings.update({key: 'members_signup_access'}, {value: 'none'});
|
||||
|
||||
await visit('/dashboard');
|
||||
expect(find('[data-test-dashboard-members-graphs]'), 'members graphs block').to.not.exist;
|
||||
});
|
||||
});
|
||||
|
||||
describe('permissions', function () {
|
||||
beforeEach(async function () {
|
||||
this.server.db.users.remove();
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
import hbs from 'htmlbars-inline-precompile';
|
||||
import {describe, it} from 'mocha';
|
||||
import {expect} from 'chai';
|
||||
import {find, findAll, render} from '@ember/test-helpers';
|
||||
import {setupMirage} from 'ember-cli-mirage/test-support';
|
||||
import {setupRenderingTest} from 'ember-mocha';
|
||||
|
||||
describe('Integration: Component: <Dashboard::LatestMemberActivity>', function () {
|
||||
const hooks = setupRenderingTest();
|
||||
setupMirage(hooks);
|
||||
|
||||
it('renders with no activities', async function () {
|
||||
await render(hbs(`<Dashboard::LatestMemberActivity />`));
|
||||
|
||||
expect(find('[data-test-dashboard-member-activity]')).to.exist;
|
||||
expect(find('[data-test-no-member-activities]')).to.exist;
|
||||
});
|
||||
|
||||
it('renders 5 latest activities', async function () {
|
||||
this.server.createList('member-activity-event', 10);
|
||||
|
||||
await render(hbs(`<Dashboard::LatestMemberActivity />`));
|
||||
|
||||
expect(find('[data-test-dashboard-member-activity]')).to.exist;
|
||||
expect(find('[data-test-no-member-activities]')).to.not.exist;
|
||||
|
||||
expect(findAll('[data-test-dashboard-member-activity-item]').length).to.equal(5);
|
||||
});
|
||||
});
|
Loading…
Add table
Reference in a new issue