Move statistics page layout from premium to free

[MAILPOET-2104]
This commit is contained in:
Ján Mikláš
2019-06-25 09:53:48 +02:00
committed by M. Shull
parent 0cea017f96
commit b33f31ebba
3 changed files with 446 additions and 0 deletions

View 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;

View 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;

View 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);