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',