diff --git a/assets/css/src/listing/newsletters.styl b/assets/css/src/listing/newsletters.styl
index 329c08c0c3..ae34606d22 100644
--- a/assets/css/src/listing/newsletters.styl
+++ b/assets/css/src/listing/newsletters.styl
@@ -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
\ No newline at end of file
+ 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
diff --git a/assets/js/src/newsletters/badges/badge.jsx b/assets/js/src/newsletters/badges/badge.jsx
new file mode 100644
index 0000000000..9bd039f994
--- /dev/null
+++ b/assets/js/src/newsletters/badges/badge.jsx
@@ -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, '
') || false;
+ // tooltip ID must be unique, defaults to tooltip text
+ const tooltipId = this.props.tooltipId || tooltip;
+
+ return (
+
+
+ {this.props.name}
+
+ { tooltip && (
+
+ ) }
+
+ );
+ }
+}
+
+export default Badge;
diff --git a/assets/js/src/newsletters/badges/stats.jsx b/assets/js/src/newsletters/badges/stats.jsx
new file mode 100644
index 0000000000..6f3230b53b
--- /dev/null
+++ b/assets/js/src/newsletters/badges/stats.jsx
@@ -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 = (
+
+ );
+
+ if (this.props.headline) {
+ const headlineClasses = classNames(
+ `mailpoet_stat_${badgeType}`,
+ this.props.size ? `mailpoet_badge_size_${this.props.size}` : ''
+ );
+
+ return (
+
+
+ {this.props.headline}
+ {content}
+
+ );
+ }
+
+ return content;
+ }
+}
+
+export default StatsBadge;
diff --git a/assets/js/src/newsletters/listings/mixins.jsx b/assets/js/src/newsletters/listings/mixins.jsx
index ce2f1ee508..22db6cd1d1 100644
--- a/assets/js/src/newsletters/listings/mixins.jsx
+++ b/assets/js/src/newsletters/listings/mixins.jsx
@@ -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 = (
-
- { percentage_opened }%, { percentage_clicked }%, { percentage_unsubscribed }%
-
- );
-
- if (total_sent > 0 && params.link) {
- return (
- { content }
- );
- }
-
- 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 (
{MailPoet.I18n.t('notSentYet')}
);
}
+
+ 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 = (
+
+
+ { percentage_opened_display }%
+
+
+
+ { percentage_clicked_display }%
+
+
+
+ { percentage_unsubscribed_display }%
+
+
+ );
+ } else {
+ // display simple stats
+ content = (
+
+ { percentage_opened_display }%,
+ { " " }
+ { percentage_clicked_display }%
+
+ , { percentage_unsubscribed_display }%
+
+
+ );
+ }
+
+ if (total_sent > 0 && params.link) {
+ return (
+ { content }
+ );
+ }
+
+ return content;
}
}
diff --git a/assets/js/src/newsletters/listings/welcome.jsx b/assets/js/src/newsletters/listings/welcome.jsx
index 9a90d4e870..226eefcf9b 100644
--- a/assets/js/src/newsletters/listings/welcome.jsx
+++ b/assets/js/src/newsletters/listings/welcome.jsx
@@ -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({
);
},
- 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 = (
-
- { percentage_opened }%, { percentage_clicked }%, { percentage_unsubscribed }%
-
- );
-
- if (params.link) {
- return (
- { content }
- );
- }
-
- return content;
- } else {
- return (
- {MailPoet.I18n.t('notSentYet')}
- );
- }
- },
renderItem: function(newsletter, actions) {
const rowClasses = classNames(
'manage-column',
@@ -345,7 +303,10 @@ const NewsletterListWelcome = React.createClass({
{ (mailpoet_tracking_enabled === true) ? (
- { this.renderStatistics(newsletter) }
+ { this.renderStatistics(
+ newsletter,
+ newsletter.total_sent > 0 && newsletter.statistics
+ ) }
|
) : null }
diff --git a/package.json b/package.json
index ae5c11c6da..cd924bc121 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/views/newsletters.html b/views/newsletters.html
index 40d74f3b8f..83bff9cebc 100644
--- a/views/newsletters.html
+++ b/views/newsletters.html
@@ -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'),
diff --git a/webpack.config.js b/webpack.config.js
index f4f53d7410..ea3d5e179a 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -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',
|