mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-29 13:52:10 +03:00
Removed labs dashboard and feature flag
no issue The updated dashboard is shelved for now whilst the underlying analytics architecture is improved.
This commit is contained in:
parent
8e84b8e5ef
commit
b540d9f066
@ -1,270 +0,0 @@
|
||||
import Controller from '@ember/controller';
|
||||
import {action} from '@ember/object';
|
||||
import {getSymbol} from 'ghost-admin/utils/currency';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {task} from 'ember-concurrency-decorators';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
|
||||
export default class DashboardController extends Controller {
|
||||
@service feature;
|
||||
@service session;
|
||||
@service membersStats;
|
||||
@service store;
|
||||
@service settings;
|
||||
@service whatsNew;
|
||||
|
||||
@tracked eventsData = null;
|
||||
@tracked eventsError = null;
|
||||
@tracked eventsLoading = false;
|
||||
|
||||
@tracked mrrStatsData = null;
|
||||
@tracked mrrStatsError = null;
|
||||
@tracked mrrStatsLoading = false;
|
||||
|
||||
@tracked memberCountStatsData = null;
|
||||
@tracked memberCountStatsError = null;
|
||||
@tracked memberCountStatsLoading = false;
|
||||
|
||||
@tracked topMembersData = null;
|
||||
@tracked topMembersError = null;
|
||||
@tracked topMembersLoading = false;
|
||||
|
||||
@tracked newsletterOpenRatesData = null;
|
||||
@tracked newsletterOpenRatesError = null;
|
||||
@tracked newsletterOpenRatesLoading = false;
|
||||
|
||||
@tracked latestNewsletters = null;
|
||||
|
||||
@tracked whatsNewEntries = null;
|
||||
@tracked whatsNewEntriesLoading = null;
|
||||
@tracked whatsNewEntriesError = null;
|
||||
|
||||
get topMembersDataHasOpenRates() {
|
||||
return this.topMembersData && this.topMembersData.find((member) => {
|
||||
return member.emailOpenRate !== null;
|
||||
});
|
||||
}
|
||||
|
||||
get showMembersData() {
|
||||
return this.settings.get('membersSignupAccess') !== 'none';
|
||||
}
|
||||
|
||||
initialise() {
|
||||
this.loadEvents();
|
||||
this.loadTopMembers();
|
||||
this.loadCharts();
|
||||
this.loadLatestNewsletters.perform();
|
||||
this.loadWhatsNew();
|
||||
}
|
||||
|
||||
async loadMRRStats() {
|
||||
const products = await this.store.query('product', {include: 'monthly_price,yearly_price', limit: 'all'});
|
||||
const defaultProduct = products?.firstObject;
|
||||
|
||||
this.mrrStatsLoading = true;
|
||||
this.membersStats.fetchMRR().then((stats) => {
|
||||
this.mrrStatsLoading = false;
|
||||
const statsData = stats.data || [];
|
||||
const defaultCurrency = defaultProduct?.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 currentFreeCount = dateValues.length ? dateValues[dateValues.length - 1].free : 0;
|
||||
const rangeStartAllCount = dateValues.length ? dateValues[0].total : 0;
|
||||
const rangeStartPaidCount = dateValues.length ? dateValues[0].paid : 0;
|
||||
const rangeStartFreeCount = dateValues.length ? dateValues[0].free : 0;
|
||||
const allCountPercentGrowth = rangeStartAllCount !== 0 ? ((currentAllCount - rangeStartAllCount) / rangeStartAllCount) * 100 : 0;
|
||||
const paidCountPercentGrowth = rangeStartPaidCount !== 0 ? ((currentPaidCount - rangeStartPaidCount) / rangeStartPaidCount) * 100 : 0;
|
||||
const freeCountPercentGrowth = rangeStartFreeCount !== 0 ? ((currentFreeCount - rangeStartFreeCount) / rangeStartFreeCount) * 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
|
||||
},
|
||||
free: {
|
||||
percentGrowth: freeCountPercentGrowth.toFixed(1),
|
||||
percentClass: (freeCountPercentGrowth > 0 ? 'positive' : (freeCountPercentGrowth < 0 ? 'negative' : '')),
|
||||
total: dateValues.length ? dateValues[dateValues.length - 1].free : 0,
|
||||
options: {
|
||||
rangeInDays: 30
|
||||
},
|
||||
data: {
|
||||
label: 'Members',
|
||||
dateLabels: Object.keys(statsDateObj),
|
||||
dateValues: dateValues.map(d => d.paid)
|
||||
},
|
||||
title: 'Free Members',
|
||||
stats: stats
|
||||
}
|
||||
};
|
||||
}
|
||||
}, (error) => {
|
||||
this.memberCountStatsError = error;
|
||||
this.memberCountStatsLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
loadCharts() {
|
||||
this.loadMRRStats();
|
||||
this.loadMemberCountStats();
|
||||
this.loadNewsletterOpenRates();
|
||||
}
|
||||
|
||||
loadEvents() {
|
||||
this.eventsLoading = true;
|
||||
this.membersStats.fetchTimeline({limit: 5}).then(({events}) => {
|
||||
this.eventsData = events;
|
||||
this.eventsLoading = false;
|
||||
}, (error) => {
|
||||
this.eventsError = error;
|
||||
this.eventsLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
@task
|
||||
*loadLatestNewsletters() {
|
||||
this.latestNewsletters = yield this.store.query('email', {
|
||||
limit: 5,
|
||||
order: 'created_at desc'
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
loadTopMembers() {
|
||||
this.topMembersLoading = true;
|
||||
let query = {
|
||||
filter: 'email_open_rate:-null',
|
||||
order: 'email_open_rate desc',
|
||||
limit: 5
|
||||
};
|
||||
this.store.query('member', query).then((result) => {
|
||||
if (!result.length) {
|
||||
return this.store.query('member', {
|
||||
filter: 'status:paid',
|
||||
order: 'created_at asc',
|
||||
limit: 5
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}).then((result) => {
|
||||
this.topMembersData = result;
|
||||
this.topMembersLoading = false;
|
||||
}).catch((error) => {
|
||||
this.topMembersError = error;
|
||||
this.topMembersLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
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.feature.set('launchComplete', true);
|
||||
}
|
||||
}
|
@ -24,7 +24,6 @@ Router.map(function () {
|
||||
this.route('whatsnew');
|
||||
this.route('site');
|
||||
this.route('dashboard');
|
||||
this.route('dashboard-labs');
|
||||
this.route('launch');
|
||||
|
||||
this.route('pro', function () {
|
||||
|
@ -1,28 +0,0 @@
|
||||
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default class DashboardRoute extends AuthenticatedRoute {
|
||||
@service feature;
|
||||
|
||||
beforeModel() {
|
||||
super.beforeModel(...arguments);
|
||||
|
||||
if (!this.session.user.isAdmin) {
|
||||
return this.transitionTo('site');
|
||||
}
|
||||
|
||||
if (!this.feature.dashboardTwo) {
|
||||
return this.transitionTo('dashboard');
|
||||
}
|
||||
}
|
||||
|
||||
buildRouteInfoMetadata() {
|
||||
return {
|
||||
mainClasses: ['gh-main-wide']
|
||||
};
|
||||
}
|
||||
|
||||
setupController() {
|
||||
this.controller.initialise();
|
||||
}
|
||||
}
|
@ -10,10 +10,6 @@ export default class DashboardRoute extends AuthenticatedRoute {
|
||||
if (!this.session.user.isAdmin) {
|
||||
return this.transitionTo('site');
|
||||
}
|
||||
|
||||
if (this.feature.dashboardTwo) {
|
||||
return this.transitionTo('dashboard-labs');
|
||||
}
|
||||
}
|
||||
|
||||
buildRouteInfoMetadata() {
|
||||
|
@ -57,7 +57,6 @@ export default Service.extend({
|
||||
membersFiltering: feature('membersFiltering', {developer: true}),
|
||||
oauthLogin: feature('oauthLogin', {developer: true}),
|
||||
emailOnlyPosts: feature('emailOnlyPosts', {developer: true}),
|
||||
dashboardTwo: feature('dashboardTwo', {developer: true}),
|
||||
|
||||
_user: null,
|
||||
|
||||
|
@ -1,388 +0,0 @@
|
||||
<section class="gh-canvas">
|
||||
<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.feature.launchComplete))}}
|
||||
<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" {{action "dismissLaunchBanner"}}><span>Dismiss</span></button>
|
||||
</GhDropdown>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{else if this.showMembersData}}
|
||||
<section class="gh-dashboard-area charts">
|
||||
<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.memberCountStatsLoading}}
|
||||
Loading...
|
||||
{{else}}
|
||||
{{#if this.memberCountStatsError}}
|
||||
<p class="error">
|
||||
There was an error loading free members
|
||||
<code>{{this.memberCountStatsData.message}}</code>
|
||||
</p>
|
||||
{{else}}
|
||||
<div class="gh-dashboard-summary small">
|
||||
<h4 class="gh-dashboard-header">Free members</h4>
|
||||
<div class="data-container">
|
||||
<div class="data">{{format-number this.memberCountStatsData.free.total}}</div>
|
||||
<div class="growth {{this.memberCountStatsData.free.percentClass}}">{{this.memberCountStatsData.free.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.free}} />
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{/if}}
|
||||
|
||||
<section class="gh-dashboard-area mixed">
|
||||
<div class="gh-dashboard-container">
|
||||
<div class="gh-dashboard-box">
|
||||
<div class="gh-dashboard-header-container">
|
||||
<h4 class="gh-dashboard-header w-100">Latest newsletters</h4>
|
||||
</div>
|
||||
<div class="content">
|
||||
{{#if this.loadLatestNewsletters.isRunning}}
|
||||
Loading...
|
||||
{{else}}
|
||||
<ol class="gh-list">
|
||||
<li class="gh-list-row header">
|
||||
<div class="gh-list-header w-100">Title</div>
|
||||
<div class="gh-list-header">Sends</div>
|
||||
<div class="gh-list-header">Open %</div>
|
||||
</li>
|
||||
{{#each this.latestNewsletters as |newsletter|}}
|
||||
<li class="gh-list-row">
|
||||
<LinkTo @route="editor.edit" @models={{array "post" newsletter.postId}} class="gh-list-data">{{newsletter.subject}}</LinkTo>
|
||||
<div class="gh-list-data">{{newsletter.emailCount}}</div>
|
||||
<div class="gh-list-data">{{newsletter.openRate}}%</div>
|
||||
</li>
|
||||
{{else}}
|
||||
<li>No newsletters sent.</li>
|
||||
{{/each}}
|
||||
</ol>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="footer">
|
||||
<LinkTo @route="posts" class="green">See all posts</LinkTo>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if (not this.feature.launchComplete)}}
|
||||
<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>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="gh-dashboard-container col-2">
|
||||
<div class="gh-dashboard-box">
|
||||
<div class="content">
|
||||
<h2>Customize your site design</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"><span>Brand</span></LinkTo>
|
||||
<LinkTo class="gh-btn gh-btn-outline mt2 mr2" @route="settings.members-email"><span>Email</span></LinkTo>
|
||||
<LinkTo class="gh-btn gh-btn-outline mt2" @route="settings.theme"><span>Theme</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"><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">
|
||||
<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}}
|
||||
{{#if this.topMembersData}}
|
||||
<div class="gh-dashboard-box grey top-members">
|
||||
<div class="gh-dashboard-header-container">
|
||||
<h4 class="gh-dashboard-header">Top members</h4>
|
||||
{{#if this.topMembersDataHasOpenRates}}
|
||||
<h4 class="gh-dashboard-header secondary">Open rate</h4>
|
||||
{{else}}
|
||||
<h4 class="gh-dashboard-header secondary">Member since</h4>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="gh-dashboard-list">
|
||||
{{#if this.topMembersLoading}}
|
||||
Loading...
|
||||
{{else}}
|
||||
{{#if this.topMembersError}}
|
||||
<p class="tc">
|
||||
There was an error loading member events.
|
||||
<code class="hidden">{{this.events.error.message}}</code>
|
||||
</p>
|
||||
{{else}}
|
||||
<ul class="gh-dashboard-top-members">
|
||||
{{#each this.topMembersData as |member|}}
|
||||
<li class="gh-dashboard-top-member">
|
||||
<LinkTo class="member-details" @route="member" @model="{{member.id}}">
|
||||
<GhMemberAvatar @member={{member}} @containerClass="w9 h9 mr3 flex-shrink-0" />
|
||||
{{#if member.name}}
|
||||
<span class="name">{{member.name}}</span>
|
||||
{{else}}
|
||||
<span class="email">{{member.email}}</span>
|
||||
{{/if}}
|
||||
</LinkTo>
|
||||
{{#if member.emailOpenRate}}
|
||||
<span class="open-rate">{{member.emailOpenRate}}%</span>
|
||||
{{else}}
|
||||
<span class="open-rate">
|
||||
{{moment-format member.createdAtUTC "D MMM YYYY"}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
<div class="gh-dashboard-top-members-footer">
|
||||
<LinkTo @route="members">See all members {{svg-jar "arrow-right"}}</LinkTo>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="gh-dashboard-box">
|
||||
<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>
|
||||
|
||||
{{#unless (and this.session.user.isOwnerOnly (not this.feature.launchComplete))}}
|
||||
<div class="gh-dashboard-box grey activity-feed">
|
||||
<h4 class="gh-dashboard-header">Activity feed</h4>
|
||||
<div class="content">
|
||||
{{#if this.eventsLoading}}
|
||||
Loading...
|
||||
{{else}}
|
||||
{{#if this.eventsError}}
|
||||
<p class="error">
|
||||
There was an error loading events
|
||||
<code>{{this.eventsError.message}}</code>
|
||||
</p>
|
||||
{{else}}
|
||||
<GhEventTimeline @events={{this.eventsData}}/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
|
||||
{{#unless (or whatsNewEntriesLoading whatsNewEntriesError)}}
|
||||
<div class="gh-dashboard-box whats-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|}}
|
||||
<a href={{entry.url}} target="_blank" rel="noopener noreferrer">
|
||||
<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}}
|
||||
</a>
|
||||
{{/each}}
|
||||
</div>
|
||||
<div class="footer">
|
||||
<a class="green" href="https://ghost.org/changelog" target="_blank" rel="noopener noreferrer">See all updates</a>
|
||||
</div>
|
||||
</div>
|
||||
{{/unless}}
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
@ -287,19 +287,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-expandable-block">
|
||||
<div class="gh-expandable-header">
|
||||
<div>
|
||||
<h4 class="gh-expandable-title">Dashboard 2.0</h4>
|
||||
<p class="gh-expandable-description">
|
||||
More graphs. Better graphs. Fewer useless graphs.
|
||||
</p>
|
||||
</div>
|
||||
<div class="for-switch">
|
||||
<GhFeatureFlag @flag="dashboardTwo" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
Loading…
Reference in New Issue
Block a user