diff --git a/assets/js/src/newsletters/campaign_stats/newsletter_info.jsx b/assets/js/src/newsletters/campaign_stats/newsletter_info.jsx new file mode 100644 index 0000000000..0fc4c2a38d --- /dev/null +++ b/assets/js/src/newsletters/campaign_stats/newsletter_info.jsx @@ -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 ( +
+
+ + {MailPoet.I18n.t('statsPreviewNewsletter')} + +
+ +

+ {MailPoet.I18n.t('statsDateSent')} +: + {' '} + {MailPoet.Date.format(newsletterDate)} +

+ + { segments && ( +

+ {MailPoet.I18n.t('statsToSegments')} +: + {' '} + { segments } +

+ ) } + +

+ {MailPoet.I18n.t('statsFromAddress')} +: + {' '} + { senderAddress } +

+ + {replyToAddress && ( +

+ {MailPoet.I18n.t('statsReplyToAddress')} +: + {' '} + { replyToAddress } +

+ ) } +
+ ); +} + +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; diff --git a/assets/js/src/newsletters/campaign_stats/newsletter_stats.jsx b/assets/js/src/newsletters/campaign_stats/newsletter_stats.jsx new file mode 100644 index 0000000000..50230e896b --- /dev/null +++ b/assets/js/src/newsletters/campaign_stats/newsletter_stats.jsx @@ -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 = ( +
+
+ +
+
+ +
+ +
+ +
+
+ ); + } else { + // display stats without badges + statsContent = ( +
+
+ {headlineOpened} +
+
+ {headlineClicked} +
+ +
+ {headlineUnsubscribed} +
+
+ ); + } + + return ( +
+

+ {MailPoet.I18n.t('statsTotalSent')} + {' '} + {parseInt(totalSent, 10).toLocaleString()} +

+ {statsContent} + { newsletter.ga_campaign && ( +

+ {MailPoet.I18n.t('googleAnalytics')} + : + { newsletter.ga_campaign } +

+ ) } +

+ + {MailPoet.I18n.t('readMoreOnStats')} + +

+
+ ); +}; + +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; diff --git a/assets/js/src/newsletters/campaign_stats/page.jsx b/assets/js/src/newsletters/campaign_stats/page.jsx new file mode 100644 index 0000000000..14a71f3f94 --- /dev/null +++ b/assets/js/src/newsletters/campaign_stats/page.jsx @@ -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) => ( + + {match} + + ) + ); + + message = ReactStringReplace(message, '%s', () => segmentName); + + segmentCreatedSuccessMessage = ( +
+

{message}

+
+ ); + } + + return segmentCreatedSuccessMessage; + } + + renderCreateSegmentError() { + const { segmentErrors } = this.state; + let error; + + if (segmentErrors.length > 0) { + error = ( +
+ {segmentErrors.map(errorMessage => ( +
+

{errorMessage}

+
+ ))} +
+ ); + } + + return error; + } + + render() { + const { item, loading, savingSegment } = this.state; + const newsletter = item; + const { match, location } = this.props; + + if (loading || !newsletter.queue) { + return ( +
+

+ {MailPoet.I18n.t('statsTitle')} + + {MailPoet.I18n.t('backToList')} + +

+
+ ); + } + + return ( +
+

+ {`${MailPoet.I18n.t('statsTitle')}: ${newsletter.subject}`} + + {MailPoet.I18n.t('backToList')} + +

+ +
+
+ +
+
+ +
+
+
+ +

{MailPoet.I18n.t('clickedLinks')}

+ +
+ +
+ +
+ +
+ +

{MailPoet.I18n.t('subscriberEngagement')}

+ + {this.renderCreateSegmentSuccess()} + {this.renderCreateSegmentError()} + + +
+ ); + } +} + +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);