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 @@ +thanks for subscribiCreated with Sketch. 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" /> + +