import Component from '@glimmer/component'; import {action} from '@ember/object'; import {didCancel, task} from 'ember-concurrency'; import {inject as service} from '@ember/service'; import {tracked} from '@glimmer/tracking'; /** * @typedef {import('../../services/dashboard-stats').SourceAttributionCount} SourceAttributionCount */ const DISPLAY_OPTIONS = [{ name: 'Free signups', value: 'signups' }, { name: 'Paid conversions', value: 'paid' }]; export default class Analytics extends Component { @service ajax; @service ghostPaths; @service settings; @service membersUtils; @service utils; @service feature; @tracked sources = null; @tracked links = null; @tracked sortColumn = 'signups'; @tracked showSuccess; @tracked updateLinkId; displayOptions = DISPLAY_OPTIONS; get post() { return this.args.post; } get allowedDisplayOptions() { if (!this.hasPaidConversionData) { return this.displayOptions.filter(d => d.value === 'signups'); } if (!this.hasFreeSignups) { return this.displayOptions.filter(d => d.value === 'paid'); } return this.displayOptions; } get isDropdownDisabled() { if (!this.hasPaidConversionData || !this.hasFreeSignups) { return true; } return false; } get selectedDisplayOption() { if (!this.hasPaidConversionData) { return this.displayOptions.find(d => d.value === 'signups'); } if (!this.hasFreeSignups) { return this.displayOptions.find(d => d.value === 'paid'); } return this.displayOptions.find(d => d.value === this.sortColumn) ?? this.displayOptions[0]; } get selectedSortColumn() { if (!this.hasPaidConversionData) { return 'signups'; } if (!this.hasFreeSignups) { return 'paid'; } return this.sortColumn; } get hasPaidConversionData() { return this.sources.some(sourceData => sourceData.paidConversions > 0); } get hasFreeSignups() { return this.sources.some(sourceData => sourceData.signups > 0); } @action onDisplayChange(selected) { this.sortColumn = selected.value; } @action setSortColumn(column) { this.sortColumn = column; } @action updateLink(linkId, linkTo) { if (this._updateLinks.isRunning) { return this._updateLinks.last; } return this._updateLinks.perform(linkId, linkTo); } @action loadData() { if (this.showSources) { this.fetchReferrersStats(); } else { this.sources = []; } if (this.showLinks) { this.fetchLinks(); } else { this.links = []; } } updateLinkData(linksData) { let updatedLinks; if (this.links?.length) { updatedLinks = this.links.map((link) => { let linkData = linksData.find(l => l.link.link_id === link.link.link_id); if (linkData) { return { ...linkData, link: { ...linkData.link, originalTo: linkData.link.to, to: this.utils.cleanTrackedUrl(linkData.link.to, false), title: this.utils.cleanTrackedUrl(linkData.link.to, true) } }; } return link; }); } else { updatedLinks = linksData.map((link) => { return { ...link, link: { ...link.link, originalTo: link.link.to, to: this.utils.cleanTrackedUrl(link.link.to, false), title: this.utils.cleanTrackedUrl(link.link.to, true) } }; }); } // Remove duplicates by title ad merge const linksByTitle = updatedLinks.reduce((acc, link) => { if (!acc[link.link.title]) { acc[link.link.title] = link; } else { acc[link.link.title].clicks += link.clicks; } return acc; }, {}); this.links = Object.values(linksByTitle); } async fetchReferrersStats() { try { if (this._fetchReferrersStats.isRunning) { return this._fetchReferrersStats.last; } return this._fetchReferrersStats.perform(); } catch (e) { if (!didCancel(e)) { // re-throw the non-cancelation error throw e; } } } async fetchLinks() { if (this._fetchLinks.isRunning) { return this._fetchLinks.last; } return this._fetchLinks.perform(); } @task *_updateLinks(linkId, newLink) { this.updateLinkId = linkId; let currentLink; this.links = this.links?.map((link) => { if (link.link.link_id === linkId) { currentLink = new URL(link.link.originalTo); return { ...link, link: { ...link.link, to: this.utils.cleanTrackedUrl(newLink, false), title: this.utils.cleanTrackedUrl(newLink, true) } }; } return link; }); const filter = `post_id:${this.post.id}+to:'${currentLink}'`; let bulkUpdateUrl = this.ghostPaths.url.api(`links/bulk`) + `?filter=${encodeURIComponent(filter)}`; yield this.ajax.put(bulkUpdateUrl, { data: { bulk: { action: 'updateLink', meta: {link: {to: newLink}} } } }); // Refresh links data const linksFilter = `post_id:${this.post.id}`; let statsUrl = this.ghostPaths.url.api(`links/`) + `?filter=${encodeURIComponent(linksFilter)}`; let result = yield this.ajax.request(statsUrl); this.updateLinkData(result.links); this.showSuccess = this.updateLinkId; setTimeout(() => { this.showSuccess = null; }, 2000); } @task *_fetchReferrersStats() { let statsUrl = this.ghostPaths.url.api(`stats/referrers/posts/${this.post.id}`); let result = yield this.ajax.request(statsUrl); this.sources = result.stats.map((stat) => { return { source: stat.source ?? 'Direct', signups: stat.signups, paidConversions: stat.paid_conversions }; }); } @task *_fetchLinks() { const filter = `post_id:${this.post.id}`; let statsUrl = this.ghostPaths.url.api(`links/`) + `?filter=${encodeURIComponent(filter)}`; let result = yield this.ajax.request(statsUrl); this.updateLinkData(result.links); } get showLinks() { return this.post.showEmailClickAnalytics; } get showSources() { return this.post.showAttributionAnalytics; } get isLoaded() { return this.links !== null && this.souces !== null; } }