Add badges to stats in a newsletter listing, change stats style [PREMIUM-1] [MAILPOET-877]
This commit is contained in:
@ -1,3 +1,59 @@
|
||||
$excellent-badge-color = #2993ab
|
||||
$good-badge-color = #f0b849
|
||||
$bad-badge-color = #d54e21
|
||||
$grey-stat-color = #707070
|
||||
|
||||
#newsletters_container
|
||||
h2.nav-tab-wrapper
|
||||
margin-bottom: 1rem
|
||||
|
||||
.mailpoet_stats_text
|
||||
font-size: 14px
|
||||
font-weight: 600;
|
||||
|
||||
.mailpoet_stat
|
||||
|
||||
&_excellent
|
||||
color: $excellent-badge-color
|
||||
|
||||
&_good
|
||||
color: $good-badge-color
|
||||
|
||||
&_bad
|
||||
color: $bad-badge-color
|
||||
|
||||
&_grey
|
||||
color: $grey-stat-color
|
||||
|
||||
&_big
|
||||
font-size: 23px
|
||||
font-weight: 600
|
||||
line-height: normal
|
||||
|
||||
&_spaced
|
||||
margin-bottom: 1rem
|
||||
|
||||
&_tplspaced
|
||||
margin-bottom: 3rem
|
||||
|
||||
&_hidden
|
||||
display: none
|
||||
|
||||
.mailpoet_badge
|
||||
padding: 4px 6px 3px 6px
|
||||
color: #FFFFFF
|
||||
margin-right: 4px
|
||||
text-transform: uppercase
|
||||
font-size: 0.5625rem
|
||||
font-weight: 500
|
||||
border-radius: 3px
|
||||
letter-spacing: 1px
|
||||
|
||||
&_excellent
|
||||
background: $excellent-badge-color
|
||||
|
||||
&_good
|
||||
background: $good-badge-color
|
||||
|
||||
&_bad
|
||||
background: $bad-badge-color
|
||||
|
38
assets/js/src/newsletters/badges/badge.jsx
Normal file
38
assets/js/src/newsletters/badges/badge.jsx
Normal file
@ -0,0 +1,38 @@
|
||||
import React from 'react'
|
||||
import classNames from 'classnames'
|
||||
import ReactTooltip from 'react-tooltip'
|
||||
|
||||
class Badge extends React.Component {
|
||||
render() {
|
||||
const badgeClasses = classNames(
|
||||
'mailpoet_badge',
|
||||
this.props.type ? `mailpoet_badge_${this.props.type}` : '',
|
||||
this.props.size ? `mailpoet_badge_size_${this.props.size}` : ''
|
||||
);
|
||||
|
||||
const tooltip = this.props.tooltip.replace(/\n/g, '<br />') || false;
|
||||
// tooltip ID must be unique, defaults to tooltip text
|
||||
const tooltipId = this.props.tooltipId || tooltip;
|
||||
|
||||
return (
|
||||
<span>
|
||||
<span
|
||||
className={badgeClasses}
|
||||
data-tip={tooltip}
|
||||
data-for={tooltipId}
|
||||
>
|
||||
{this.props.name}
|
||||
</span>
|
||||
{ tooltip && (
|
||||
<ReactTooltip
|
||||
place="right"
|
||||
multiline={true}
|
||||
id={tooltipId}
|
||||
/>
|
||||
) }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Badge;
|
111
assets/js/src/newsletters/badges/stats.jsx
Normal file
111
assets/js/src/newsletters/badges/stats.jsx
Normal file
@ -0,0 +1,111 @@
|
||||
import MailPoet from 'mailpoet'
|
||||
import React from 'react'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import Badge from './badge.jsx'
|
||||
|
||||
const badges = {
|
||||
excellent: {
|
||||
name: MailPoet.I18n.t('excellentBadgeName'),
|
||||
tooltipTitle: MailPoet.I18n.t('excellentBadgeTooltip')
|
||||
},
|
||||
good: {
|
||||
name: MailPoet.I18n.t('goodBadgeName'),
|
||||
tooltipTitle: MailPoet.I18n.t('goodBadgeTooltip')
|
||||
},
|
||||
bad: {
|
||||
name: MailPoet.I18n.t('badBadgeName'),
|
||||
tooltipTitle: MailPoet.I18n.t('badBadgeTooltip')
|
||||
}
|
||||
};
|
||||
|
||||
const stats = {
|
||||
opened: {
|
||||
badgeRanges: [30, 15, 0],
|
||||
badgeTypes: [
|
||||
'excellent',
|
||||
'good',
|
||||
'bad'
|
||||
],
|
||||
tooltipText: MailPoet.I18n.t('openedStatTooltip'),
|
||||
},
|
||||
clicked: {
|
||||
badgeRanges: [3, 1, 0],
|
||||
badgeTypes: [
|
||||
'excellent',
|
||||
'good',
|
||||
'bad'
|
||||
],
|
||||
tooltipText: MailPoet.I18n.t('clickedStatTooltip')
|
||||
},
|
||||
unsubscribed: {
|
||||
badgeRanges: [3, 1, 0],
|
||||
badgeTypes: [
|
||||
'bad',
|
||||
'good',
|
||||
'excellent'
|
||||
],
|
||||
tooltipText: MailPoet.I18n.t('unsubscribedStatTooltip')
|
||||
},
|
||||
};
|
||||
|
||||
class StatsBadge extends React.Component {
|
||||
getBadgeType(stat, rate) {
|
||||
const len = stat.badgeRanges.length;
|
||||
for (var i = 0; i < len; i++) {
|
||||
if (rate > stat.badgeRanges[i]) {
|
||||
return stat.badgeTypes[i];
|
||||
}
|
||||
}
|
||||
// rate must be zero at this point
|
||||
return stat.badgeTypes[len - 1];
|
||||
}
|
||||
render() {
|
||||
const stat = stats[this.props.stat] || null;
|
||||
if (!stat) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rate = this.props.rate;
|
||||
if (rate < 0 || rate > 100) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const badgeType = this.getBadgeType(stat, rate);
|
||||
const badge = badges[badgeType] || null;
|
||||
if (!badge) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tooltipText = `${badge.tooltipTitle}\n\n${stat.tooltipText}`;
|
||||
const tooltipId = this.props.tooltipId || null;
|
||||
|
||||
const content = (
|
||||
<Badge
|
||||
type={badgeType}
|
||||
name={badge.name}
|
||||
tooltip={tooltipText}
|
||||
tooltipId={tooltipId}
|
||||
/>
|
||||
);
|
||||
|
||||
if (this.props.headline) {
|
||||
const headlineClasses = classNames(
|
||||
`mailpoet_stat_${badgeType}`,
|
||||
this.props.size ? `mailpoet_badge_size_${this.props.size}` : ''
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span className={headlineClasses}>
|
||||
{this.props.headline}
|
||||
</span> {content}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
export default StatsBadge;
|
@ -6,6 +6,7 @@ import MailPoet from 'mailpoet'
|
||||
import classNames from 'classnames'
|
||||
import jQuery from 'jquery'
|
||||
import Hooks from 'wp-js-hooks'
|
||||
import StatsBadge from 'newsletters/badges/stats.jsx'
|
||||
|
||||
const _QueueMixin = {
|
||||
pauseSending: function(newsletter) {
|
||||
@ -142,53 +143,90 @@ const _QueueMixin = {
|
||||
};
|
||||
|
||||
const _StatisticsMixin = {
|
||||
renderStatistics: function(newsletter) {
|
||||
if (
|
||||
newsletter.statistics
|
||||
&& newsletter.queue
|
||||
&& newsletter.queue.status !== 'scheduled'
|
||||
) {
|
||||
let params = {};
|
||||
params = Hooks.applyFilters('mailpoet_newsletters_listing_stats_before', params, newsletter);
|
||||
|
||||
const total_sent = ~~(newsletter.queue.count_processed);
|
||||
|
||||
let percentage_clicked = 0;
|
||||
let percentage_opened = 0;
|
||||
let percentage_unsubscribed = 0;
|
||||
|
||||
if (total_sent > 0) {
|
||||
percentage_clicked = (newsletter.statistics.clicked * 100) / total_sent;
|
||||
percentage_opened = (newsletter.statistics.opened * 100) / total_sent;
|
||||
percentage_unsubscribed = (newsletter.statistics.unsubscribed * 100) / total_sent;
|
||||
}
|
||||
|
||||
// format to 1 decimal place
|
||||
percentage_clicked = MailPoet.Num.toLocaleFixed(percentage_clicked, 1);
|
||||
percentage_opened = MailPoet.Num.toLocaleFixed(percentage_opened, 1);
|
||||
percentage_unsubscribed = MailPoet.Num.toLocaleFixed(percentage_unsubscribed, 1);
|
||||
|
||||
const content = (
|
||||
<span>
|
||||
{ percentage_opened }%, { percentage_clicked }%, { percentage_unsubscribed }%
|
||||
</span>
|
||||
);
|
||||
|
||||
if (total_sent > 0 && params.link) {
|
||||
return (
|
||||
<Link
|
||||
key={ `stats-${newsletter.id}` }
|
||||
to={ params.link }
|
||||
>{ content }</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return content;
|
||||
} else {
|
||||
renderStatistics: function(newsletter, sentCondition) {
|
||||
if (sentCondition === undefined) {
|
||||
// condition for standard and post notification listings
|
||||
sentCondition = newsletter.statistics
|
||||
&& newsletter.queue
|
||||
&& newsletter.queue.status !== 'scheduled'
|
||||
}
|
||||
if (!sentCondition) {
|
||||
return (
|
||||
<span>{MailPoet.I18n.t('notSentYet')}</span>
|
||||
);
|
||||
}
|
||||
|
||||
let params = {};
|
||||
params = Hooks.applyFilters('mailpoet_newsletters_listing_stats_before', params, newsletter);
|
||||
|
||||
// welcome emails provide explicit total_sent value
|
||||
const total_sent = ~~(newsletter.total_sent || newsletter.queue.count_processed);
|
||||
|
||||
let percentage_clicked = 0;
|
||||
let percentage_opened = 0;
|
||||
let percentage_unsubscribed = 0;
|
||||
|
||||
if (total_sent > 0) {
|
||||
percentage_clicked = (newsletter.statistics.clicked * 100) / total_sent;
|
||||
percentage_opened = (newsletter.statistics.opened * 100) / total_sent;
|
||||
percentage_unsubscribed = (newsletter.statistics.unsubscribed * 100) / total_sent;
|
||||
}
|
||||
|
||||
// format to 1 decimal place
|
||||
const percentage_clicked_display = MailPoet.Num.toLocaleFixed(percentage_clicked, 1);
|
||||
const percentage_opened_display = MailPoet.Num.toLocaleFixed(percentage_opened, 1);
|
||||
const percentage_unsubscribed_display = MailPoet.Num.toLocaleFixed(percentage_unsubscribed, 1);
|
||||
|
||||
let content;
|
||||
if (total_sent >= 20 && newsletter.statistics.opened >= 5) {
|
||||
// display stats with badges
|
||||
content = (
|
||||
<div className="mailpoet_stats_text">
|
||||
<div>
|
||||
<span>{ percentage_opened_display }% </span>
|
||||
<StatsBadge
|
||||
stat="opened"
|
||||
rate={percentage_opened}
|
||||
tooltipId={`opened-${newsletter.id}`}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<span>{ percentage_clicked_display }% </span>
|
||||
<StatsBadge
|
||||
stat="clicked"
|
||||
rate={percentage_clicked}
|
||||
tooltipId={`clicked-${newsletter.id}`}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<span className="mailpoet_stat_hidden">{ percentage_unsubscribed_display }%</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
// display simple stats
|
||||
content = (
|
||||
<span className="mailpoet_stats_text">
|
||||
{ percentage_opened_display }%,
|
||||
{ " " }
|
||||
{ percentage_clicked_display }%
|
||||
<span className="mailpoet_stat_hidden">
|
||||
, { percentage_unsubscribed_display }%
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (total_sent > 0 && params.link) {
|
||||
return (
|
||||
<Link
|
||||
key={ `stats-${newsletter.id}` }
|
||||
to={ params.link }
|
||||
>{ content }</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,7 @@ import { createHashHistory } from 'history'
|
||||
import Listing from 'listing/listing.jsx'
|
||||
import ListingTabs from 'newsletters/listings/tabs.jsx'
|
||||
|
||||
import { MailerMixin } from 'newsletters/listings/mixins.jsx'
|
||||
import { StatisticsMixin, MailerMixin } from 'newsletters/listings/mixins.jsx'
|
||||
|
||||
import classNames from 'classnames'
|
||||
import jQuery from 'jquery'
|
||||
@ -156,7 +156,7 @@ const newsletter_actions = [
|
||||
];
|
||||
|
||||
const NewsletterListWelcome = React.createClass({
|
||||
mixins: [ MailerMixin ],
|
||||
mixins: [ StatisticsMixin, MailerMixin ],
|
||||
updateStatus: function(e) {
|
||||
// make the event persist so that we can still override the selected value
|
||||
// in the ajax callback
|
||||
@ -277,48 +277,6 @@ const NewsletterListWelcome = React.createClass({
|
||||
</span>
|
||||
);
|
||||
},
|
||||
renderStatistics: function(newsletter) {
|
||||
if (mailpoet_tracking_enabled === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
let params = {};
|
||||
params = Hooks.applyFilters('mailpoet_newsletters_listing_stats_before', params, newsletter);
|
||||
|
||||
if (newsletter.total_sent > 0 && newsletter.statistics) {
|
||||
const total_sent = ~~(newsletter.total_sent);
|
||||
|
||||
let percentage_clicked = (newsletter.statistics.clicked * 100) / total_sent;
|
||||
let percentage_opened = (newsletter.statistics.opened * 100) / total_sent;
|
||||
let percentage_unsubscribed = (newsletter.statistics.unsubscribed * 100) / total_sent;
|
||||
|
||||
// format to 1 decimal place
|
||||
percentage_clicked = MailPoet.Num.toLocaleFixed(percentage_clicked, 1);
|
||||
percentage_opened = MailPoet.Num.toLocaleFixed(percentage_opened, 1);
|
||||
percentage_unsubscribed = MailPoet.Num.toLocaleFixed(percentage_unsubscribed, 1);
|
||||
|
||||
const content = (
|
||||
<span>
|
||||
{ percentage_opened }%, { percentage_clicked }%, { percentage_unsubscribed }%
|
||||
</span>
|
||||
);
|
||||
|
||||
if (params.link) {
|
||||
return (
|
||||
<Link
|
||||
key={ `stats-${newsletter.id}` }
|
||||
to={ params.link }
|
||||
>{ content }</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return content;
|
||||
} else {
|
||||
return (
|
||||
<span>{MailPoet.I18n.t('notSentYet')}</span>
|
||||
);
|
||||
}
|
||||
},
|
||||
renderItem: function(newsletter, actions) {
|
||||
const rowClasses = classNames(
|
||||
'manage-column',
|
||||
@ -345,7 +303,10 @@ const NewsletterListWelcome = React.createClass({
|
||||
</td>
|
||||
{ (mailpoet_tracking_enabled === true) ? (
|
||||
<td className="column" data-colname={ MailPoet.I18n.t('statistics') }>
|
||||
{ this.renderStatistics(newsletter) }
|
||||
{ this.renderStatistics(
|
||||
newsletter,
|
||||
newsletter.total_sent > 0 && newsletter.statistics
|
||||
) }
|
||||
</td>
|
||||
) : null }
|
||||
<td className="column-date" data-colname={ MailPoet.I18n.t('lastModifiedOn') }>
|
||||
|
@ -27,6 +27,7 @@
|
||||
"react-dom": "~15.4.2",
|
||||
"react-router": "~3.0.2",
|
||||
"react-string-replace": "^0.3.2",
|
||||
"react-tooltip": "^3.2.10",
|
||||
"select2": "^4.0.0",
|
||||
"spectrum-colorpicker": "^1.6.2",
|
||||
"tinymce": "4.5.6",
|
||||
|
@ -60,7 +60,7 @@
|
||||
|
||||
'subject': __('Subject'),
|
||||
'status': __('Status'),
|
||||
'statistics': __('Opened, Clicked, Unsubscribed'),
|
||||
'statistics': __('Opened, Clicked'),
|
||||
'lists': __('Lists'),
|
||||
'settings': __('Settings'),
|
||||
'history': __('History'),
|
||||
@ -90,6 +90,16 @@
|
||||
'paused': __('Paused'),
|
||||
'new': __('Add New'),
|
||||
|
||||
'excellentBadgeName': __('Excellent'),
|
||||
'excellentBadgeTooltip': __('Congrats!'),
|
||||
'goodBadgeName': __('Good'),
|
||||
'goodBadgeTooltip': __('Good stuff.'),
|
||||
'badBadgeName': __('Bad'),
|
||||
'badBadgeTooltip': __('Something to improve.'),
|
||||
'openedStatTooltip': __('Above 30% is excellent.\\nBetween 15 and 30% is good.\\nUnder 15% is bad.'),
|
||||
'clickedStatTooltip': __('Above 3% is excellent.\\nBetween 1 and 3% is good.\\nUnder 1% is bad.'),
|
||||
'unsubscribedStatTooltip': __('Under 1% is excellent.\\nBetween 1 and 3% is good.\\nOver 3% is bad.'),
|
||||
|
||||
'templateFileMalformedError': __('This template file appears to be damaged. Please try another one.'),
|
||||
'importTemplateTitle': __('Import a template'),
|
||||
'selectJsonFileToUpload': __('Select a .json file to upload'),
|
||||
|
@ -81,6 +81,10 @@ baseConfig = {
|
||||
test: /listing.jsx/i,
|
||||
loader: 'expose-loader?' + globalPrefix + '.Listing!babel-loader',
|
||||
},
|
||||
{
|
||||
include: path.resolve(__dirname, 'assets/js/src/newsletters/badges/stats.jsx'),
|
||||
loader: 'expose-loader?' + globalPrefix + '.StatsBadge!babel-loader',
|
||||
},
|
||||
{
|
||||
include: /Blob.js$/,
|
||||
loader: 'exports-loader?window.Blob',
|
||||
@ -134,7 +138,8 @@ config.push(_.extend({}, baseConfig, {
|
||||
'react-dom',
|
||||
'react-router',
|
||||
'react-string-replace',
|
||||
'listing/listing.jsx'
|
||||
'listing/listing.jsx',
|
||||
'newsletters/badges/stats.jsx'
|
||||
],
|
||||
admin: [
|
||||
'subscribers/subscribers.jsx',
|
||||
|
Reference in New Issue
Block a user