The big cleanup (#2328)

* Trying out a tooltip alternative for combined graph in new dashboard

refs: https://github.com/TryGhost/Team/issues/1467

* Trying out a different type of interaction with the combined graph for the new dashboard that includes different style

refs: https://github.com/TryGhost/Team/issues/1467

* Working through the interface and code to majorly clean up for the new Dashboard

refs: https://github.com/TryGhost/Team/issues/1462

- lots of moving around css
- trying out some different layouts
- refactoring lots of code
- known bug: paid graphs don't work
- known bug: without newsletters, layout breaks

* Finishing up the basic styling of the new dashboard to be more presentable

refs: https://github.com/TryGhost/Team/issues/1462

- add an animation between the top metrics on combined graph
- ensure all graphs are responsive to parent container
- refactor many of the components and tidy up the styles
- tighten up spacing, headers, chart heights and more
- make the tooltip hovers a little more presentable
- balance the colors to be more muted, for the moment
- a million other tiny tweaks
This commit is contained in:
James Morris 2022-04-06 18:11:46 +01:00 committed by GitHub
parent 9b8875c4be
commit 560d862d91
25 changed files with 923 additions and 1100 deletions

View File

@ -7,7 +7,7 @@
<article class="gh-dashboard5-box">
<Dashboard::V5::ChartAnchor @days={{this.days}} />
</article>
<div class="prototype-selection">
<div id="gh-dashboard5-anchor-selection" class="prototype-selection">
<PowerSelect
@selected={{this.selectedDaysOption}}
@options={{this.daysOptions}}
@ -22,6 +22,9 @@
{{#if option.name}}{{option.name}}{{else}}<span class="red">Unknown option</span>{{/if}}
</PowerSelect>
</div>
<div id="gh-dashboard5-anchor-globaldate">
March 31, 2022
</div>
</section>
{{#if this.hasPaidTiers}}
@ -42,15 +45,15 @@
{{#if this.areNewslettersEnabled}}
<section class="gh-dashboard5-split is-third">
<section class="gh-dashboard5-section gh-dashboard5-engagement">
<Dashboard::V5::ChartEngagement />
</section>
<section class="gh-dashboard5-section gh-dashboard5-email">
<article class="gh-dashboard5-box">
<Dashboard::V5::ChartEmail />
</article>
</section>
<section class="gh-dashboard5-section gh-dashboard5-engagement">
<Dashboard::V5::ChartEngagement />
</section>
</section>
{{else}}
<section class="gh-dashboard5-section gh-dashboard5-engagement">
@ -76,7 +79,9 @@
</section>
<section class="gh-dashboard5-section gh-dashboard5-activity">
<Dashboard::V5::RecentActivity />
<article class="gh-dashboard5-box">
<Dashboard::V5::RecentActivity />
</article>
</section>
</section>

View File

@ -1,41 +1,51 @@
<div class="gh-dashboard5-stats" {{did-insert this.loadCharts}}>
<button type="button" {{on "click" (fn this.changeChartDisplay "total")}} class={{if this.chartShowingTotal 'is-selected'}}>
<div class="gh-dashboard5-number">
{{format-number this.totalMembers}}
{{#if this.hasTrends}}
<Dashboard::v5::parts::ChartPercentage @percentage={{this.totalMembersTrend}}/>
{{/if}}
</div>
<small class="gh-dashboard5-info">{{gh-pluralize this.totalMembers "Total member" without-count=true}}</small>
<button class="gh-dashboard5-stats-button {{if this.chartShowingTotal 'is-selected'}}" type="button" {{on "click" (fn this.changeChartDisplay "total")}}>
<Dashboard::v5::parts::ChartMetric
@label={{gh-pluralize this.totalMembers "Total member" without-count=true}}
@value={{format-number this.totalMembers}}
@trends={{this.hasTrends}}
@percentage={{this.totalMembersTrend}}
@large={{true}} />
<div class="gh-dashboard5-stats-highlight"></div>
</button>
{{#if this.hasPaidTiers}}
<button type="button" {{on "click" (fn this.changeChartDisplay "paid")}} class={{if this.chartShowingPaid 'is-selected'}}>
<div class="gh-dashboard5-number">
{{format-number this.paidMembers}}
{{#if this.hasTrends}}
<Dashboard::v5::parts::ChartPercentage @percentage={{this.paidMembersTrend}}/>
{{/if}}
</div>
<small class="gh-dashboard5-info">{{gh-pluralize this.paidMembers "Paid member" without-count=true}}</small>
<button class="gh-dashboard5-stats-button {{if this.chartShowingMonthly 'is-selected'}}" type="button" {{on "click" (fn this.changeChartDisplay "monthly")}}>
<Dashboard::v5::parts::ChartMetric
@label={{gh-pluralize this.paidMembers "Paid member" without-count=true}}
@value={{format-number this.paidMembers}}
@trends={{this.hasTrends}}
@percentage={{this.paidMembersTrend}}
@large={{true}} />
<div class="gh-dashboard5-stats-highlight"></div>
</button>
<button type="button" {{on "click" (fn this.changeChartDisplay "monthly")}} class={{if this.chartShowingMonthly 'is-selected'}}>
<div class="gh-dashboard5-number">
${{gh-price-amount this.currentMRR}}
{{#if this.hasTrends}}
<Dashboard::v5::parts::ChartPercentage @percentage={{this.mrrTrend}}/>
{{/if}}
</div>
<small class="gh-dashboard5-info">Monthly revenue (MRR)</small>
<button class="gh-dashboard5-stats-button {{if this.chartShowingPaid 'is-selected'}}" type="button" {{on "click" (fn this.changeChartDisplay "paid")}}>
<Dashboard::v5::parts::ChartMetric
@label="Monthly revenue (MRR)"
@value="${{gh-price-amount this.currentMRR}}"
@trends={{this.hasTrends}}
@percentage={{this.mrrTrend}}
@large={{true}} />
<div class="gh-dashboard5-stats-highlight"></div>
</button>
{{/if}}
</div>
<div class="gh-dashboard5-chart">
{{#if this.loading}}
<div class="gh-dashboard5-loading" style={{html-safe (concat "height: " this.chartHeight "px;")}}/>
<div class="gh-dashboard5-chart-loading" style={{html-safe (concat "height: " this.chartHeight "px;")}}/>
{{else}}
<EmberChart
@type={{this.chartType}}
@data={{this.chartData}}
@options={{this.chartOptions}} />
<div class="gh-dashboard5-chart-ticks">
<span>1,000</span>
<span>750</span>
<span>500</span>
<span>250</span>
</div>
<div class="gh-dashboard5-chart-container">
<EmberChart
@type={{this.chartType}}
@data={{this.chartData}}
@options={{this.chartOptions}}
@height={{this.chartHeight}} />
</div>
{{/if}}
</div>

View File

@ -1,8 +1,11 @@
import Component from '@glimmer/component';
import moment from 'moment';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
import {tracked} from '@glimmer/tracking';
const DATE_FORMAT = 'D MMM YYYY';
export default class ChartAnchor extends Component {
@service dashboardStats;
@tracked chartDisplay = 'total';
@ -16,6 +19,8 @@ export default class ChartAnchor extends Component {
@action
changeChartDisplay(type) {
this.chartDisplay = type;
document.querySelector('#gh-dashboard5-bar').classList.remove('is-show');
document.querySelector('#gh-dashboard5-anchor-tooltip').classList.remove('is-show');
}
get chartShowingTotal() {
@ -112,17 +117,20 @@ export default class ChartAnchor extends Component {
labels: labels,
datasets: [{
data: data,
tension: 0.1,
tension: 0,
cubicInterpolationMode: 'monotone',
fill: true,
fillColor: '#F5FBFF',
backgroundColor: '#F5FBFF',
fillColor: '#F3F7FF',
backgroundColor: '#F3F7FF',
pointRadius: 0,
pointHitRadius: 10,
borderColor: '#14b8ff',
borderJoinStyle: 'miter',
maxBarThickness: 20,
minBarLength: 2
pointBorderColor: '#5597F9',
pointBackgroundColor: '#5597F9',
pointHoverBackgroundColor: '#5597F9',
pointHoverBorderColor: '#5597F9',
pointHoverRadius: 0,
borderColor: '#5597F9',
borderJoinStyle: 'miter'
}]
};
}
@ -139,7 +147,46 @@ export default class ChartAnchor extends Component {
},
layout: {
padding: {
top: 20
top: 0
}
},
hover: {
onHover: function (e) {
e.target.style.cursor = 'pointer';
}
},
tooltips: {
intersect: false,
mode: 'index',
displayColors: false,
backgroundColor: '#15171A',
xPadding: 7,
yPadding: 7,
cornerRadius: 5,
caretSize: 7,
caretPadding: 5,
bodyFontSize: 12.5,
titleFontSize: 12,
titleFontStyle: 'normal',
titleFontColor: 'rgba(255, 255, 255, 0.7)',
titleMarginBottom: 3,
callbacks: {
label: (tooltipItems, data) => {
let valueText = data.datasets[tooltipItems.datasetIndex].data[tooltipItems.index].toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
if (this.chartDisplay === 'total') {
return `Total members: ${valueText}`;
}
if (this.chartDisplay === 'paid') {
return `Paid members: ${valueText}`;
}
if (this.chartDisplay === 'monthly') {
return `Monthly revenue (MRR): ${valueText}`;
}
},
title: (tooltipItems) => {
return moment(tooltipItems[0].xLabel).format(DATE_FORMAT);
}
}
},
scales: {
@ -154,8 +201,10 @@ export default class ChartAnchor extends Component {
maxTicksLimit: 5,
fontColor: '#7C8B9A',
padding: 8,
precision: 0
}
precision: 0,
beginAtZero: false
},
display: true
}],
xAxes: [{
gridLines: {
@ -165,10 +214,9 @@ export default class ChartAnchor extends Component {
},
ticks: {
display: false,
maxTicksLimit: 5,
autoSkip: true,
maxRotation: 0,
minRotation: 0
minRotation: 0,
beginAtZero: false
},
type: 'time',
time: {
@ -183,12 +231,17 @@ export default class ChartAnchor extends Component {
quarter: 'MMM DD',
year: 'MMM DD'
}
}
},
display: true
}]
}
};
}
get chartHeight() {
return 160;
}
calculatePercentage(from, to) {
if (from === 0) {
if (to > 0) {

View File

@ -1,30 +1,41 @@
<h3 {{did-insert this.loadCharts}}>Email</h3>
<div class="gh-dashboard5-insert">
<div class="gh-dashboard5-box">
<div class="gh-dashboard5-number is-solo">
{{format-number this.dataSubscribers.total}}<small><span class="gh-dashboard5-slash">/</span>{{format-number this.dataSubscribers.paid}} paid</small>
</div>
<small class="gh-dashboard5-info">Newsletter subscribers</small>
<div {{did-insert this.loadCharts}} class="gh-dashboard5-insert">
<div class="gh-dashboard5-insert-item">
<Dashboard::v5::parts::ChartMetric
@label="Newsletter subscribers"
@value={{format-number this.dataSubscribers.total}}
@extra="{{format-number this.dataSubscribers.paid}} paid members" />
</div>
<div class="gh-dashboard5-box">
<div class="gh-dashboard5-number is-solo">{{format-number this.dataEmailsSent}}</div>
<small class="gh-dashboard5-info">Sent in the past 30 days</small>
<div class="gh-dashboard5-insert-item">
<Dashboard::v5::parts::ChartMetric
@label="Emails sent over last 30 days"
@value={{format-number this.dataEmailsSent}}
@extra="All members" />
</div>
<div class="gh-dashboard5-box">
{{#if this.loading}}
<div class="gh-dashboard5-loading" style={{html-safe (concat "height: " this.chartHeight "px;")}}/>
{{else}}
<EmberChart
@type={{this.chartType}}
@data={{this.chartData}}
@options={{this.chartOptions}}
@height={{130}} />
{{/if}}
<div>
<div class="gh-dashboard5-number">{{this.currentOpenRate}}%</div>
<small class="gh-dashboard5-info">Open rate</small>
<div class="gh-dashboard5-insert-item">
<Dashboard::v5::parts::ChartMetric
@label="Open rate"
@value="{{this.currentOpenRate}}%" />
<div class="gh-dashboard5-chart">
{{#if this.loading}}
<div class="gh-dashboard5-chart-loading" style={{html-safe (concat "height: " this.chartHeight "px;")}}/>
{{else}}
<div class="gh-dashboard5-chart-ticks">
<span>100%</span>
<span>75%</span>
<span>50%</span>
<span>25%</span>
</div>
<div class="gh-dashboard5-chart-container">
<EmberChart
@type={{this.chartType}}
@data={{this.chartData}}
@options={{this.chartOptions}}
@height={{this.chartHeight}} />
</div>
{{/if}}
</div>
</div>
</div>

View File

@ -55,27 +55,44 @@ export default class ChartEmailOpenRate extends Component {
datasets: [{
data,
fill: false,
backgroundColor: '#14b8ff',
tension: 0.1,
backgroundColor: '#7BA4F3',
cubicInterpolationMode: 'monotone',
pointRadius: 0,
pointHitRadius: 10,
borderColor: '#14b8ff',
borderJoinStyle: 'miter',
maxBarThickness: 15,
minBarLength: 2
barThickness: 10
}]
};
}
get chartOptions() {
return {
responsive: true,
maintainAspectRatio: false,
title: {
display: false
},
legend: {
display: false
},
hover: {
onHover: function (e) {
e.target.style.cursor = 'pointer';
}
},
tooltips: {
intersect: false,
mode: 'index',
displayColors: false,
backgroundColor: '#15171A',
xPadding: 7,
yPadding: 7,
cornerRadius: 5,
caretSize: 7,
caretPadding: 5,
bodyFontSize: 12.5,
titleFontSize: 12,
titleFontStyle: 'normal',
titleFontColor: 'rgba(255, 255, 255, 0.7)',
titleMarginBottom: 3
},
scales: {
yAxes: [{
gridLines: {
@ -110,6 +127,6 @@ export default class ChartEmailOpenRate extends Component {
}
get chartHeight() {
return 175;
return 150;
}
}

View File

@ -1,15 +1,17 @@
<h3 {{did-insert this.loadCharts}}>Engagement</h3>
<article class="gh-dashboard5-box">
<article {{did-insert this.loadCharts}} class="gh-dashboard5-box">
<div class="gh-dashboard5-insert">
<div class="gh-dashboard5-box">
<div class="gh-dashboard5-number is-solo">{{this.data30Days}}<small><span class="gh-dashboard5-slash">/</span>99 members</small></div>
<small class="gh-dashboard5-info">Engaged over 30 days</small>
<div class="gh-dashboard5-insert-item">
<Dashboard::v5::parts::ChartMetric
@label="Engaged over 30 days"
@value={{this.data30Days}}
@extra="99 members" />
</div>
<div class="gh-dashboard5-box">
<div class="gh-dashboard5-number is-solo">{{this.data7Days}}<small><span class="gh-dashboard5-slash">/</span>99 members</small></div>
<small class="gh-dashboard5-info">Engaged over 7 days</small>
<div class="gh-dashboard5-insert-item">
<Dashboard::v5::parts::ChartMetric
@label="Engaged over 7 days"
@value={{this.data7Days}}
@extra="99 members" />
</div>
</div>
</article>

View File

@ -4,13 +4,13 @@ import {inject as service} from '@ember/service';
import {tracked} from '@glimmer/tracking';
const STATUS_OPTIONS = [{
name: 'All members',
name: 'All',
value: 'total'
}, {
name: 'Paid members',
name: 'Paid',
value: 'paid'
}, {
name: 'Free members',
name: 'Free',
value: 'free'
}];

View File

@ -1,23 +0,0 @@
<div class="gh-dashboard5-insert">
<div class="gh-dashboard5-box">
<h4 class="gh-dashboard5-metric">{{gh-pluralize this.totalMembers "Total member" without-count=true}}</h4>
<div class="gh-dashboard5-number">{{format-number this.totalMembers}}</div>
{{#if this.hasTrends}}
<Dashboard::v5::parts::ChartPercentage @percentage={{this.totalMembersTrend}}/>
{{/if}}
</div>
<div class="gh-dashboard5-box">
<h4 class="gh-dashboard5-metric">{{gh-pluralize this.paidMembers "Paid member" without-count=true}}</h4>
<div class="gh-dashboard5-number">{{format-number this.paidMembers}}</div>
{{#if this.hasTrends}}
<Dashboard::v5::parts::ChartPercentage @percentage={{this.paidMembersTrend}}/>
{{/if}}
</div>
<div class="gh-dashboard5-box">
<h4 class="gh-dashboard5-metric">{{gh-pluralize this.freeMembers "Free member" without-count=true}}</h4>
<div class="gh-dashboard5-number">{{format-number this.freeMembers}}</div>
{{#if this.hasTrends}}
<Dashboard::v5::parts::ChartPercentage @percentage={{this.freeMembersTrend}}/>
{{/if}}
</div>
</div>

View File

@ -1,64 +0,0 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
export default class ChartMembersCountInsert extends Component {
@service dashboardStats;
@action
loadCharts() {
this.dashboardStats.loadMemberCountStats();
}
get totalMembers() {
return this.dashboardStats.memberCounts?.total ?? 0;
}
get paidMembers() {
return this.dashboardStats.memberCounts?.paid ?? 0;
}
get freeMembers() {
return this.dashboardStats.memberCounts?.free ?? 0;
}
get hasTrends() {
return this.dashboardStats.memberCounts !== null && this.dashboardStats.memberCountsTrend !== null;
}
get totalMembersTrend() {
return this.calculatePercentage(this.dashboardStats.memberCountsTrend.total, this.dashboardStats.memberCounts.total);
}
get paidMembersTrend() {
return this.calculatePercentage(this.dashboardStats.memberCountsTrend.paid, this.dashboardStats.memberCounts.paid);
}
get freeMembersTrend() {
return this.calculatePercentage(this.dashboardStats.memberCountsTrend.free, this.dashboardStats.memberCounts.free);
}
calculatePercentage(from, to) {
if (from === 0) {
if (to > 0) {
return 100;
}
return 0;
}
const percentage = (to - from) / from * 100;
if (Math.abs(percentage) < 0.05) {
// Round on two decimals
return Math.round(percentage * 100) / 100;
}
if (Math.abs(percentage) < 0.25) {
// Round on one decimal
return Math.round(percentage * 10) / 10;
}
if (Math.abs(percentage) < 1) {
// Round on 0.5
return Math.round(percentage * 2) / 2;
}
return Math.round(percentage);
}
}

View File

@ -1,26 +0,0 @@
<div class="gh-dashboard5-header">
<h3 {{did-insert this.loadCharts}}>Overview</h3>
</div>
<div class="gh-dashboard5-container">
<div class="gh-dashboard5-box">
<h4 class="gh-dashboard5-metric">{{gh-pluralize this.totalMembers "Total member" without-count=true}}</h4>
<div class="gh-dashboard5-number">{{format-number this.totalMembers}}</div>
{{#if this.hasTrends}}
<Dashboard::v5::parts::ChartPercentage @percentage={{this.totalMembersTrend}}/>
{{/if}}
</div>
<div class="gh-dashboard5-box">
<h4 class="gh-dashboard5-metric">{{gh-pluralize this.paidMembers "Paid member" without-count=true}}</h4>
<div class="gh-dashboard5-number">{{format-number this.paidMembers}}</div>
{{#if this.hasTrends}}
<Dashboard::v5::parts::ChartPercentage @percentage={{this.paidMembersTrend}}/>
{{/if}}
</div>
<div class="gh-dashboard5-box">
<h4 class="gh-dashboard5-metric">{{gh-pluralize this.freeMembers "Free member" without-count=true}}</h4>
<div class="gh-dashboard5-number">{{format-number this.freeMembers}}</div>
{{#if this.hasTrends}}
<Dashboard::v5::parts::ChartPercentage @percentage={{this.freeMembersTrend}}/>
{{/if}}
</div>
</div>

View File

@ -1,64 +0,0 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
export default class ChartMembersCounts extends Component {
@service dashboardStats;
@action
loadCharts() {
this.dashboardStats.loadMemberCountStats();
}
get totalMembers() {
return this.dashboardStats.memberCounts?.total ?? 0;
}
get paidMembers() {
return this.dashboardStats.memberCounts?.paid ?? 0;
}
get freeMembers() {
return this.dashboardStats.memberCounts?.free ?? 0;
}
get hasTrends() {
return this.dashboardStats.memberCounts !== null && this.dashboardStats.memberCountsTrend !== null;
}
get totalMembersTrend() {
return this.calculatePercentage(this.dashboardStats.memberCountsTrend.total, this.dashboardStats.memberCounts.total);
}
get paidMembersTrend() {
return this.calculatePercentage(this.dashboardStats.memberCountsTrend.paid, this.dashboardStats.memberCounts.paid);
}
get freeMembersTrend() {
return this.calculatePercentage(this.dashboardStats.memberCountsTrend.free, this.dashboardStats.memberCounts.free);
}
calculatePercentage(from, to) {
if (from === 0) {
if (to > 0) {
return 100;
}
return 0;
}
const percentage = (to - from) / from * 100;
if (Math.abs(percentage) < 0.05) {
// Round on two decimals
return Math.round(percentage * 100) / 100;
}
if (Math.abs(percentage) < 0.25) {
// Round on one decimal
return Math.round(percentage * 10) / 10;
}
if (Math.abs(percentage) < 1) {
// Round on 0.5
return Math.round(percentage * 2) / 2;
}
return Math.round(percentage);
}
}

View File

@ -1,14 +0,0 @@
<div class="gh-dashboard5-number"
{{did-insert this.loadCharts}}
>$32</div>
<small class="gh-dashboard5-info">Monthly revenue (MMR)</small>
{{#if this.loading}}
<div class="gh-dashboard5-loading" style={{html-safe (concat "height: " this.chartHeight "px;")}}/>
{{else}}
<EmberChart
@type={{this.chartType}}
@data={{this.chartData}}
@options={{this.chartOptions}}
@height={{this.chartHeight}} />
{{/if}}

View File

@ -1,107 +0,0 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
export default class ChartMonthlyRevenue extends Component {
@service dashboardStats;
/**
* Call this method when you need to fetch new data from the server.
*/
@action
loadCharts() {
// The dashboard stats service will take care or reusing and limiting API-requests between charts
this.dashboardStats.loadMrrStats();
}
get loading() {
return this.dashboardStats.mrrStats === null;
}
get chartType() {
return 'line';
}
get chartData() {
const stats = this.dashboardStats.filledMrrStats;
const labels = stats.map(stat => stat.date);
const data = stats.map(stat => stat.mrr);
return {
labels: labels,
datasets: [{
data: data,
tension: 0.1,
cubicInterpolationMode: 'monotone',
fill: true,
fillColor: '#F5FBFF',
backgroundColor: '#F5FBFF',
pointRadius: 0,
pointHitRadius: 10,
borderColor: '#14b8ff',
borderJoinStyle: 'miter',
maxBarThickness: 20,
minBarLength: 2
}]
};
}
get chartOptions() {
return {
title: {
display: false
},
legend: {
display: false
},
scales: {
yAxes: [{
gridLines: {
drawTicks: false,
display: false,
drawBorder: false
},
ticks: {
display: false,
maxTicksLimit: 5,
fontColor: '#7C8B9A',
padding: 8,
precision: 0
}
}],
xAxes: [{
gridLines: {
drawTicks: false,
display: false,
drawBorder: false
},
ticks: {
display: false,
maxTicksLimit: 5,
autoSkip: true,
maxRotation: 0,
minRotation: 0
},
type: 'time',
time: {
displayFormats: {
millisecond: 'MMM DD',
second: 'MMM DD',
minute: 'MMM DD',
hour: 'MMM DD',
day: 'MMM DD',
week: 'MMM DD',
month: 'MMM DD',
quarter: 'MMM DD',
year: 'MMM DD'
}
}
}]
}
};
}
get chartHeight() {
return 200;
}
}

View File

@ -1,16 +1,23 @@
<small
class="gh-dashboard5-info is-solo"
<Dashboard::v5::parts::ChartMetric
{{did-insert this.loadCharts}}
>Paid members</small>
@label="Paid member" />
{{#if this.loading}}
<div class="gh-dashboard5-loading" style={{html-safe (concat "height: " this.chartHeight "px;")}}/>
{{else}}
<div class="gh-dashboard5-chart">
<EmberChart
@type={{this.chartType}}
@data={{this.chartData}}
@options={{this.chartOptions}}
@height={{150}} />
</div>
{{/if}}
<div class="gh-dashboard5-chart">
{{#if this.loading}}
<div class="gh-dashboard5-chart-loading" style={{html-safe (concat "height: " this.chartHeight "px;")}}/>
{{else}}
<div class="gh-dashboard5-chart-ticks">
<span>10</span>
<span>5</span>
<span>0</span>
<span>-5</span>
</div>
<div class="gh-dashboard5-chart-container">
<EmberChart
@type={{this.chartType}}
@data={{this.chartData}}
@options={{this.chartOptions}}
@height={{this.chartHeight}} />
</div>
{{/if}}
</div>

View File

@ -1,7 +1,10 @@
import Component from '@glimmer/component';
import moment from 'moment';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
const DATE_FORMAT = 'D MMM YYYY';
export default class ChartPaidMembers extends Component {
@service dashboardStats;
@ -34,38 +37,53 @@ export default class ChartPaidMembers extends Component {
{
data: newData,
fill: true,
borderColor: '#9B90F9',
backgroundColor: '#9B90F9',
tension: 0.1,
barThickness: 10,
minBarLength: 3,
borderWidth: 2,
borderRadius: 5
backgroundColor: '#7BA4F3',
barThickness: 10
},{
data: canceledData,
fill: true,
borderColor: '#E28B9D',
backgroundColor: '#E28B9D',
tension: 0.1,
barThickness: 10,
minBarLength: 3,
borderWidth: 2,
borderRadius: 5
backgroundColor: '#E5E5E5',
barThickness: 10
}]
};
}
get chartOptions() {
return {
animation: {
duration: 0
},
responsive: true,
maintainAspectRatio: false,
title: {
display: false
},
legend: {
display: false
},
hover: {
onHover: function (e) {
e.target.style.cursor = 'pointer';
}
},
tooltips: {
intersect: false,
mode: 'index',
displayColors: false,
backgroundColor: '#15171A',
xPadding: 7,
yPadding: 7,
cornerRadius: 5,
caretSize: 7,
caretPadding: 5,
bodyFontSize: 12.5,
titleFontSize: 12,
titleFontStyle: 'normal',
titleFontColor: 'rgba(255, 255, 255, 0.7)',
titleMarginBottom: 3,
callbacks: {
title: (tooltipItems) => {
return moment(tooltipItems[0].xLabel).format(DATE_FORMAT);
}
}
},
scales: {
yAxes: [{
offset: true,
@ -113,4 +131,8 @@ export default class ChartPaidMembers extends Component {
}
};
}
get chartHeight() {
return 125;
}
}

View File

@ -17,19 +17,20 @@
</div>
{{/if}}
<small
class="gh-dashboard5-info is-solo"
<Dashboard::v5::parts::ChartMetric
{{did-insert this.loadCharts}}
>Paid mix</small>
@label="Paid mix" />
{{#if this.loading}}
<div class="gh-dashboard5-loading" style={{html-safe (concat "height: " this.chartHeight "px;")}}/>
{{else}}
<div class="gh-dashboard5-chart">
<EmberChart
@type={{this.chartType}}
@data={{this.chartData}}
@options={{this.chartOptions}}
@height={{150}} />
</div>
{{/if}}
<div class="gh-dashboard5-chart">
{{#if this.loading}}
<div class="gh-dashboard5-chart-loading" style={{html-safe (concat "height: " this.chartHeight "px;")}}/>
{{else}}
<div class="gh-dashboard5-chart-container">
<EmberChart
@type={{this.chartType}}
@data={{this.chartData}}
@options={{this.chartOptions}}
@height={{this.chartHeight}} />
</div>
{{/if}}
</div>

View File

@ -66,8 +66,7 @@ export default class ChartPaidMix extends Component {
datasets: [{
data: [this.dashboardStats.paidMembersByCadence.monthly, this.dashboardStats.paidMembersByCadence.annual],
fill: false,
backgroundColor: ['#14b8ff'],
tension: 0.1
backgroundColor: ['#7BA4F3']
}]
};
}
@ -80,22 +79,48 @@ export default class ChartPaidMix extends Component {
datasets: [{
data,
fill: false,
backgroundColor: ['#14b8ff'],
borderWidth: 3,
tension: 0.1
backgroundColor: ['#7BA4F3'],
borderWidth: 3
}]
};
}
get chartOptions() {
return {
responsive: true,
maintainAspectRatio: false,
cutoutPercentage: (this.mode === 'cadence' ? 85 : 75),
legend: {
display: false
},
animation: {
duration: 0
},
hover: {
onHover: function (e) {
e.target.style.cursor = 'pointer';
}
},
tooltips: {
intersect: false,
mode: 'index',
displayColors: false,
backgroundColor: '#15171A',
xPadding: 7,
yPadding: 7,
cornerRadius: 5,
caretSize: 7,
caretPadding: 5,
bodyFontSize: 12.5,
titleFontSize: 12,
titleFontStyle: 'normal',
titleFontColor: 'rgba(255, 255, 255, 0.7)',
titleMarginBottom: 3
}
};
}
get chartHeight() {
return 75;
}
}

View File

@ -1,10 +0,0 @@
{{#if this.loading}}
<div {{did-insert this.loadCharts}} class="gh-dashboard5-loading" style={{html-safe (concat "height: " this.chartHeight "px;")}}/>
{{else}}
<Dashboard::V5::ChartMembersCountsInsert />
<EmberChart
@type={{this.chartType}}
@data={{this.chartData}}
@options={{this.chartOptions}}
@height={{this.chartHeight}} />
{{/if}}

View File

@ -1,107 +0,0 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
export default class ChartTotalMembers extends Component {
@service dashboardStats;
/**
* Call this method when you need to fetch new data from the server.
*/
@action
loadCharts() {
// The dashboard stats service will take care or reusing and limiting API-requests between charts
this.dashboardStats.loadMemberCountStats();
}
get loading() {
return this.dashboardStats.memberCountStats === null;
}
get chartType() {
return 'line';
}
get chartData() {
const stats = this.dashboardStats.filledMemberCountStats;
const labels = stats.map(stat => stat.date);
const data = stats.map(stat => stat.paid + stat.free + stat.comped);
return {
labels: labels,
datasets: [{
data: data,
tension: 0.1,
cubicInterpolationMode: 'monotone',
fill: true,
fillColor: '#F5FBFF',
backgroundColor: '#F5FBFF',
pointRadius: 0,
pointHitRadius: 10,
borderColor: '#14b8ff',
// borderJoinStyle: 'miter',
maxBarThickness: 20,
minBarLength: 2
}]
};
}
get chartOptions() {
return {
title: {
display: false
},
legend: {
display: false
},
scales: {
yAxes: [{
gridLines: {
drawTicks: false,
display: false,
drawBorder: false
},
ticks: {
display: false,
maxTicksLimit: 5,
fontColor: '#7C8B9A',
padding: 8,
precision: 0
}
}],
xAxes: [{
gridLines: {
drawTicks: false,
display: false,
drawBorder: false
},
ticks: {
display: false,
maxTicksLimit: 5,
autoSkip: true,
maxRotation: 0,
minRotation: 0
},
type: 'time',
time: {
displayFormats: {
millisecond: 'MMM DD',
second: 'MMM DD',
minute: 'MMM DD',
hour: 'MMM DD',
day: 'MMM DD',
week: 'MMM DD',
month: 'MMM DD',
quarter: 'MMM DD',
year: 'MMM DD'
}
}
}]
}
};
}
get chartHeight() {
return 50;
}
}

View File

@ -1,14 +0,0 @@
<div class="gh-dashboard5-number"
{{did-insert this.loadCharts}}
>$999</div>
<small class="gh-dashboard5-info">Total paid</small>
{{#if this.loading}}
<div class="gh-dashboard5-loading" style={{html-safe (concat "height: " this.chartHeight "px;")}}/>
{{else}}
<EmberChart
@type={{this.chartType}}
@data={{this.chartData}}
@options={{this.chartOptions}}
@height={{this.chartHeight}} />
{{/if}}

View File

@ -1,107 +0,0 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
export default class ChartTotalPaid extends Component {
@service dashboardStats;
/**
* Call this method when you need to fetch new data from the server.
*/
@action
loadCharts() {
// The dashboard stats service will take care or reusing and limiting API-requests between charts
this.dashboardStats.loadMemberCountStats();
}
get loading() {
return this.dashboardStats.memberCountStats === null;
}
get chartType() {
return 'line';
}
get chartData() {
const stats = this.dashboardStats.filledMemberCountStats;
const labels = stats.map(stat => stat.date);
const data = stats.map(stat => stat.paid);
return {
labels,
datasets: [{
data,
tension: 0.1,
cubicInterpolationMode: 'monotone',
fill: true,
fillColor: '#F5FBFF',
backgroundColor: '#F5FBFF',
pointRadius: 0,
pointHitRadius: 10,
borderColor: '#14b8ff',
borderJoinStyle: 'miter',
maxBarThickness: 20,
minBarLength: 2
}]
};
}
get chartOptions() {
return {
title: {
display: false
},
legend: {
display: false
},
scales: {
yAxes: [{
gridLines: {
drawTicks: false,
display: false,
drawBorder: false
},
ticks: {
display: false,
maxTicksLimit: 5,
fontColor: '#7C8B9A',
padding: 8,
precision: 0
}
}],
xAxes: [{
gridLines: {
drawTicks: false,
display: false,
drawBorder: false
},
ticks: {
display: false,
maxTicksLimit: 5,
autoSkip: true,
maxRotation: 0,
minRotation: 0
},
type: 'time',
time: {
displayFormats: {
millisecond: 'MMM DD',
second: 'MMM DD',
minute: 'MMM DD',
hour: 'MMM DD',
day: 'MMM DD',
week: 'MMM DD',
month: 'MMM DD',
quarter: 'MMM DD',
year: 'MMM DD'
}
}
}]
}
};
}
get chartHeight() {
return 50;
}
}

View File

@ -0,0 +1,20 @@
<div class="gh-dashboard5-metric {{if @extra "is-stretch"}}">
<div class="gh-dashboard5-metric-data">
<h5 class="gh-dashboard5-metric-label">
{{@label}}
</h5>
{{#if @value}}
<div class="gh-dashboard5-metric-value {{if @large "is-large"}}">
{{@value}}
{{#if @trends}}
<Dashboard::v5::parts::ChartPercentage @percentage={{@percentage}}/>
{{/if}}
</div>
{{/if}}
</div>
{{#if @extra}}
<div class="gh-dashboard5-metric-extra">
{{@extra}}
</div>
{{/if}}
</div>

View File

@ -1,59 +1,46 @@
{{#if this.shouldDisplay}}
<div class="gh-dashboard-box activity gh-list" data-test-dashboard-member-activity>
<h4 class="gh-dashboard5-metric is-split">Recent activity</h4>
<div class="content">
{{#let (members-event-fetcher filter=(members-event-filter excludeEmailEvents=true) pageSize=5) as |eventsFetcher|}}
{{#if eventsFetcher.isError}}
<p class="error">
There was an error loading events
{{#if eventsFetcher.errorMessage}}
<code>{{eventsFetcher.errorMessage}}</code>
{{/if}}
</p>
{{/if}}
{{#if eventsFetcher.isLoading}}
Loading...
{{else}}
<div class="gh-event-timeline">
{{#if eventsFetcher.data}}
<ul class="gh-dashboard-activity-list">
{{#each eventsFetcher.data as |event|}}
{{#let (parse-member-event event) 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">
{{parsedEvent.action}}
{{parsedEvent.object}}
<span class="highlight">{{parsedEvent.info}}</span>
</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>
{{/if}}
{{/let}}
</div>
<div class="gh-dashboard5-list" data-test-dashboard-member-activity>
<div class="gh-dashboard5-list-header">
<Dashboard::v5::parts::ChartMetric @label="Recent activity" />
</div>
{{/if}}
<div class="gh-dashboard5-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">
<p>There was an error loading events</p>
{{#if eventsFetcher.errorMessage}}
<code>{{eventsFetcher.errorMessage}}</code>
{{/if}}
</div>
{{/if}}
{{#if eventsFetcher.isLoading}}
<div class="gh-dashboard5-list-loading">
<p>Loading...</p>
</div>
{{else}}
{{#if eventsFetcher.data}}
{{#each eventsFetcher.data as |event|}}
{{#let (parse-member-event event) as |parsedEvent|}}
<div class="gh-dashboard5-list-item" data-test-dashboard-member-activity-item>
<LinkTo class="member-details" @route="member" @model="{{parsedEvent.memberId}}">
{{parsedEvent.subject}}
{{parsedEvent.action}}
{{parsedEvent.object}}
{{parsedEvent.info}}
</LinkTo>
{{!-- <span class="gh-dashboard-activity-time">{{moment-from-now parsedEvent.timestamp}}</span> --}}
</div>
{{/let}}
{{/each}}
{{else}}
<div class="gh-dashboard5-list-empty" data-test-no-member-activities>
{{svg-jar "no-data-list"}}
<p>No activity yet.</p>
</div>
{{/if}}
{{/if}}
{{/let}}
</div>
<div class="gh-dashboard5-list-footer">
<LinkTo @route="members-activity" @query={{reset-query-params "members-activity"}}>See all activity →</LinkTo>
</div>
</div>

View File

@ -1,28 +1,22 @@
<ol class="gh-list" {{did-insert this.loadPosts}}>
<li class="gh-list-row header">
<h4 class="gh-dashboard5-metric is-split">Recent posts</h4>
{{!--
<div class="gh-list-header gh-posts-title-header">Title</div>
<div class="gh-list-header gh-posts-sends-header">Sends</div>
<div class="gh-list-header gh-posts-opens-header">Opens</div>
--}}
</li>
{{#each this.posts as |post|}}
<GhPostsListItem
@post={{post}}
@hideAuthor={{true}}
@hideStatusColumn={{true}}
data-test-post-id={{post.id}} />
{{else}}
<li class="no-posts-box">
<div class="no-posts">
<h3>No published posts yet.</h3>
<div class="gh-dashboard5-list" {{did-insert this.loadPosts}}>
<div class="gh-dashboard5-list-header">
<Dashboard::v5::parts::ChartMetric @label="Recent posts" />
</div>
<div class="gh-dashboard5-list-body">
{{#each this.posts as |post|}}
<div class="gh-dashboard5-list-item">
<LinkTo class="gh-dashboard5-list-post permalink" @route="editor.edit" @models={{array post.displayName post.id}}>
{{post.title}}
</LinkTo>
</div>
</li>
{{/each}}
</ol>
<div class="footer">
<LinkTo @route="posts" @query={{reset-query-params "posts"}}>See all posts →</LinkTo>
{{else}}
<div class="gh-dashboard5-list-empty">
{{svg-jar "no-data-list"}}
<p>No published posts yet.</p>
</div>
{{/each}}
</div>
<div class="gh-dashboard5-list-footer">
<LinkTo @route="posts" @query={{reset-query-params "posts"}}>See all posts →</LinkTo>
</div>
</div>

View File

@ -994,8 +994,8 @@ a.gh-dashboard-container {
.prototype-paid-mix-dropdown {
position: absolute;
right: 15px;
top: 25px;
right: 0px;
top: 8px;
}
.prototype-counts {
@ -1028,7 +1028,7 @@ a.gh-dashboard-container {
}
.prototype-box {
border: 1px solid #ebeef0;
border: 1px solid var(--whitegrey);
padding: 28px;
position: relative;
}
@ -1037,71 +1037,17 @@ a.gh-dashboard-container {
font-size: 24px;
}
.gh-dashboard5 {
max-width: 1230px;
margin: 0 auto;
}
.gh-dashboard5-split {
display: grid;
gap: 24px;
grid-template-columns: 1fr 1fr;
}
.gh-dashboard5-split.is-third {
grid-template-columns: 2fr 1fr;
}
.gh-dashboard5-section {
flex: 1;
display: flex;
flex-direction: column;
margin-bottom: 24px;
}
.gh-dashboard5-container {
display: flex;
flex-direction: row;
align-items: stretch;
gap: 0;
}
.gh-dashboard5-box {
flex: 1;
border: 1px solid var(--whitegrey);
padding: 28px;
border-radius: 3px;
display: flex;
flex-direction: column;
align-items: flex-start;
position: relative; /* Temporarily added for absolute positioned prototype-paid-mix-dropdown */
}
.gh-dashboard5-container .gh-dashboard5-box {
border-radius: 0;
border-width: 1px 0 1px 1px;
}
.gh-dashboard5-container .gh-dashboard5-box:first-child {
border-radius: 3px 0 0 3px;
}
.gh-dashboard5-container .gh-dashboard5-box:last-child {
border-radius: 0 3px 3px 0;
border-width: 1px;
}
.gh-dashboard5-number {
display: flex;
align-items: flex-end;
font-size: 3rem;
font-size: 2.4rem;
line-height: 4rem;
font-weight: 600;
font-weight: 700;
color: var(--black);
letter-spacing: -.1px;
line-height: 1;
white-space: nowrap;
margin: 0 0 4px;
margin: 4px 0 2px;
gap: 10px;
}
@ -1123,47 +1069,12 @@ a.gh-dashboard-container {
margin: 0 0.5em 0 0;
}
.gh-dashboard5-metric {
display: flex;
align-items: center;
.gh-dashboard5-info {
font-size: 1.1rem;
text-transform: uppercase;
font-weight: 500;
font-weight: 700;
letter-spacing: .2px;
margin: 0 0 8px;
padding: 0;
color: var(--midgrey);
}
.gh-dashboard5-metric.is-main {
margin-bottom: 24px;
}
.gh-dashboard5-metric.is-split {
display: flex;
flex-direction: column;
align-items: flex-start;
font-size: 1.4rem;
text-transform: none;
margin: 0 0 32px;
}
.gh-dashboard5-metric.is-split small {
font-size: 1.1rem;
text-transform: none;
font-weight: 500;
letter-spacing: .2px;
margin: 4px 0 0 0;
padding: 0;
color: var(--midgrey);
}
.gh-dashboard5-info {
font-size: 1.4rem;
text-transform: none;
font-weight: 500;
letter-spacing: .2px;
margin: 2px 0 0;
margin: 0 0 6px;
padding: 0;
color: var(--midgrey);
}
@ -1172,27 +1083,14 @@ a.gh-dashboard-container {
margin: 0 0 20px;
}
.gh-dashboard5-percentage {
flex: 0;
background: var(--whitegrey-d1);
border-radius: 3px;
font-size: 1.2rem;
line-height: 1;
font-weight: 500;
letter-spacing: 0;
color: var(--midgrey);
padding: 2px 4px;
margin: 5px 0 3px 0;
}
.gh-dashboard5-percentage.is-positive {
background: color-mod(var(--green) a(13%));
color: color-mod(var(--green) l(-5%));
}
.gh-dashboard5-percentage.is-negative {
background: color-mod(var(--yellow) a(20%));
color: color-mod(var(--yellow) l(-8%));
.gh-dashboard5-date {
font-size: 1.1rem;
text-transform: none;
font-weight: 600;
letter-spacing: .2px;
margin: 2px 0 0;
padding: 0;
color: var(--midlightgrey);
}
.gh-dashboard5-header {
@ -1213,93 +1111,6 @@ a.gh-dashboard-container {
margin: 0 -8px 6px 0;
}
.gh-dashboard5-email > h3 {
display: none;
}
.gh-dashboard5-email .gh-dashboard5-container {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
gap: 0;
}
/*
.gh-dashboard5-email .gh-dashboard5-box {
border-width: 1px 0 1px 1px;
border-radius: 0;
}
.gh-dashboard5-email .gh-dashboard5-box:nth-child(1) {
grid-column: 1;
grid-row: 1;
border-radius: 3px 0 0 0;
}
.gh-dashboard5-email .gh-dashboard5-box:nth-child(2) {
grid-column: 1;
grid-row: 2;
border-width: 0 0 1px 1px;
border-radius: 0 0 0 3px;
}
.gh-dashboard5-email .gh-dashboard5-box:nth-child(3) {
grid-column: 2;
grid-row: 1 / span 2;
} */
.gh-dashboard5-growth {
display: flex;
margin-top: -25px;
}
/* .gh-dashboard5-growth .gh-dashboard5-box:nth-child(1) {
grid-column: 1 / span 2;
grid-row: 1;
border-radius: 3px 3px 0 0;
border-width: 1px;
} */
.gh-dashboard5-growth .gh-dashboard5-box:nth-child(1) {
/* grid-column: 1;
grid-row: 1; */
flex: 2;
border-radius: 3px 0 0 0;
border-width: 1px;
padding-top: 32px;
}
.gh-dashboard5-growth .gh-dashboard5-box:nth-child(2) {
/* grid-column: 2;
grid-row: 1; */
flex: 1;
border-radius: 0 3px 0 0;
border-width: 1px 1px 1px 0;
padding-top: 32px;
}
/* .gh-dashboard5-growth .gh-dashboard5-box:nth-child(3) {
grid-column: 3;
grid-row: 2;
border-radius: 0 0 0 3px;
border-width: 0 1px 1px 0;
}
.gh-dashboard5-growth .gh-dashboard5-box:nth-child(4) {
grid-column: 4;
grid-row: 2;
border-radius: 0 0 3px 0;
border-width: 0 1px 1px 0;
} */
.gh-dashboard5-growth {
height: 250px;
}
.gh-dashboard5-growth canvas {
width: 100%;
height: 100%;
}
.gh-dashboard5 .gh-dashboard-box {
margin-bottom: 0;
}
@ -1352,88 +1163,20 @@ a.gh-dashboard-container {
right: 20px;
}
.gh-dashboard5-engagement {
position: relative;
}
.gh-dashboard5-engagement > h3 {
display: none;
}
.gh-dashboard5-engagement > .gh-dashboard5-box {
padding: 0;
align-items: stretch;
}
.gh-dashboard5-engagement .gh-dashboard5-insert {
display: flex;
flex-direction: row;
margin: 20px 28px;
flex: 1;
}
.gh-dashboard5-engagement .gh-dashboard5-insert .gh-dashboard5-box {
border-radius: 0;
border-width: 0 1px 0 0;
padding: 8px 24px 0 0;
}
.gh-dashboard5-engagement .gh-dashboard5-insert .gh-dashboard5-box:first-child {
justify-content: flex-end;
padding-top: 0;
padding-bottom: 24px;
}
.gh-dashboard5-engagement .gh-dashboard5-insert .gh-dashboard5-box:last-child {
border-left-width: 0;
flex-direction: column;
justify-content: flex-end;
padding-top: 24px;
padding-bottom: 0;
}
.gh-dashboard5-split .gh-dashboard5-engagement .gh-dashboard5-insert {
display: flex;
flex-direction: column;
margin: 20px 28px;
flex: 1;
}
.gh-dashboard5-split .gh-dashboard5-engagement .gh-dashboard5-insert .gh-dashboard5-box {
border-radius: 0;
border-width: 0 0 1px;
padding: 8px 24px 24px 0;
}
.gh-dashboard5-split .gh-dashboard5-engagement .gh-dashboard5-insert .gh-dashboard5-box:first-child {
padding-top: 0;
padding-bottom: 24px;
}
.gh-dashboard5-split .gh-dashboard5-engagement .gh-dashboard5-insert .gh-dashboard5-box:last-child {
border-bottom-width: 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: flex-end;
padding-top: 24px;
padding-bottom: 0;
}
.gh-dashboard5-section .prototype-selection {
position: absolute;
top: 18px;
right: 12px;
top: 10px;
right: 0;
}
.gh-dashboard5-section .ember-power-select-selected-item {
color: var(--darkgrey);
text-align: right;
font-size: 1.1rem;
text-transform: uppercase;
font-weight: 500;
font-weight: 600;
letter-spacing: .2px;
margin: 0 0 8px;
padding: 0;
color: var(--midgrey);
white-space: nowrap;
}
.gh-dashboard5 .gh-list-header {
@ -1446,106 +1189,568 @@ a.gh-dashboard-container {
color: rgb(21, 23, 26);
}
.gh-dashboard5-posts .gh-dashboard5-box {
display: flex;
flex-direction: column;
justify-content: space-between;
padding-bottom: 16px;
}
.gh-dashboard5-posts .footer {
padding-top: 12px;
}
.gh-dashboard5-email h3 {
.gh-dashboard5-tooltip {
position: absolute;
bottom: 16px;
right: 16px;
display: none;
}
.gh-dashboard5-email canvas {
border-bottom: 1px solid #ebeef0;
.gh-dashboard5-tooltip.is-show {
display: block;
}
.gh-dashboard5-email > .gh-dashboard5-box {
.gh-dashboard5-stats button {
position: relative;
}
.gh-dashboard5-normal {
visibility: visible;
opacity: 1;
}
.gh-dashboard5-normal.is-fade {
opacity: 0.2;
}
.gh-dashboard5-normal.is-hide {
visibility: hidden;
}
#gh-dashboard5-anchor-selection {
opacity: 1;
}
#gh-dashboard5-anchor-selection.is-hide {
opacity: 0;
}
#gh-dashboard5-anchor-globaldate {
display: none;
position: absolute;
top: 28px;
right: 31px;
color: #394047;
font-size: 1.1rem;
text-transform: uppercase;
font-weight: 500;
letter-spacing: .2px;
margin: 0 0 8px;
padding: 0;
}
#gh-dashboard5-anchor-globaldate.is-show {
display: block;
}
#gh-dashboard5-bar {
display: none;
position: absolute;
top: 0;
left: 100px;
width: 1px;
height: 100%;
background: var(--lightgrey);
transition: left 125ms linear;
}
#gh-dashboard5-bar.is-show {
display: block;
}
#gh-dashboard5 canvas {
z-index: 99;
}
#gh-dashboard5-anchor-tooltip {
position: absolute;
bottom: 14px;
right: 16px;
opacity: 0;
transition: transform 125ms ease-out;
transform: translateY(8px);
}
#gh-dashboard5-anchor-tooltip.is-show {
opacity: 1;
transform: translateY(0);
}
#gh-dashboard5-anchor-tooltip-value {
text-align: right;
font-size: 2rem;
line-height: 4rem;
font-weight: 700;
color: var(--black);
letter-spacing: -.1px;
line-height: 1;
white-space: nowrap;
}
#gh-dashboard5-anchor-tooltip-label {
text-align: right;
font-size: 1.2rem;
text-transform: none;
font-weight: 500;
letter-spacing: .2px;
margin: 2px 0 0;
padding: 0;
color: var(--midgrey);
}
/* ---------------------------------
Dashboard v5 Layout */
.gh-dashboard5 {
max-width: 1230px;
margin: 0 auto;
}
.gh-dashboard5-split {
display: grid;
gap: 24px;
grid-template-columns: 1fr 1fr;
}
.gh-dashboard5-split.is-third {
grid-template-columns: 1fr 3fr;
}
.gh-dashboard5-section {
flex: 1;
display: flex;
flex-direction: column;
margin-bottom: 24px;
}
.gh-dashboard5-container {
display: flex;
flex-direction: row;
align-items: stretch;
gap: 0;
}
.gh-dashboard5-box {
flex: 1;
border: 1px solid var(--whitegrey);
padding: 16px;
border-radius: 3px;
display: flex;
flex-direction: column;
position: relative; /* Temporarily added for absolute positioned prototype-paid-mix-dropdown */
align-items: stretch;
}
.gh-dashboard5-container .gh-dashboard5-box {
border-radius: 0;
border-width: 1px 0 1px 1px;
}
.gh-dashboard5-container .gh-dashboard5-box:first-child {
border-radius: 3px 0 0 3px;
}
.gh-dashboard5-container .gh-dashboard5-box:last-child {
border-radius: 0 3px 3px 0;
border-width: 1px;
}
/* ---------------------------------
Dashboard v5 Chart */
.gh-dashboard5-chart {
display: flex;
flex-direction: row;
flex: 1;
}
.gh-dashboard5-chart-loading {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.gh-dashboard5-chart-ticks {
flex: none;
padding: 8px 24px 16px 16px;
font-size: 1.2rem;
text-transform: none;
font-weight: 500;
letter-spacing: .2px;
display: flex;
flex-direction: column;
justify-content: space-between;
font-size: 1.1rem;
letter-spacing: .2px;
color: var(--midgrey);
line-height: 1em;
}
.gh-dashboard5-chart-container {
flex: 1;
position: relative;
width: 0%; /* hack for ChartJS responsive resizing */
height: 100%;
}
/* ---------------------------------
Dashboard v5 Percentage */
.gh-dashboard5-percentage {
flex: 0;
background: var(--whitegrey-d1);
border-radius: 3px;
font-size: 1.2rem;
line-height: 1;
font-weight: 500;
letter-spacing: 0;
color: var(--midgrey);
padding: 2px 4px;
margin: 5px 0 3px 0;
}
.gh-dashboard5-percentage.is-positive {
background: color-mod(var(--green) a(13%));
color: color-mod(var(--green) l(-5%));
}
.gh-dashboard5-percentage.is-negative {
background: color-mod(var(--yellow) a(20%));
color: color-mod(var(--yellow) l(-8%));
}
/* ---------------------------------
Dashboard v5 Metric */
.gh-dashboard5-metric {
display: flex;
flex-direction: column;
}
.gh-dashboard5-metric.is-stretch {
flex: 1;
justify-content: space-between;
}
.gh-dashboard5-metric-data {
display: flex;
flex-direction: column;
}
.gh-dashboard5-metric-label {
align-items: center;
font-size: 1.1rem;
text-transform: uppercase;
font-weight: 600;
letter-spacing: .2px;
line-height: 1em;
margin: 4px 0 8px;
padding: 0;
color: var(--middarkgrey);
white-space: nowrap;
}
.gh-dashboard5-metric-value {
display: flex;
align-items: flex-end;
font-size: 2.2rem;
line-height: 4rem;
font-weight: 700;
color: var(--black);
letter-spacing: -.1px;
line-height: 1em;
white-space: nowrap;
margin: 4px 0 2px;
gap: 10px;
}
.gh-dashboard5-metric-value.is-large {
font-size: 2.8rem;
}
.gh-dashboard5-metric-extra {
font-size: 1.2rem;
text-transform: none;
font-weight: 500;
letter-spacing: .2px;
font-size: 1.1rem;
letter-spacing: .2px;
color: var(--midlightgrey);
line-height: 1em;
}
/* ---------------------------------
Dashboard v5 List */
.gh-dashboard5-list {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.gh-dashboard5-list-header {
padding: 0 0 8px;
}
.gh-dashboard5-list-body {
flex: 1;
display: flex;
flex-direction: column;
padding: 0 0 8px;
}
.gh-dashboard5-list-item {
padding: 8px 0;
}
.gh-dashboard5-list-item a {
font-weight: 500;
color: var(--black);
}
.gh-dashboard5-list-footer {
border-top: 1px solid var(--whitegrey);
padding: 12px 0 0;
}
/* ---------------------------------
Dashboard v5 Section Anchor */
.gh-dashboard5-anchor {
position: relative;
}
.gh-dashboard5-anchor .gh-dashboard5-box {
padding: 0;
align-items: stretch;
}
.gh-dashboard5-email .gh-dashboard5-insert {
margin: 20px 28px;
.gh-dashboard5-anchor .gh-dashboard5-stats {
display: flex;
flex-direction: row;
width: 55%;
padding: 16px;
margin: 0 0 8px;
}
.gh-dashboard5-anchor .gh-dashboard5-stats-button {
cursor: pointer;
position: relative;
flex: 1;
padding: 0 32px 0 0;
margin: 0;
text-align: left;
}
.gh-dashboard5-anchor .gh-dashboard5-stats-highlight {
width: 1px;
height: 3px;
border-radius: 5px;
background: var(--whitegrey);
margin: 8px 0 0;
background: #5B98F2;
opacity: 0;
transition: width 175ms ease-out, opacity 125ms linear;
}
.gh-dashboard5-anchor .gh-dashboard5-stats-button.is-selected .gh-dashboard5-stats-highlight {
width: 25px;
opacity: 1;
}
.gh-dashboard5-anchor canvas {
border-left: 1px solid var(--whitegrey);
}
/* ---------------------------------
Dashboard v5 Section Growth */
.gh-dashboard5-growth {
display: flex;
margin-top: -25px;
}
.gh-dashboard5-growth .gh-dashboard5-box:nth-child(1) {
flex: 2;
border-radius: 3px 0 0 0;
border-width: 1px;
padding-top: 16px;
}
.gh-dashboard5-growth .gh-dashboard5-box:nth-child(2) {
flex: 1;
border-radius: 0 3px 0 0;
border-width: 1px 1px 1px 0;
padding-top: 16px;
}
.gh-dashboard5-growth .gh-dashboard5-chart-ticks {
padding: 8px 24px 16px 0;
width: 55px;
}
.gh-dashboard5-growth .gh-dashboard5-metric {
margin: 0 0 20px;
}
.gh-dashboard5-growth canvas {
border-left: 1px solid var(--whitegrey);
}
/* ---------------------------------
Dashboard v5 Section Email */
.gh-dashboard5-email canvas {
border-bottom: 1px solid var(--whitegrey);
}
.gh-dashboard5-email .gh-dashboard5-container {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
gap: 0;
}
.gh-dashboard5-email .gh-dashboard5-insert .gh-dashboard5-box:nth-child(1) {
.gh-dashboard5-email .gh-dashboard5-insert {
display: grid;
grid-template-columns: 1fr 2fr;
grid-template-rows: 1fr 1fr;
gap: 0;
}
.gh-dashboard5-email .gh-dashboard5-insert-item {
display: flex;
flex-direction: column;
justify-content: flex-start;
}
.gh-dashboard5-email .gh-dashboard5-insert-item:nth-child(1) {
/* --- Newsletter Subscribers */
grid-column: 1;
grid-row: 1;
border-radius: 0;
padding: 4px 16px 16px 0;
border: 1px solid var(--whitegrey);
border-width: 0 1px 1px 0;
padding: 8px 24px 24px 0;
}
.gh-dashboard5-email .gh-dashboard5-insert .gh-dashboard5-box:nth-child(2) {
.gh-dashboard5-email .gh-dashboard5-insert-item:nth-child(2) {
/* --- Emails Sent */
grid-column: 1;
grid-row: 2;
border-radius: 0;
border-width: 0 1px 0 0;
padding: 20px 24px 0 0;
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: 16px 16px 0 0;
border-right: 1px solid var(--whitegrey);
}
.gh-dashboard5-email .gh-dashboard5-insert .gh-dashboard5-box:nth-child(3) {
.gh-dashboard5-email .gh-dashboard5-insert-item:nth-child(3) {
/* --- Open Rate */
grid-column: 2;
grid-row: 1 / span 2;
border-radius: 0;
border-width: 0;
padding: 8px 0 0 28px;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: stretch;
padding: 4px 0 0 16px;
}
.gh-dashboard5-anchor {
.gh-dashboard5-email .gh-dashboard5-chart-ticks {
padding: 28px 16px 0 0;
}
/* ---------------------------------
Dashboard v5 Section Engagement */
.gh-dashboard5-engagement {
position: relative;
}
.gh-dashboard5-anchor > .gh-dashboard5-box {
padding: 0;
.gh-dashboard5-engagement .prototype-selection {
top: 12px;
}
.gh-dashboard5-stats {
.gh-dashboard5-engagement .gh-dashboard5-insert {
display: flex;
flex-direction: row;
justify-content: space-evenly;
width: 75%;
padding: 28px;
}
.gh-dashboard5-stats > button {
flex: 1;
border-left: 2px solid #D6EDF9;
padding: 2px 0 0 12px;
margin: 0 8px 0 0;
transition: all 100ms ease-out;
cursor: pointer;
display: block;
text-align: left;
padding-left: 13px;
}
.gh-dashboard5-stats > button.is-selected {
border-color: #14b8ff;
border-width: 3px;
padding-left: 12px;
}
.gh-dashboard5-chart {
.gh-dashboard5-engagement .gh-dashboard5-insert-item {
flex: 1;
display: flex;
flex-direction: row;
justify-content: stretch;
width: 100%;
height: 200px;
justify-content: flex-end;
min-height: 80px;
}
.gh-dashboard5-engagement .gh-dashboard5-insert-item:nth-child(1) {
/* --- Engaged Over 30 Days */
padding: 4px 16px 0 0;
border-right: 1px solid var(--whitegrey);
}
.gh-dashboard5-engagement .gh-dashboard5-insert-item:nth-child(2) {
/* --- Engaged Over 7 Days */
padding: 4px 16px 0 16px;
}
.gh-dashboard5-split .gh-dashboard5-engagement .gh-dashboard5-insert {
flex-direction: column;
}
.gh-dashboard5-split .gh-dashboard5-engagement .gh-dashboard5-insert-item {
border-radius: 0;
border-width: 0 0 1px;
padding: 8px 16px 16px 0;
}
.gh-dashboard5-split .gh-dashboard5-engagement .gh-dashboard5-insert-item:nth-child(1) {
/* --- Engaged Over 30 Days */
padding: 4px 16px 16px 0;
border-bottom: 1px solid var(--whitegrey);
}
.gh-dashboard5-split .gh-dashboard5-engagement .gh-dashboard5-insert-item:nth-child(2) {
/* --- Engaged Over 7 Days */
flex: 1;
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: 16px 16px 0 0;
}
/* ---------------------------------
Dashboard v5 Section Posts */
.gh-dashboard5-posts .gh-dashboard5-box {
padding: 16px;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: stretch;
}
/* ---------------------------------
Dashboard v5 Section Activity */
.gh-dashboard5-activity .gh-dashboard5-box {
padding: 16px;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: stretch;
}