0
Fork 0
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:
Simon Backx 2022-05-17 09:34:34 +02:00 committed by GitHub
parent 8b5b3aa734
commit 8502ebb96a
57 changed files with 2464 additions and 4393 deletions

View file

@ -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>

View file

@ -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)) {

View file

@ -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}}

View file

@ -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';

View file

@ -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}}

View file

@ -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() {

View file

@ -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>

View file

@ -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;
}
}
},

View file

@ -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}}

View file

@ -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) {

View file

@ -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>

View file

@ -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;
}
}
},

View file

@ -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">&mdash;</span>
<span class="gh-dashboard-metric-minivalue na">&mdash;</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 &rarr;</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 &rarr;</LinkTo>
</div>
</div>

View file

@ -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;
}

View file

@ -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}}

View file

@ -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;
}
}

View file

@ -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}}

View file

@ -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;
}
}

View file

@ -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>

View file

@ -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;
});
}
}

View file

@ -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}}

View file

@ -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}}

View file

@ -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 &rarr;</LinkTo></p>

View 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 &rarr;
</div>
</a>
</section>

View file

@ -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&nbsp;<span>to the newsletter&nbsp;</span>&rarr;</a>
</div>
</article>
</section>

View 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 &rarr;</a>
</div>
</div>
{{/if}}
</article>
</section>

View file

@ -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>

View 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 &rarr;</LinkTo>
</div>
</article>
</section>

View file

@ -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>

View file

@ -1,3 +0,0 @@
<div class="gh-dashboard5-zero">
</div>

View file

@ -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}}

View file

@ -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 &rarr;
</div>
</a>
</section>

View file

@ -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&nbsp;<span>to the newsletter&nbsp;</span>&rarr;</a>
</div>
</article>
</section>

View file

@ -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 &rarr;</a>
</div>
</div>
{{/if}}
</article>
</section>

View file

@ -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>

View file

@ -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 &rarr;</LinkTo>
</div>
</article>
</section>

View file

@ -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>

View file

@ -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();
}
}
});
}
});

View file

@ -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;
}
}

View file

@ -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();
}
}

View file

@ -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;

View file

@ -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;
}

View file

@ -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

View file

@ -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 doesnt 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 &rarr;</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 &rarr;</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 &rarr;</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 &rarr;</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 &amp; 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 &rarr;</LinkTo>
</div>
</div>
{{/if}}
</section>
</div>
{{/if}}
{{#if (enable-developer-experiments)}}
<Dashboard::Prototype::ControlPanel />
{{/if}}
</div>
</section>

View file

@ -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}}

View file

@ -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 -------------------------------------------------------- */

View 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
}
}
};
});
}

View file

@ -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();

View file

@ -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);
});
});