Move statistics page layout from premium to free
[MAILPOET-2104]
This commit is contained in:
91
assets/js/src/newsletters/campaign_stats/newsletter_info.jsx
Normal file
91
assets/js/src/newsletters/campaign_stats/newsletter_info.jsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import MailPoet from 'mailpoet';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function formatAddress(address, name) {
|
||||
let addressString = '';
|
||||
if (address) {
|
||||
addressString = (name) ? `${name} <${address}>` : address;
|
||||
}
|
||||
return addressString;
|
||||
}
|
||||
|
||||
function NewsletterStatsInfo(props) {
|
||||
const { newsletter } = props;
|
||||
|
||||
const newsletterDate = newsletter.queue.scheduled_at || newsletter.queue.created_at;
|
||||
|
||||
const senderAddress = formatAddress(
|
||||
newsletter.sender_address || '',
|
||||
newsletter.sender_name || ''
|
||||
);
|
||||
const replyToAddress = formatAddress(
|
||||
newsletter.reply_to_address || '',
|
||||
newsletter.reply_to_name || ''
|
||||
);
|
||||
|
||||
const segments = (newsletter.segments || []).map(segment => segment.name).join(', ');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mailpoet_stat_spaced">
|
||||
<a
|
||||
href={newsletter.preview_url}
|
||||
className="button-secondary"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{MailPoet.I18n.t('statsPreviewNewsletter')}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
{MailPoet.I18n.t('statsDateSent')}
|
||||
:
|
||||
{' '}
|
||||
{MailPoet.Date.format(newsletterDate)}
|
||||
</p>
|
||||
|
||||
{ segments && (
|
||||
<p>
|
||||
{MailPoet.I18n.t('statsToSegments')}
|
||||
:
|
||||
{' '}
|
||||
{ segments }
|
||||
</p>
|
||||
) }
|
||||
|
||||
<p>
|
||||
{MailPoet.I18n.t('statsFromAddress')}
|
||||
:
|
||||
{' '}
|
||||
{ senderAddress }
|
||||
</p>
|
||||
|
||||
{replyToAddress && (
|
||||
<p>
|
||||
{MailPoet.I18n.t('statsReplyToAddress')}
|
||||
:
|
||||
{' '}
|
||||
{ replyToAddress }
|
||||
</p>
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
NewsletterStatsInfo.propTypes = {
|
||||
newsletter: PropTypes.shape({
|
||||
queue: PropTypes.shape({
|
||||
scheduled_at: PropTypes.string,
|
||||
created_at: PropTypes.string,
|
||||
}).isRequired,
|
||||
sender_address: PropTypes.string,
|
||||
sender_name: PropTypes.string,
|
||||
reply_to_address: PropTypes.string,
|
||||
reply_to_name: PropTypes.string,
|
||||
segments: PropTypes.array,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default NewsletterStatsInfo;
|
124
assets/js/src/newsletters/campaign_stats/newsletter_stats.jsx
Normal file
124
assets/js/src/newsletters/campaign_stats/newsletter_stats.jsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import MailPoet from 'mailpoet';
|
||||
import React from 'react';
|
||||
import StatsBadge from 'stats-badge';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import RevenuesStats from './revenues_stats.jsx';
|
||||
|
||||
const NewsletterGeneralStats = ({ newsletter }) => {
|
||||
const totalSent = newsletter.total_sent || 0;
|
||||
let percentageClicked = 0;
|
||||
let percentageOpened = 0;
|
||||
let percentageUnsubscribed = 0;
|
||||
if (totalSent > 0) {
|
||||
percentageClicked = (newsletter.statistics.clicked * 100) / totalSent;
|
||||
percentageOpened = (newsletter.statistics.opened * 100) / totalSent;
|
||||
percentageUnsubscribed = (newsletter.statistics.unsubscribed * 100) / totalSent;
|
||||
}
|
||||
// format to 1 decimal place
|
||||
const percentageClickedDisplay = MailPoet.Num.toLocaleFixed(percentageClicked, 1);
|
||||
const percentageOpenedDisplay = MailPoet.Num.toLocaleFixed(percentageOpened, 1);
|
||||
const percentageUnsubscribedDisplay = MailPoet.Num.toLocaleFixed(percentageUnsubscribed, 1);
|
||||
const headlineOpened = `${percentageOpenedDisplay}% ${MailPoet.I18n.t('percentageOpened')}`;
|
||||
const headlineClicked = `${percentageClickedDisplay}% ${MailPoet.I18n.t('percentageClicked')}`;
|
||||
const headlineUnsubscribed = `${percentageUnsubscribedDisplay}% ${MailPoet.I18n.t('percentageUnsubscribed')}`;
|
||||
const statsKBLink = 'http://beta.docs.mailpoet.com/article/190-whats-a-good-email-open-rate';
|
||||
// thresholds to display badges
|
||||
const minNewslettersSent = 20;
|
||||
const minNewslettersOpened = 5;
|
||||
let statsContent;
|
||||
if (totalSent >= minNewslettersSent
|
||||
&& newsletter.statistics.opened >= minNewslettersOpened
|
||||
) {
|
||||
// display stats with badges
|
||||
statsContent = (
|
||||
<div className="mailpoet_stat_grey">
|
||||
<div className="mailpoet_stat_big mailpoet_stat_spaced">
|
||||
<StatsBadge
|
||||
stat="opened"
|
||||
rate={percentageOpened}
|
||||
headline={headlineOpened}
|
||||
/>
|
||||
</div>
|
||||
<div className="mailpoet_stat_big mailpoet_stat_spaced">
|
||||
<StatsBadge
|
||||
stat="clicked"
|
||||
rate={percentageClicked}
|
||||
headline={headlineClicked}
|
||||
/>
|
||||
</div>
|
||||
<RevenuesStats
|
||||
revenue={newsletter.statistics.revenue}
|
||||
/>
|
||||
<div>
|
||||
<StatsBadge
|
||||
stat="unsubscribed"
|
||||
rate={percentageUnsubscribed}
|
||||
headline={headlineUnsubscribed}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
// display stats without badges
|
||||
statsContent = (
|
||||
<div className="mailpoet_stat_grey">
|
||||
<div className="mailpoet_stat_big mailpoet_stat_spaced">
|
||||
{headlineOpened}
|
||||
</div>
|
||||
<div className="mailpoet_stat_big mailpoet_stat_spaced">
|
||||
{headlineClicked}
|
||||
</div>
|
||||
<RevenuesStats
|
||||
revenue={newsletter.statistics.revenue}
|
||||
/>
|
||||
<div>
|
||||
{headlineUnsubscribed}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p className="mailpoet_stat_grey mailpoet_stat_big">
|
||||
{MailPoet.I18n.t('statsTotalSent')}
|
||||
{' '}
|
||||
{parseInt(totalSent, 10).toLocaleString()}
|
||||
</p>
|
||||
{statsContent}
|
||||
{ newsletter.ga_campaign && (
|
||||
<p>
|
||||
{MailPoet.I18n.t('googleAnalytics')}
|
||||
:
|
||||
{ newsletter.ga_campaign }
|
||||
</p>
|
||||
) }
|
||||
<p>
|
||||
<a href={statsKBLink} target="_blank" rel="noopener noreferrer">
|
||||
{MailPoet.I18n.t('readMoreOnStats')}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
NewsletterGeneralStats.propTypes = {
|
||||
newsletter: PropTypes.shape({
|
||||
ga_campaign: PropTypes.string,
|
||||
total_sent: PropTypes.number,
|
||||
statistics: PropTypes.shape({
|
||||
clicked: PropTypes.number,
|
||||
opened: PropTypes.number,
|
||||
unsubscribed: PropTypes.number,
|
||||
revenue: PropTypes.shape({
|
||||
currency: PropTypes.string.isRequired,
|
||||
value: PropTypes.number.isRequired,
|
||||
formatted: PropTypes.string.isRequired,
|
||||
count: PropTypes.number.isRequired,
|
||||
}),
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default NewsletterGeneralStats;
|
231
assets/js/src/newsletters/campaign_stats/page.jsx
Normal file
231
assets/js/src/newsletters/campaign_stats/page.jsx
Normal file
@@ -0,0 +1,231 @@
|
||||
import MailPoet from 'mailpoet';
|
||||
import React from 'react';
|
||||
import { Link, withRouter } from 'react-router-dom';
|
||||
import ReactStringReplace from 'react-string-replace';
|
||||
import PropTypes from 'prop-types';
|
||||
import NewsletterGeneralStats from './newsletter_stats.jsx';
|
||||
import NewsletterStatsInfo from './newsletter_info.jsx';
|
||||
import ClickedLinksTable from './clicked_links_table.jsx';
|
||||
import SubscriberEngagementListing from './subscriber_engagement.jsx';
|
||||
import PurchasedProducts from './purchased_products.jsx';
|
||||
|
||||
class CampaignStatsPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
item: {},
|
||||
loading: true,
|
||||
savingSegment: false,
|
||||
segmentCreated: false,
|
||||
segmentErrors: [],
|
||||
};
|
||||
this.handleCreateSegment = this.handleCreateSegment.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { match } = this.props;
|
||||
// Scroll to top in case we're coming
|
||||
// from the middle of a long newsletter listing
|
||||
window.scrollTo(0, 0);
|
||||
this.loadItem(match.params.id);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(props) {
|
||||
const { match } = this.props;
|
||||
if (match.params.id !== props.match.params.id) {
|
||||
this.loadItem(props.match.params.id);
|
||||
}
|
||||
}
|
||||
|
||||
handleCreateSegment(group, newsletter, linkId) {
|
||||
const name = `${newsletter.subject} – ${group}`;
|
||||
this.setState({ savingSegment: true, segmentCreated: false, segmentErrors: [] });
|
||||
MailPoet.Ajax.post({
|
||||
api_version: window.mailpoet_api_version,
|
||||
endpoint: 'dynamic_segments',
|
||||
action: 'save',
|
||||
data: {
|
||||
segmentType: 'email',
|
||||
action: group === 'unopened' ? 'notOpened' : group,
|
||||
newsletter_id: newsletter.id,
|
||||
link_id: linkId,
|
||||
name,
|
||||
},
|
||||
}).always(() => {
|
||||
this.setState({ savingSegment: false });
|
||||
}).done(() => {
|
||||
this.setState({
|
||||
segmentCreated: true,
|
||||
segmentName: name,
|
||||
});
|
||||
}).fail((response) => {
|
||||
this.setState({
|
||||
segmentErrors:
|
||||
response.errors.map(error => ((error.error === 409) ? MailPoet.I18n.t('segmentExists') : error.message)),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
loadItem(id) {
|
||||
const { history } = this.props;
|
||||
this.setState({ loading: true });
|
||||
MailPoet.Modal.loading(true);
|
||||
|
||||
MailPoet.Ajax.post({
|
||||
api_version: window.mailpoet_api_version,
|
||||
endpoint: 'stats',
|
||||
action: 'get',
|
||||
data: {
|
||||
id,
|
||||
},
|
||||
}).always(() => {
|
||||
MailPoet.Modal.loading(false);
|
||||
}).done((response) => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
item: response.data,
|
||||
});
|
||||
}).fail((response) => {
|
||||
MailPoet.Notice.error(
|
||||
response.errors.map(error => error.message),
|
||||
{ scroll: true }
|
||||
);
|
||||
this.setState({
|
||||
loading: false,
|
||||
item: {},
|
||||
}, () => {
|
||||
history.push('/');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
renderCreateSegmentSuccess() {
|
||||
const { segmentCreated, segmentName } = this.state;
|
||||
let segmentCreatedSuccessMessage;
|
||||
|
||||
if (segmentCreated) {
|
||||
let message = ReactStringReplace(
|
||||
MailPoet.I18n.t('successMessage'),
|
||||
/\[link\](.*?)\[\/link\]/g,
|
||||
(match, i) => (
|
||||
<a
|
||||
key={i}
|
||||
href="?page=mailpoet-newsletters#/new"
|
||||
>
|
||||
{match}
|
||||
</a>
|
||||
)
|
||||
);
|
||||
|
||||
message = ReactStringReplace(message, '%s', () => segmentName);
|
||||
|
||||
segmentCreatedSuccessMessage = (
|
||||
<div className="mailpoet_notice notice inline notice-success">
|
||||
<p>{message}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return segmentCreatedSuccessMessage;
|
||||
}
|
||||
|
||||
renderCreateSegmentError() {
|
||||
const { segmentErrors } = this.state;
|
||||
let error;
|
||||
|
||||
if (segmentErrors.length > 0) {
|
||||
error = (
|
||||
<div>
|
||||
{segmentErrors.map(errorMessage => (
|
||||
<div className="mailpoet_notice notice inline error" key={`error-${errorMessage}`}>
|
||||
<p>{errorMessage}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { item, loading, savingSegment } = this.state;
|
||||
const newsletter = item;
|
||||
const { match, location } = this.props;
|
||||
|
||||
if (loading || !newsletter.queue) {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="title">
|
||||
{MailPoet.I18n.t('statsTitle')}
|
||||
<Link
|
||||
className="page-title-action"
|
||||
to="/"
|
||||
>
|
||||
{MailPoet.I18n.t('backToList')}
|
||||
</Link>
|
||||
</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="title">
|
||||
{`${MailPoet.I18n.t('statsTitle')}: ${newsletter.subject}`}
|
||||
<Link
|
||||
className="page-title-action"
|
||||
to="/"
|
||||
>
|
||||
{MailPoet.I18n.t('backToList')}
|
||||
</Link>
|
||||
</h1>
|
||||
|
||||
<div className="mailpoet_stat_triple-spaced">
|
||||
<div className="mailpoet_stat_info">
|
||||
<NewsletterStatsInfo newsletter={newsletter} />
|
||||
</div>
|
||||
<div className="mailpoet_stat_general">
|
||||
<NewsletterGeneralStats newsletter={newsletter} />
|
||||
</div>
|
||||
<div style={{ clear: 'both' }} />
|
||||
</div>
|
||||
|
||||
<h2>{MailPoet.I18n.t('clickedLinks')}</h2>
|
||||
|
||||
<div className="mailpoet_stat_triple-spaced">
|
||||
<ClickedLinksTable links={newsletter.clicked_links} />
|
||||
</div>
|
||||
|
||||
<div className="mailpoet_stat_triple-spaced">
|
||||
<PurchasedProducts newsletter={newsletter} />
|
||||
</div>
|
||||
|
||||
<h2>{MailPoet.I18n.t('subscriberEngagement')}</h2>
|
||||
|
||||
{this.renderCreateSegmentSuccess()}
|
||||
{this.renderCreateSegmentError()}
|
||||
|
||||
<SubscriberEngagementListing
|
||||
location={location}
|
||||
params={match.params}
|
||||
newsletter={newsletter}
|
||||
handleCreateSegment={this.handleCreateSegment}
|
||||
savingSegment={savingSegment}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CampaignStatsPage.propTypes = {
|
||||
match: PropTypes.shape({
|
||||
params: PropTypes.object.isRequired,
|
||||
}).isRequired,
|
||||
location: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
history: PropTypes.shape({
|
||||
push: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default withRouter(CampaignStatsPage);
|
Reference in New Issue
Block a user