mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-03-04 02:01:58 -05:00
Added animation to the analytics numbers when refreshed (#20842)
ref DES-709 - when refresh button is clicked, the numbers in the analytics will be animated if changed - for the animation to be performant, added a new dependency "animejs" - to minimize the flash and layout shift, the analytics data is kept as it is while loading - once finished loading, it will be replaced with the new data
This commit is contained in:
parent
1d17600f5d
commit
1afe96ae34
8 changed files with 365 additions and 126 deletions
|
@ -41,8 +41,10 @@
|
|||
@buttonText="Refresh"
|
||||
@task={{this.fetchPostTask}}
|
||||
@showIcon={{true}}
|
||||
@idleIcon="reload"
|
||||
@successText="Refreshed"
|
||||
@class="gh-btn gh-btn-icon refresh" />
|
||||
@class="gh-btn gh-btn-icon refresh"
|
||||
@successClass="gh-btn gh-btn-icon refresh" />
|
||||
{{/if}}
|
||||
{{#unless this.post.emailOnly}}
|
||||
<button type="button" class="gh-post-list-cta share" {{on "click" this.togglePublishFlowModal}}>
|
||||
|
@ -99,7 +101,10 @@
|
|||
<Tabs::Tabs class="gh-tabs-analytics {{if (eq this.post.hasBeenEmailed null) "no-tabs"}}" @forceRender={{true}} as |tabs|>
|
||||
{{#if this.post.hasBeenEmailed}}
|
||||
<tabs.tab>
|
||||
<h3>{{svg-jar "analytics-tab-sent-large"}}{{format-number this.post.email.emailCount}}</h3>
|
||||
<h3>
|
||||
{{svg-jar "analytics-tab-sent-large"}}
|
||||
<span class="animated-number sent" {{did-update this.applyClasses this.post.email.emailCount}}>{{split-number this.post.email.emailCount this.previousSentCount}}</span>
|
||||
</h3>
|
||||
<p>{{svg-jar "analytics-tab-sent"}}<span class="analytics-tab-label">Sent</span></p>
|
||||
</tabs.tab>
|
||||
|
||||
|
@ -113,7 +118,10 @@
|
|||
|
||||
{{#if this.post.showEmailOpenAnalytics }}
|
||||
<tabs.tab>
|
||||
<h3>{{svg-jar "analytics-tab-opened-large"}}{{format-number this.post.email.openedCount}}</h3>
|
||||
<h3>
|
||||
{{svg-jar "analytics-tab-opened-large"}}
|
||||
<span class="animated-number opened" {{did-update this.applyClasses this.post.email.openedCount}}>{{split-number this.post.email.openedCount this.previousOpenedCount}}</span>
|
||||
</h3>
|
||||
<p>{{svg-jar "analytics-tab-opened"}}<span class="analytics-tab-label">Opened<span class="analytics-tab-percentage"> — {{this.post.email.openRate}}%</span></span></p>
|
||||
</tabs.tab>
|
||||
|
||||
|
@ -128,7 +136,10 @@
|
|||
|
||||
{{#if this.post.showEmailClickAnalytics }}
|
||||
<tabs.tab>
|
||||
<h3>{{svg-jar "analytics-tab-clicked-large"}}{{format-number this.post.count.clicks}}</h3>
|
||||
<h3>
|
||||
{{svg-jar "analytics-tab-clicked-large"}}
|
||||
<span class="animated-number clicked" {{did-update this.applyClasses this.post.count.clicks}}>{{split-number this.post.count.clicks this.previousClickedCount}}</span>
|
||||
</h3>
|
||||
<p>{{svg-jar "analytics-tab-clicked"}}<span class="analytics-tab-label">Clicked<span class="analytics-tab-percentage"> — {{this.post.clickRate}}%</span></span></p>
|
||||
</tabs.tab>
|
||||
|
||||
|
@ -143,7 +154,10 @@
|
|||
|
||||
{{#if this.post.isFeedbackEnabledForEmail }}
|
||||
<tabs.tab>
|
||||
<h3>{{svg-jar "analytics-tab-feedback-large"}}{{format-number this.totalFeedback}}</h3>
|
||||
<h3>
|
||||
{{svg-jar "analytics-tab-feedback-large"}}
|
||||
<span class="animated-number feedback" {{did-update this.applyClasses this.totalFeedback}}>{{split-number this.totalFeedback this.previousFeedbackCount}}</span>
|
||||
</h3>
|
||||
<p>{{svg-jar "analytics-tab-feedback"}}<span class="analytics-tab-label">Feedback<span class="analytics-tab-percentage"> — {{this.post.sentiment}}%</span></span></p>
|
||||
</tabs.tab>
|
||||
|
||||
|
@ -162,7 +176,7 @@
|
|||
<h3>
|
||||
{{svg-jar "analytics-tab-conversions-large"}}
|
||||
{{#if this.post.hasBeenEmailed}}
|
||||
{{format-number this.post.count.conversions}}
|
||||
<span class="animated-number conversions" {{did-update this.applyClasses this.post.count.conversions}}>{{split-number this.post.count.conversions this.previousConversionsCount}}</span>
|
||||
{{else}}
|
||||
Conversions
|
||||
{{/if}}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import Component from '@glimmer/component';
|
||||
import DeletePostModal from '../modals/delete-post';
|
||||
import PostSuccessModal from '../modal-post-success';
|
||||
import anime from 'animejs/lib/anime.es.js';
|
||||
import {action} from '@ember/object';
|
||||
import {didCancel, task} from 'ember-concurrency';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
@ -39,6 +40,12 @@ export default class Analytics extends Component {
|
|||
@tracked _post = null;
|
||||
@tracked postCount = null;
|
||||
@tracked showPostCount = false;
|
||||
@tracked shouldAnimate = false;
|
||||
@tracked previousSentCount = this.post.email.emailCount;
|
||||
@tracked previousOpenedCount = this.post.email.openedCount;
|
||||
@tracked previousClickedCount = this.post.count.clicks;
|
||||
@tracked previousFeedbackCount = this.totalFeedback;
|
||||
@tracked previousConversionsCount = this.post.count.conversions;
|
||||
displayOptions = DISPLAY_OPTIONS;
|
||||
|
||||
constructor() {
|
||||
|
@ -367,19 +374,61 @@ export default class Analytics extends Component {
|
|||
|
||||
@task
|
||||
*fetchPostTask() {
|
||||
const result = yield this.store.query('post', {filter: `id:${this.post.id}`, include: 'count.clicks,count.conversions,count.paid_conversions,count.positive_feedback,count.negative_feedback', limit: 1});
|
||||
const currentSentCount = this.post.email.emailCount;
|
||||
const currentOpenedCount = this.post.email.openedCount;
|
||||
const currentClickedCount = this.post.count.clicks;
|
||||
const currentFeedbackCount = this.totalFeedback;
|
||||
const currentConversionsCount = this.post.count.conversions;
|
||||
|
||||
this.shouldAnimate = true;
|
||||
|
||||
const result = yield this.store.query('post', {filter: `id:${this.post.id}`, include: 'email,count.clicks,count.conversions,count.positive_feedback,count.negative_feedback', limit: 1});
|
||||
this.post = result.toArray()[0];
|
||||
|
||||
if (this.post.email) {
|
||||
this.notifications.showNotification('Post analytics refreshing', {
|
||||
description: 'It can take up to five minutes for all data to show.',
|
||||
type: 'success'
|
||||
});
|
||||
}
|
||||
this.previousSentCount = currentSentCount;
|
||||
this.previousOpenedCount = currentOpenedCount;
|
||||
this.previousClickedCount = currentClickedCount;
|
||||
this.previousFeedbackCount = currentFeedbackCount;
|
||||
this.previousConversionsCount = currentConversionsCount;
|
||||
|
||||
yield this.fetchLinks();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@action
|
||||
applyClasses(element) {
|
||||
if (!this.shouldAnimate ||
|
||||
(element.classList.contains('sent') && this.post.email.emailCount === this.previousSentCount) ||
|
||||
(element.classList.contains('opened') && this.post.email.openedCount === this.previousOpenedCount) ||
|
||||
(element.classList.contains('clicked') && this.post.count.clicks === this.previousClickedCount) ||
|
||||
(element.classList.contains('feedback') && this.totalFeedback === this.previousFeedbackCount) ||
|
||||
(element.classList.contains('conversions') && this.post.count.conversions === this.previousConversionsCount)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
anime({
|
||||
targets: `${Array.from(element.classList).map(className => `.${className}`).join('')} .new-number span`,
|
||||
translateY: [10,0],
|
||||
// translateZ: 0,
|
||||
opacity: [0,1],
|
||||
easing: 'easeOutElastic',
|
||||
elasticity: 650,
|
||||
duration: 1000,
|
||||
delay: (el, i) => 100 + 30 * i
|
||||
});
|
||||
|
||||
anime({
|
||||
targets: `${Array.from(element.classList).map(className => `.${className}`).join('')} .old-number span`,
|
||||
translateY: [0,-10],
|
||||
opacity: [1,0],
|
||||
easing: 'easeOutExpo',
|
||||
duration: 400,
|
||||
delay: (el, i) => 100 + 10 * i
|
||||
});
|
||||
}
|
||||
|
||||
get showLinks() {
|
||||
return this.post.showEmailClickAnalytics;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
<div class="gh-post-activity-feed">
|
||||
{{#let (activity-feed-fetcher filter=(members-event-filter post=@post.id includeEvents=this.getEventTypes) pageSize=this.pageSize) as |eventsFetcher|}}
|
||||
{{compute (fn this.saveEvents eventsFetcher)}}
|
||||
|
||||
{{#if eventsFetcher.isError}}
|
||||
<div class="gh-dashboard-list-body">
|
||||
<div class="gh-dashboard-list-error">
|
||||
|
@ -11,129 +13,232 @@
|
|||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if eventsFetcher.data}}
|
||||
<div class="gh-dashboard-list-body gh-dashboard-list-cols-{{this.eventType}}">
|
||||
{{#each eventsFetcher.data as |event|}}
|
||||
{{#let (parse-member-event event) as |parsedEvent|}}
|
||||
<div class="gh-dashboard-list-item">
|
||||
<div class="gh-dashboard-list-item-sub">
|
||||
{{#if parsedEvent.member}}
|
||||
<GhMemberAvatar @member={{parsedEvent.member}} @containerClass="w6 h6 mr3 flex-shrink-0" />
|
||||
<LinkTo class="gh-dashboard-list-text" @route="member" @model="{{parsedEvent.memberId}}" @query={{hash postAnalytics=@post.id}}>{{parsedEvent.subject}}</LinkTo>
|
||||
{{else}}
|
||||
{{#if parsedEvent.email}}
|
||||
<GhMemberAvatar @name={{parsedEvent.subject}} @containerClass="w6 h6 mr3 flex-shrink-0" />
|
||||
<a class="gh-dashboard-list-text" href="mailto:{{parsedEvent.email}}" target="_blank" rel="noopener noreferrer" title="{{parsedEvent.email}}">{{parsedEvent.subject}}</a>
|
||||
{{else}}
|
||||
<GhMemberAvatar @name={{parsedEvent.subject}} @containerClass="w6 h6 mr3 flex-shrink-0" />
|
||||
<span class="gh-dashboard-list-text">{{parsedEvent.subject}}</span>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="gh-dashboard-list-item-sub">
|
||||
{{svg-jar parsedEvent.icon }}
|
||||
<span class="gh-dashboard-list-subtext">
|
||||
<span class="gh-members-activity-description">
|
||||
<span class="gh-members-activity-event-text">{{capitalize-first-letter parsedEvent.action}}</span>
|
||||
{{#if parsedEvent.info}}
|
||||
<span class="highlight"> ({{parsedEvent.info}})</span>
|
||||
{{/if}}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
{{#if (eq this.eventType "conversion")}}
|
||||
{{#if eventsFetcher.isLoading}}
|
||||
{{#if this.savedEventsFetcher.data}}
|
||||
<div class="gh-dashboard-list-body gh-dashboard-list-cols-{{this.eventType}}">
|
||||
{{#each this.savedEventsFetcher.data as |event|}}
|
||||
{{#let (parse-member-event event) as |parsedEvent|}}
|
||||
<div class="gh-dashboard-list-item">
|
||||
<div class="gh-dashboard-list-item-sub">
|
||||
{{#if parsedEvent.source}}
|
||||
<span title="Source" class="gh-members-activity-description">{{svg-jar "event-extras-source"}}<span class="gh-members-activity-event-text">{{parsedEvent.source.name}}</span></span>
|
||||
{{#if parsedEvent.member}}
|
||||
<GhMemberAvatar @member={{parsedEvent.member}} @containerClass="w6 h6 mr3 flex-shrink-0" />
|
||||
<LinkTo class="gh-dashboard-list-text" @route="member" @model="{{parsedEvent.memberId}}" @query={{hash postAnalytics=@post.id}}>{{parsedEvent.subject}}</LinkTo>
|
||||
{{else}}
|
||||
<span class="midlightgrey">—</span>
|
||||
{{#if parsedEvent.email}}
|
||||
<GhMemberAvatar @name={{parsedEvent.subject}} @containerClass="w6 h6 mr3 flex-shrink-0" />
|
||||
<a class="gh-dashboard-list-text" href="mailto:{{parsedEvent.email}}" target="_blank" rel="noopener noreferrer" title="{{parsedEvent.email}}">{{parsedEvent.subject}}</a>
|
||||
{{else}}
|
||||
<GhMemberAvatar @name={{parsedEvent.subject}} @containerClass="w6 h6 mr3 flex-shrink-0" />
|
||||
<span class="gh-dashboard-list-text">{{parsedEvent.subject}}</span>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="gh-dashboard-list-item-sub">
|
||||
{{svg-jar parsedEvent.icon }}
|
||||
<span class="gh-dashboard-list-subtext">
|
||||
<span class="gh-members-activity-description">
|
||||
<span class="gh-members-activity-event-text">{{capitalize-first-letter parsedEvent.action}}</span>
|
||||
{{#if parsedEvent.info}}
|
||||
<span class="highlight"> ({{parsedEvent.info}})</span>
|
||||
{{/if}}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
{{#if (eq this.eventType "conversion")}}
|
||||
<div class="gh-dashboard-list-item-sub">
|
||||
{{#if parsedEvent.source}}
|
||||
<span title="Source" class="gh-members-activity-description">{{svg-jar "event-extras-source"}}<span class="gh-members-activity-event-text">{{parsedEvent.source.name}}</span></span>
|
||||
{{else}}
|
||||
<span class="midlightgrey">—</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="gh-dashboard-list-item-sub">
|
||||
<span class="gh-dashboard-list-subtext">{{moment-from-now parsedEvent.timestamp}}</span>
|
||||
</div>
|
||||
</div>
|
||||
{{/let}}
|
||||
{{/each}}
|
||||
|
||||
{{#if (compute (fn this.areStubsNeeded eventsFetcher))}}
|
||||
{{#let (compute (fn this.getAmountOfStubs eventsFetcher)) as |stubs|}}
|
||||
{{#each stubs}}
|
||||
<div class="gh-dashboard-list-item gh-dashboard-list-item-stub"></div>
|
||||
{{/each}}
|
||||
{{/let}}
|
||||
{{/if}}
|
||||
|
||||
<div class="gh-post-activity-feed-footer">
|
||||
<Posts::PostActivityFeed::FooterLinks
|
||||
@eventType={{this.eventType}}
|
||||
@post={{@post}}
|
||||
/>
|
||||
|
||||
<div class="gh-post-activity-feed-pagination">
|
||||
{{#if (compute (fn this.isPaginationNotNeeded this.savedEventsFetcher))}}
|
||||
Showing {{this.savedEventsFetcher.totalEvents}} in total
|
||||
{{else}}
|
||||
Showing {{this.savedEventsFetcher.previousEvents}}-{{this.savedEventsFetcher.shownEvents}} of {{this.savedEventsFetcher.totalEvents}}
|
||||
|
||||
<div class="gh-post-activity-feed-pagination-group">
|
||||
<button
|
||||
class="gh-post-activity-feed-pagination-button gh-post-activity-feed-prev-button"
|
||||
type="button"
|
||||
title="Previous page"
|
||||
disabled={{compute (fn this.isPreviousButtonDisabled this.savedEventsFetcher)}}
|
||||
{{on "click" this.savedEventsFetcher.loadPreviousPage}}
|
||||
>
|
||||
{{svg-jar "arrow-left-pagination"}}
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="gh-post-activity-feed-pagination-button gh-post-activity-feed-next-button"
|
||||
type="button"
|
||||
title="Next page"
|
||||
disabled={{compute (fn this.isNextButtonDisabled this.savedEventsFetcher)}}
|
||||
{{on "click" this.savedEventsFetcher.loadNextPage}}
|
||||
>
|
||||
{{svg-jar "arrow-right-pagination"}}
|
||||
</button>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="gh-dashboard-list-item-sub">
|
||||
<span class="gh-dashboard-list-subtext">{{moment-from-now parsedEvent.timestamp}}</span>
|
||||
</div>
|
||||
</div>
|
||||
{{/let}}
|
||||
{{/each}}
|
||||
|
||||
{{#if (compute (fn this.areStubsNeeded eventsFetcher))}}
|
||||
{{#let (compute (fn this.getAmountOfStubs eventsFetcher)) as |stubs|}}
|
||||
{{#each stubs}}
|
||||
<div class="gh-dashboard-list-item gh-dashboard-list-item-stub"></div>
|
||||
{{/each}}
|
||||
{{/let}}
|
||||
{{/if}}
|
||||
|
||||
<div class="gh-post-activity-feed-footer">
|
||||
<Posts::PostActivityFeed::FooterLinks
|
||||
@eventType={{this.eventType}}
|
||||
@post={{@post}}
|
||||
/>
|
||||
|
||||
<div class="gh-post-activity-feed-pagination">
|
||||
{{#if (compute (fn this.isPaginationNotNeeded eventsFetcher))}}
|
||||
Showing {{eventsFetcher.totalEvents}} in total
|
||||
{{else}}
|
||||
Showing {{eventsFetcher.previousEvents}}-{{eventsFetcher.shownEvents}} of {{eventsFetcher.totalEvents}}
|
||||
|
||||
<div class="gh-post-activity-feed-pagination-group">
|
||||
<button
|
||||
class="gh-post-activity-feed-pagination-button gh-post-activity-feed-prev-button"
|
||||
type="button"
|
||||
title="Previous page"
|
||||
disabled={{compute (fn this.isPreviousButtonDisabled eventsFetcher)}}
|
||||
{{on "click" eventsFetcher.loadPreviousPage}}
|
||||
>
|
||||
{{svg-jar "arrow-left-pagination"}}
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="gh-post-activity-feed-pagination-button gh-post-activity-feed-next-button"
|
||||
type="button"
|
||||
title="Next page"
|
||||
disabled={{compute (fn this.isNextButtonDisabled eventsFetcher)}}
|
||||
{{on "click" eventsFetcher.loadNextPage}}
|
||||
>
|
||||
{{svg-jar "arrow-right-pagination"}}
|
||||
</button>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if (eq @eventType 'feedback')}}
|
||||
<Posts::FeedbackEventsChart @data={{@data}} />
|
||||
{{else}}
|
||||
<div class="gh-post-activity-feed-empty">
|
||||
<div class="gh-post-analytics-loading">
|
||||
<div class="gh-loading-spinner-outer">
|
||||
<div class="gh-loading-spinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<div class="gh-dashboard-list-body">
|
||||
<div class="gh-post-activity-feed-empty">
|
||||
<div class="attribution-list-empty">
|
||||
{{#if (eq this.eventType "sent")}}
|
||||
{{svg-jar "empty-sent"}}
|
||||
<h4>No members have received your email yet</h4>
|
||||
<p>Once someone receives your email, you'll be able to see the member activity here.</p>
|
||||
{{else if (eq this.eventType "opened")}}
|
||||
{{svg-jar "empty-opened"}}
|
||||
<h4>No members have opened your newsletter</h4>
|
||||
<p>Once someone opens, you'll see them listed here.</p>
|
||||
{{else if (eq this.eventType "clicked")}}
|
||||
{{svg-jar "empty-clicked"}}
|
||||
<h4>No links have been clicked in your newsletter</h4>
|
||||
<p>Once a member clicks a link, you'll see them listed here.</p>
|
||||
{{else if (eq this.eventType "feedback")}}
|
||||
{{svg-jar "empty-feedback"}}
|
||||
<h4>No members have given feedback yet</h4>
|
||||
<p>When someone does, you'll see their response here.</p>
|
||||
{{else if (eq this.eventType "conversion")}}
|
||||
{{svg-jar "empty-conversion"}}
|
||||
<h4>No members have signed up on this post</h4>
|
||||
<p>When someone new signs up, you'll see them here.</p>
|
||||
{{/if}}
|
||||
{{#if eventsFetcher.data}}
|
||||
<div class="gh-dashboard-list-body gh-dashboard-list-cols-{{this.eventType}}">
|
||||
{{#each eventsFetcher.data as |event|}}
|
||||
{{#let (parse-member-event event) as |parsedEvent|}}
|
||||
<div class="gh-dashboard-list-item">
|
||||
<div class="gh-dashboard-list-item-sub">
|
||||
{{#if parsedEvent.member}}
|
||||
<GhMemberAvatar @member={{parsedEvent.member}} @containerClass="w6 h6 mr3 flex-shrink-0" />
|
||||
<LinkTo class="gh-dashboard-list-text" @route="member" @model="{{parsedEvent.memberId}}" @query={{hash postAnalytics=@post.id}}>{{parsedEvent.subject}}</LinkTo>
|
||||
{{else}}
|
||||
{{#if parsedEvent.email}}
|
||||
<GhMemberAvatar @name={{parsedEvent.subject}} @containerClass="w6 h6 mr3 flex-shrink-0" />
|
||||
<a class="gh-dashboard-list-text" href="mailto:{{parsedEvent.email}}" target="_blank" rel="noopener noreferrer" title="{{parsedEvent.email}}">{{parsedEvent.subject}}</a>
|
||||
{{else}}
|
||||
<GhMemberAvatar @name={{parsedEvent.subject}} @containerClass="w6 h6 mr3 flex-shrink-0" />
|
||||
<span class="gh-dashboard-list-text">{{parsedEvent.subject}}</span>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="gh-dashboard-list-item-sub">
|
||||
{{svg-jar parsedEvent.icon }}
|
||||
<span class="gh-dashboard-list-subtext">
|
||||
<span class="gh-members-activity-description">
|
||||
<span class="gh-members-activity-event-text">{{capitalize-first-letter parsedEvent.action}}</span>
|
||||
{{#if parsedEvent.info}}
|
||||
<span class="highlight"> ({{parsedEvent.info}})</span>
|
||||
{{/if}}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
{{#if (eq this.eventType "conversion")}}
|
||||
<div class="gh-dashboard-list-item-sub">
|
||||
{{#if parsedEvent.source}}
|
||||
<span title="Source" class="gh-members-activity-description">{{svg-jar "event-extras-source"}}<span class="gh-members-activity-event-text">{{parsedEvent.source.name}}</span></span>
|
||||
{{else}}
|
||||
<span class="midlightgrey">—</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="gh-dashboard-list-item-sub">
|
||||
<span class="gh-dashboard-list-subtext">{{moment-from-now parsedEvent.timestamp}}</span>
|
||||
</div>
|
||||
</div>
|
||||
{{/let}}
|
||||
{{/each}}
|
||||
|
||||
{{#if (compute (fn this.areStubsNeeded eventsFetcher))}}
|
||||
{{#let (compute (fn this.getAmountOfStubs eventsFetcher)) as |stubs|}}
|
||||
{{#each stubs}}
|
||||
<div class="gh-dashboard-list-item gh-dashboard-list-item-stub"></div>
|
||||
{{/each}}
|
||||
{{/let}}
|
||||
{{/if}}
|
||||
|
||||
<div class="gh-post-activity-feed-footer">
|
||||
<Posts::PostActivityFeed::FooterLinks
|
||||
@eventType={{this.eventType}}
|
||||
@post={{@post}}
|
||||
/>
|
||||
|
||||
<div class="gh-post-activity-feed-pagination">
|
||||
{{#if (compute (fn this.isPaginationNotNeeded eventsFetcher))}}
|
||||
Showing {{eventsFetcher.totalEvents}} in total
|
||||
{{else}}
|
||||
Showing {{eventsFetcher.previousEvents}}-{{eventsFetcher.shownEvents}} of {{eventsFetcher.totalEvents}}
|
||||
|
||||
<div class="gh-post-activity-feed-pagination-group">
|
||||
<button
|
||||
class="gh-post-activity-feed-pagination-button gh-post-activity-feed-prev-button"
|
||||
type="button"
|
||||
title="Previous page"
|
||||
disabled={{compute (fn this.isPreviousButtonDisabled eventsFetcher)}}
|
||||
{{on "click" eventsFetcher.loadPreviousPage}}
|
||||
>
|
||||
{{svg-jar "arrow-left-pagination"}}
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="gh-post-activity-feed-pagination-button gh-post-activity-feed-next-button"
|
||||
type="button"
|
||||
title="Next page"
|
||||
disabled={{compute (fn this.isNextButtonDisabled eventsFetcher)}}
|
||||
{{on "click" eventsFetcher.loadNextPage}}
|
||||
>
|
||||
{{svg-jar "arrow-right-pagination"}}
|
||||
</button>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if (eq @eventType 'feedback')}}
|
||||
<Posts::FeedbackEventsChart @data={{@data}} />
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<div class="gh-dashboard-list-body">
|
||||
<div class="gh-post-activity-feed-empty">
|
||||
<div class="attribution-list-empty">
|
||||
{{#if (eq this.eventType "sent")}}
|
||||
{{svg-jar "empty-sent"}}
|
||||
<h4>No members have received your email yet</h4>
|
||||
<p>Once someone receives your email, you'll be able to see the member activity here.</p>
|
||||
{{else if (eq this.eventType "opened")}}
|
||||
{{svg-jar "empty-opened"}}
|
||||
<h4>No members have opened your newsletter</h4>
|
||||
<p>Once someone opens, you'll see them listed here.</p>
|
||||
{{else if (eq this.eventType "clicked")}}
|
||||
{{svg-jar "empty-clicked"}}
|
||||
<h4>No links have been clicked in your newsletter</h4>
|
||||
<p>Once a member clicks a link, you'll see them listed here.</p>
|
||||
{{else if (eq this.eventType "feedback")}}
|
||||
{{svg-jar "empty-feedback"}}
|
||||
<h4>No members have given feedback yet</h4>
|
||||
<p>When someone does, you'll see their response here.</p>
|
||||
{{else if (eq this.eventType "conversion")}}
|
||||
{{svg-jar "empty-conversion"}}
|
||||
<h4>No members have signed up on this post</h4>
|
||||
<p>When someone new signs up, you'll see them here.</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/let}}
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import Component from '@glimmer/component';
|
||||
import {action} from '@ember/object';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
|
||||
export default class PostActivityFeed extends Component {
|
||||
@service feature;
|
||||
|
@ -14,6 +15,16 @@ export default class PostActivityFeed extends Component {
|
|||
conversion: ['subscription_event', 'signup_event']
|
||||
};
|
||||
|
||||
@tracked savedEventsFetcher = null;
|
||||
|
||||
@action
|
||||
saveEvents(fetcher) {
|
||||
if (fetcher && fetcher.data && fetcher.data.length > 0) {
|
||||
this.savedEventsFetcher = fetcher;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
get getEventTypes() {
|
||||
return this._eventTypes[this.args.eventType];
|
||||
}
|
||||
|
|
19
ghost/admin/app/helpers/split-number.js
Normal file
19
ghost/admin/app/helpers/split-number.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
import {formatNumber} from './format-number';
|
||||
import {helper} from '@ember/component/helper';
|
||||
import {htmlSafe} from '@ember/template';
|
||||
|
||||
export function splitNumber([number, previousNumber]) {
|
||||
let formattedNewNumber = formatNumber(number);
|
||||
let formattedOldNumber = formatNumber(previousNumber);
|
||||
|
||||
let oldChars = formattedOldNumber.split('').map(char => `<span class="old-char">${char}</span>`).join('');
|
||||
|
||||
let newChars = formattedNewNumber.split('').map(char => `<span class="new-char">${char}</span>`).join('');
|
||||
|
||||
return htmlSafe(`
|
||||
<div class="number-group old-number">${oldChars}</div>
|
||||
<div class="number-group new-number">${newChars}</div>
|
||||
`);
|
||||
}
|
||||
|
||||
export default helper(splitNumber);
|
|
@ -1500,6 +1500,21 @@
|
|||
margin-top: -2px;
|
||||
}
|
||||
|
||||
.gh-post-analytics-header .gh-btn.refresh {
|
||||
border: 1px solid var(--whitegrey-d1);
|
||||
background: var(--white);
|
||||
color: var(--darkgrey);
|
||||
}
|
||||
|
||||
.gh-post-analytics-header .gh-btn.refresh:hover {
|
||||
border-color: var(--lightgrey-l2);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.gh-post-analytics-header .gh-btn.refresh svg path {
|
||||
stroke-width: 2.25;
|
||||
}
|
||||
|
||||
.gh-post-list-cta.is-hovered {
|
||||
border-color: var(--whitegrey-d2);
|
||||
}
|
||||
|
@ -1532,7 +1547,7 @@
|
|||
}
|
||||
|
||||
.gh-post-list-cta > svg path {
|
||||
stroke-width: 2;
|
||||
stroke-width: 1.75;
|
||||
}
|
||||
|
||||
.gh-post-list-cta > span {
|
||||
|
@ -1727,6 +1742,26 @@ span.dropdown .gh-post-list-cta > span {
|
|||
height: 16px;
|
||||
}
|
||||
|
||||
.gh-tabs-analytics .animated-number {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.gh-tabs-analytics .animated-number .new-number {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.gh-tabs-analytics .animated-number .new-char,
|
||||
.gh-tabs-analytics .animated-number .old-char {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.gh-tabs-analytics .animated-number .new-char {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
.gh-tabs-analytics .gh-dashboard-list-item {
|
||||
grid-template-columns: 40% 40% 20%;
|
||||
}
|
||||
|
|
|
@ -56,6 +56,7 @@
|
|||
"@tryghost/nql-lang": "0.6.1",
|
||||
"@tryghost/string": "0.2.12",
|
||||
"@tryghost/timezone-data": "0.4.3",
|
||||
"animejs": "3.2.2",
|
||||
"autoprefixer": "9.8.6",
|
||||
"babel-eslint": "10.1.0",
|
||||
"babel-plugin-transform-class-properties": "6.24.1",
|
||||
|
@ -205,4 +206,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9473,6 +9473,11 @@ amperize@0.6.1:
|
|||
uuid "^7.0.1"
|
||||
validator "^12.0.0"
|
||||
|
||||
animejs@3.2.2:
|
||||
version "3.2.2"
|
||||
resolved "https://registry.yarnpkg.com/animejs/-/animejs-3.2.2.tgz#59be98c58834339d5847f4a70ddba74ac75b6afc"
|
||||
integrity sha512-Ao95qWLpDPXXM+WrmwcKbl6uNlC5tjnowlaRYtuVDHHoygjtIPfDUoK9NthrlZsQSKjZXlmji2TrBUAVbiH0LQ==
|
||||
|
||||
ansi-colors@4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
|
||||
|
|
Loading…
Add table
Reference in a new issue