Merge pull request #864 from mailpoet/campaign_stats

Add detailed stats page support in Free, change stats style [PREMIUM-1] [MAILPOET-877]
This commit is contained in:
Tautvidas Sipavičius
2017-04-26 14:30:51 +03:00
committed by GitHub
18 changed files with 436 additions and 75 deletions

View File

@@ -209,8 +209,8 @@ const ListingItems = React.createClass({
className="colspanchange">
{
(this.props.loading === true)
? MailPoet.I18n.t('loadingItems')
: MailPoet.I18n.t('noItemsFound')
? (this.props.messages.onLoadingItems || MailPoet.I18n.t('loadingItems'))
: (this.props.messages.onNoItemsFound || MailPoet.I18n.t('noItemsFound'))
}
</td>
</tr>
@@ -793,6 +793,12 @@ const Listing = React.createClass({
groups = false;
}
// messages
let messages = {};
if (this.props.messages !== undefined) {
messages = this.props.messages;
}
return (
<div>
{ groups }
@@ -846,6 +852,7 @@ const Listing = React.createClass({
count={ this.state.count }
limit={ this.state.limit }
item_actions={ item_actions }
messages={ messages }
items={ items } />
<tfoot>

View File

@@ -0,0 +1,37 @@
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}` : ''
);
const tooltip = this.props.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;

View File

@@ -0,0 +1,105 @@
import MailPoet from 'mailpoet'
import React from 'react'
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) {
return (
<div>
<span className={`mailpoet_stat_${badgeType}`}>
{this.props.headline}
</span> {content}
</div>
);
}
return content;
}
}
export default StatsBadge;

View File

@@ -1,9 +1,13 @@
import React from 'react'
import ReactDOM from 'react-dom'
import ReactStringReplace from 'react-string-replace'
import { Link } from 'react-router'
import MailPoet from 'mailpoet'
import classNames from 'classnames'
import moment from 'moment'
import jQuery from 'jquery'
import Hooks from 'wp-js-hooks'
import StatsBadge from 'newsletters/badges/stats.jsx'
const _QueueMixin = {
pauseSending: function(newsletter) {
@@ -140,40 +144,162 @@ const _QueueMixin = {
};
const _StatisticsMixin = {
renderStatistics: function(newsletter) {
if (
newsletter.statistics
&& newsletter.queue
&& newsletter.queue.status !== 'scheduled'
) {
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 = Math.round(
(~~(newsletter.statistics.clicked) * 100) / total_sent
);
percentage_opened = Math.round(
(~~(newsletter.statistics.opened) * 100) / total_sent
);
percentage_unsubscribed = Math.round(
(~~(newsletter.statistics.unsubscribed) * 100) / total_sent
);
}
return (
<span>
{ percentage_opened }%, { percentage_clicked }%, { percentage_unsubscribed }%
</span>
);
} else {
renderStatistics: function(newsletter, is_sent, current_time) {
if (is_sent === undefined) {
// condition for standard and post notification listings
is_sent = newsletter.statistics
&& newsletter.queue
&& newsletter.queue.status !== 'scheduled'
}
if (!is_sent) {
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 show_stats_timeout,
newsletter_date,
sent_hours_ago,
too_early_for_stats,
show_kb_link;
if (current_time !== undefined) {
// standard emails and post notifications:
// display green box for newsletters that were just sent
show_stats_timeout = 6; // in hours
newsletter_date = newsletter.queue.scheduled_at || newsletter.queue.created_at;
sent_hours_ago = moment(current_time).diff(moment(newsletter_date), 'hours');
too_early_for_stats = sent_hours_ago < show_stats_timeout;
show_kb_link = true;
} else {
// welcome emails: no green box and KB link
too_early_for_stats = false;
show_kb_link = false;
}
const improveStatsKBLink = 'http://beta.docs.mailpoet.com/article/191-how-to-improve-my-open-and-click-rates';
// thresholds to display badges
const min_newsletters_sent = 20;
const min_newsletter_opens = 5;
let content;
if (total_sent >= min_newsletters_sent
&& newsletter.statistics.opened >= min_newsletter_opens
&& !too_early_for_stats
) {
// 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 = (
<div>
<span className="mailpoet_stats_text">
{ percentage_opened_display }%,
{ " " }
{ percentage_clicked_display }%
<span className="mailpoet_stat_hidden">
, { percentage_unsubscribed_display }%
</span>
</span>
{ too_early_for_stats && (
<div className="mailpoet_badge mailpoet_badge_green">
{MailPoet.I18n.t('checkBackInHours')
.replace('%$1d', show_stats_timeout - sent_hours_ago)}
</div>
) }
</div>
);
}
// thresholds to display bad open rate help
const max_percentage_opened = 5;
const min_sent_hours_ago = 24;
const min_total_sent = 10;
let after_content;
if (show_kb_link
&& percentage_opened < max_percentage_opened
&& sent_hours_ago >= min_sent_hours_ago
&& total_sent >= min_total_sent
) {
// help link for bad open rate
after_content = (
<div>
<a
href={improveStatsKBLink}
target="_blank"
className="mailpoet_stat_link_small"
>
{MailPoet.I18n.t('improveThisLinkText')}
</a>
</div>
);
}
if (total_sent > 0 && !too_early_for_stats && params.link) {
// wrap content in a link
return (
<div>
<Link
key={ `stats-${newsletter.id}` }
to={ params.link }
>
{content}
</Link>
{after_content}
</div>
);
}
return (
<div>
{content}
{after_content}
</div>
);
}
}

View File

@@ -87,7 +87,7 @@ const NewsletterListNotificationHistory = React.createClass({
</td>
{ (mailpoet_tracking_enabled === true) ? (
<td className="column" data-colname={ MailPoet.I18n.t('statistics') }>
{ this.renderStatistics(newsletter) }
{ this.renderStatistics(newsletter, undefined, meta.current_time) }
</td>
) : null }
<td className="column-date" data-colname={ MailPoet.I18n.t('lastModifiedOn') }>

View File

@@ -184,7 +184,7 @@ const NewsletterListStandard = React.createClass({
</td>
{ (mailpoet_tracking_enabled === true) ? (
<td className="column" data-colname={ MailPoet.I18n.t('statistics') }>
{ this.renderStatistics(newsletter) }
{ this.renderStatistics(newsletter, undefined, meta.current_time) }
</td>
) : null }
<td className="column-date" data-colname={ MailPoet.I18n.t('lastModifiedOn') }>

View File

@@ -5,12 +5,13 @@ 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'
import MailPoet from 'mailpoet'
import _ from 'underscore'
import Hooks from 'wp-js-hooks'
const mailpoet_roles = window.mailpoet_roles || {};
const mailpoet_segments = window.mailpoet_segments || {};
@@ -155,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
@@ -276,35 +277,6 @@ const NewsletterListWelcome = React.createClass({
</span>
);
},
renderStatistics: function(newsletter) {
if (mailpoet_tracking_enabled === false) {
return;
}
if (newsletter.total_sent > 0 && newsletter.statistics) {
const total_sent = ~~(newsletter.total_sent);
const percentage_clicked = Math.round(
(~~(newsletter.statistics.clicked) * 100) / total_sent
);
const percentage_opened = Math.round(
(~~(newsletter.statistics.opened) * 100) / total_sent
);
const percentage_unsubscribed = Math.round(
(~~(newsletter.statistics.unsubscribed) * 100) / total_sent
);
return (
<span>
{ percentage_opened }%, { percentage_clicked }%, { percentage_unsubscribed }%
</span>
);
} else {
return (
<span>{MailPoet.I18n.t('notSentYet')}</span>
);
}
},
renderItem: function(newsletter, actions) {
const rowClasses = classNames(
'manage-column',
@@ -331,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') }>

View File

@@ -2,6 +2,7 @@ import React from 'react'
import ReactDOM from 'react-dom'
import { Router, Route, IndexRedirect, Link, useRouterHistory } from 'react-router'
import { createHashHistory } from 'history'
import Hooks from 'wp-js-hooks'
import NewsletterTypes from 'newsletters/types.jsx'
import NewsletterTemplates from 'newsletters/templates.jsx'
@@ -27,6 +28,9 @@ const App = React.createClass({
const container = document.getElementById('newsletters_container');
if(container) {
let extra_routes = [];
extra_routes = Hooks.applyFilters('mailpoet_newsletters_before_router', extra_routes);
const mailpoet_listing = ReactDOM.render((
<Router history={ history }>
<Route path="/" component={ App }>
@@ -46,6 +50,8 @@ if(container) {
<Route name="template" path="template/:id" component={ NewsletterTemplates } />
{/* Sending options */}
<Route path="send/:id" component={ NewsletterSend } />
{/* Extra routes */}
{ extra_routes.map(rt => <Route key={rt.path} path={rt.path} component={rt.component} />) }
</Route>
</Router>
), container);

21
assets/js/src/num.js Normal file
View File

@@ -0,0 +1,21 @@
define('num',
[
'mailpoet'
], function(
MailPoet
) {
'use strict';
MailPoet.Num = {
toLocaleFixed: function (num, precision) {
precision = precision || 0;
var factor = Math.pow(10, precision);
return (Math.round(num * factor) / factor)
.toLocaleString(
undefined,
{minimumFractionDigits: precision, maximumFractionDigits: precision}
);
}
};
});