mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-28 22:43:30 +03:00
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:
parent
9b8875c4be
commit
560d862d91
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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'
|
||||
}];
|
||||
|
||||
|
@ -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>
|
@ -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);
|
||||
}
|
||||
}
|
@ -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>
|
@ -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);
|
||||
}
|
||||
}
|
@ -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}}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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}}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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}}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user