diff --git a/r2/r2/controllers/api.py b/r2/r2/controllers/api.py
index 74cc26915..851c61fc5 100644
--- a/r2/r2/controllers/api.py
+++ b/r2/r2/controllers/api.py
@@ -296,6 +296,23 @@ class ApiController(RedditController):
# Pylons does not handle 204s correctly.
return {}
+ @json_validate(
+ VModhashIfLoggedIn(),
+ VRatelimit(rate_ip=True, prefix="rate_newsletter_"),
+ email=ValidEmail("email"),
+ )
+ def POST_newsletter(self, responder, email):
+ """Add an email to our newsletter."""
+
+ VRatelimit.ratelimit(rate_ip=True,
+ prefix="rate_newsletter_")
+
+ try:
+ newsletter.add_subscriber(email, source="newsletterbar")
+ except newsletter.NewsletterError as e:
+ g.log.warning("Failed to subscribe: %r" % e)
+ abort(500)
+
@allow_oauth2_access
@json_validate()
@api_doc(api_section.captcha)
diff --git a/r2/r2/lib/js.py b/r2/r2/lib/js.py
index c6f8fff6e..c2c23dd9d 100644
--- a/r2/r2/lib/js.py
+++ b/r2/r2/lib/js.py
@@ -496,6 +496,7 @@ module["reddit"] = LocalizedModule("reddit.js",
"ui.js",
"popup.js",
"login.js",
+ "newsletter.js",
"flair.js",
"interestbar.js",
"visited.js",
diff --git a/r2/r2/lib/pages/pages.py b/r2/r2/lib/pages/pages.py
index 55b0bdb4b..a24587d6e 100644
--- a/r2/r2/lib/pages/pages.py
+++ b/r2/r2/lib/pages/pages.py
@@ -262,6 +262,7 @@ class Reddit(Templated):
#add the infobar
self.welcomebar = None
+ self.newsletterbar = None
self.locationbar = None
self.infobar = None
# generate a canonical link for google
@@ -304,6 +305,8 @@ class Reddit(Templated):
if not c.user_is_loggedin:
self.welcomebar = WelcomeBar()
+ if feature.is_enabled('newsletter') and getattr(self, "show_newsletterbar", True):
+ self.newsletterbar = NewsletterBar()
show_locationbar &= not c.user.pref_hide_locationbar
if (show_locationbar and c.used_localized_defaults and
@@ -824,9 +827,17 @@ class Reddit(Templated):
def content(self):
"""returns a Wrapped (or renderable) item for the main content div."""
+ if self.newsletterbar:
+ self.welcomebar = None
+
return self.content_stack((
- self.welcomebar, self.infobar, self.locationbar, self.nav_menu,
- self._content))
+ self.welcomebar,
+ self.newsletterbar,
+ self.infobar,
+ self.locationbar,
+ self.nav_menu,
+ self._content,
+ ))
def is_gold_page(self):
return "gold-page-ga-tracking" in self.supplied_page_classes
@@ -2294,6 +2305,9 @@ class WelcomeBar(InfoBar):
_("where your votes shape what the world is talking about."))
InfoBar.__init__(self, message=message)
+class NewsletterBar(InfoBar):
+ pass
+
class ClientInfoBar(InfoBar):
"""Draws the message the top of a login page before OAuth2 authorization"""
def __init__(self, client, *args, **kwargs):
diff --git a/r2/r2/public/static/css/components/buttons.less b/r2/r2/public/static/css/components/buttons.less
index 38202c8b7..e10d75587 100644
--- a/r2/r2/public/static/css/components/buttons.less
+++ b/r2/r2/public/static/css/components/buttons.less
@@ -39,3 +39,7 @@
.c-btn-primary {
.button-variant(@text-color: #fff; @bg-color: #4f86b5; @bevel-color: #4270a2);
}
+
+.c-btn-highlight {
+ .button-variant(@text-color: #fff; @bg-color: #DC6431; @bevel-color: #C9532B);
+}
diff --git a/r2/r2/public/static/css/components/utils.less b/r2/r2/public/static/css/components/utils.less
index 1a61b4644..25b7704d1 100644
--- a/r2/r2/public/static/css/components/utils.less
+++ b/r2/r2/public/static/css/components/utils.less
@@ -1,3 +1,7 @@
+.c-hidden {
+ display: none;
+}
+
.c-clearfix {
.clearfix();
}
diff --git a/r2/r2/public/static/css/reddit.less b/r2/r2/public/static/css/reddit.less
index 20ee79889..5a30890d2 100644
--- a/r2/r2/public/static/css/reddit.less
+++ b/r2/r2/public/static/css/reddit.less
@@ -126,6 +126,9 @@ h3 { font-size:110%; /*text-transform:uppercase;*/ }
a img { border: 0 none; }
a { text-decoration: none; color: #369; }
+/* Polyfill for HTML5 hidden attribute: http://caniuse.com/#feat=hidden */
+[hidden] { display: none; }
+
/*
a:active { border: 0 none;}
a:focus { -moz-outline-style: none; }
@@ -1847,6 +1850,104 @@ body.with-listing-chooser.explore-page #header .pagename {
border-bottom: 1px solid #a73a11;
}
+.infobar.newsletterbar {
+ .box-sizing(border-box);
+ position: relative;
+ overflow: hidden;
+ min-height: 80px;
+ padding: 15px 20px 20px;
+ border: none;
+ border-radius: 2px;
+ background-color: #30659B;
+
+ header {
+ float: left;
+ height: 45px;
+ width: 325px;
+ }
+
+ a.newsletter-close {
+ position: absolute;
+ right: 3px;
+ top: 0;
+ font-size: 11px;
+ color: #CCC;
+ }
+
+ form {
+ margin-left: 340px;
+ margin-right: 150px;
+ max-width: 400px;
+ min-width: 150px;
+ line-height: 45px;
+ white-space: nowrap;
+ }
+
+ &.success {
+ header {
+ padding-left: 65px;
+
+ &:before {
+ content: "✓";
+ color: #80d654;
+ font-weight: bold;
+ font-size: 60px;
+ position: absolute;
+ top: 0;
+ left: 15px;
+ }
+ }
+ }
+
+ h1 {
+ margin: 0;
+
+ a:hover {
+ border-bottom: 1px dotted #999;
+ }
+ }
+
+ h2 {
+ color: white;
+ font-weight: normal;
+ font-size: 14px;
+ }
+
+ .c-form-group {
+ display: inline-block;
+ width: 100%;
+ }
+
+ /* Display error-feedback indicator inside the input */
+ .c-form-control-feedback-wrapper {
+ margin-left: -30px;
+ top: 5px;
+ }
+
+ input[type="email"] {
+ display: inline-block;
+ }
+
+ button {
+ .button-size(@padding-base-vertical; @padding-base-horizontal; 12px; 20px; 3px);
+ margin-left: 10px;
+ }
+
+ @media screen and (max-width: @screen-md-min) {
+ header {
+ float: none;
+ }
+
+ form {
+ margin: 10px 0 0;
+ }
+
+ .c-form-group {
+ max-width: 50%;
+ }
+ }
+}
+
.locationbar {
margin: 5px;
diff --git a/r2/r2/public/static/js/base.js b/r2/r2/public/static/js/base.js
index 69b6fd063..cef95a7b7 100644
--- a/r2/r2/public/static/js/base.js
+++ b/r2/r2/public/static/js/base.js
@@ -124,6 +124,7 @@ $(function() {
r.saved.init()
r.messages.init()
r.filter.init()
+ r.newsletter.ui.init()
} catch (err) {
r.sendError('Error during base.js init', err)
}
diff --git a/r2/r2/public/static/js/newsletter.js b/r2/r2/public/static/js/newsletter.js
new file mode 100644
index 000000000..38584568e
--- /dev/null
+++ b/r2/r2/public/static/js/newsletter.js
@@ -0,0 +1,84 @@
+r.newsletter = {
+ post: function(form) {
+ var email = $('input[name="email"]', form.$el).val();
+ var apiTarget = form.$el.attr('action');
+
+ var params = form.serialize();
+ params.push({name:'api_type', value:'json'});
+
+ return r.ajax({
+ url: apiTarget,
+ type: 'POST',
+ dataType: 'json',
+ data: params,
+ xhrFields: {
+ withCredentials: true
+ }
+ });
+ }
+};
+
+r.newsletter.ui = {
+ init: function() {
+ var newsletterBarSeen = !!store.get('newsletterbar.seen');
+
+ if (newsletterBarSeen || $('.newsletterbar').length === 0) {
+ return;
+ }
+
+ $('.newsletterbar').show();
+
+ $('.newsletter-signup').each(function(i, el) {
+ new r.newsletter.ui.NewsletterForm(el)
+ })
+
+ $('.newsletter-close').on('click', function() {
+ $('.newsletterbar').addClass('c-hidden');
+ });
+
+ store.set('newsletterbar.seen', true);
+ },
+};
+
+r.newsletter.ui.NewsletterForm = function() {
+ r.ui.Form.apply(this, arguments)
+};
+
+r.newsletter.ui.NewsletterForm.prototype = $.extend(new r.ui.Form(), {
+ showStatus: function() {
+ this.$el.find('.error').css('opacity', 1)
+ r.ui.Form.prototype.showStatus.apply(this, arguments)
+ },
+
+ _submit: function() {
+ r.analytics.fireGAEvent('newsletter-form', 'submit');
+ return r.newsletter.post(this);
+ },
+
+ _showSuccess: function() {
+ var parentEl = this.$el.parents('.newsletterbar');
+ parentEl.find('.result-message').text(r._('you\'ll get your first newsletter soon'));
+ parentEl.addClass('success');
+ parentEl.find('header').fadeTo(250, 1);
+ },
+
+ _handleResult: function(result) {
+ if (result.json.errors.length) {
+ r.ui.Form.prototype._handleResult.call(this, result);
+ }
+
+ var parentEl = this.$el.parents('.newsletterbar');
+ var calloutImg = parentEl.find('.subscribe-callout img');
+ var thanksImg = $('
').attr('src', calloutImg.data('thanks-src'))
+ .attr('alt', r._('thanks for subscribing'));
+
+ parentEl.find('header, form').fadeTo(250, 0, function() {
+ calloutImg.hide().after(thanksImg);
+ if (thanksImg.get(0).complete) {
+ this._showSuccess();
+ } else {
+ thanksImg.one("load", this._showSuccess);
+ }
+ }.bind(this));
+ }
+})
diff --git a/r2/r2/public/static/js/ui.js b/r2/r2/public/static/js/ui.js
index da6c40a2b..6c923f956 100644
--- a/r2/r2/public/static/js/ui.js
+++ b/r2/r2/public/static/js/ui.js
@@ -255,9 +255,13 @@ r.ui.Form = function(el) {
$(this).stateify('set', 'success');
})
.on('invalid.validator', function(e, resp) {
- var error = r.utils.parseError(resp.errors[0]);
+ // resp may not always be set if client side validation triggered, like
+ // from input type=email
+ if (resp) {
+ var error = r.utils.parseError(resp.errors[0]);
- $(this).stateify('set', 'error', error.message);
+ $(this).stateify('set', 'error', error.message);
+ }
})
.on('loading.validator', function(e) {
$(this).stateify('set', 'loading');
diff --git a/r2/r2/public/static/subscribe-header-thanks.svg b/r2/r2/public/static/subscribe-header-thanks.svg
new file mode 100644
index 000000000..52bbb32ec
--- /dev/null
+++ b/r2/r2/public/static/subscribe-header-thanks.svg
@@ -0,0 +1 @@
+
diff --git a/r2/r2/public/static/subscribe-header.svg b/r2/r2/public/static/subscribe-header.svg
new file mode 100644
index 000000000..785a2e897
--- /dev/null
+++ b/r2/r2/public/static/subscribe-header.svg
@@ -0,0 +1 @@
+
diff --git a/r2/r2/templates/newsletterbar.html b/r2/r2/templates/newsletterbar.html
new file mode 100644
index 000000000..cf2569f60
--- /dev/null
+++ b/r2/r2/templates/newsletterbar.html
@@ -0,0 +1,50 @@
+## The contents of this file are subject to the Common Public Attribution
+## License Version 1.0. (the "License"); you may not use this file except in
+## compliance with the License. You may obtain a copy of the License at
+## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
+## License Version 1.1, but Sections 14 and 15 have been added to cover use of
+## software over a computer network and provide for limited attribution for the
+## Original Developer. In addition, Exhibit A has been modified to be
+## consistent with Exhibit B.
+##
+## Software distributed under the License is distributed on an "AS IS" basis,
+## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
+## the specific language governing rights and limitations under the License.
+##
+## The Original Code is reddit.
+##
+## The Original Developer is the Initial Developer. The Initial Developer of
+## the Original Code is reddit Inc.
+##
+## All portions of the code written by reddit are Copyright (c) 2006-2015
+## reddit Inc. All Rights Reserved.
+###############################################################################
+
+<%!
+ from r2.lib.template_helpers import static
+%>
+<%namespace file="utils.html" import="form_group" />
+
+
+
+
+
+
+ ${_('get the best of reddit, delivered once a week')}
+
+
+ ×
+