diff --git a/_includes/made-with-love.html b/_includes/made-with-love.html index ca9d155..c9e30a0 100644 --- a/_includes/made-with-love.html +++ b/_includes/made-with-love.html @@ -32,17 +32,58 @@ } - @v [#astro-tom]-[#prefinery_iframe_inline] - gap(200); + #grid-section { + @virtual "form"; + @h |~-~["form"]~-~| outer-gap(60) in(::); + @v [.astro-person]~200~["form"]-150-[#pp-link]-| + outer-gap([baseline]) + in(::); - @v [#prefinery_iframe_inline]-[#pp-link]-| - in(#grid-section) - gap([baseline]); - #prefinery_iframe_inline { - height: == ::[intrinsic-height]; + #signup-form-error-message, + #signup-form-success-message { + height: == ::[intrinsic-height]; + } + + @v |[.alert]-[#signup-form-input-field] + gap(20) + in("form") + chain-left("form"[left]) + chain-right("form"[right]); + + + #signup-form-input-field[height] == #signup-form-submit-button[height] == 60; + + + @if ::window[width] < 720 { + @v [#signup-form-input-field]-[#signup-form-submit-button]| + gap(20) + in("form") + chain-left("form"[left]) + chain-right("form"[right]); + } + @else { + "form"[width] == #grid-section-content[width]; + "form"[center-x] == ::[center-x]; + + @h |[#signup-form-input-field]-[#signup-form-submit-button]| + gap(20) + in("form") + chain-bottom("form"[bottom]); + + #signup-form-submit-button { + /*width: == ::[intrinsic-width];*/ + + /* + Intrinsic width is applied regardless + of how the conditional is evaluated + */ + width: == 260; + } + } } + #pp-link { text-align: center; size: == ::[intrinsic-size] !require; @@ -107,7 +148,12 @@

Open source b/c we believe

- +
+

Your request could not be fulfilled. Please try again.

+

Thanks!

+ + +
privacy policy diff --git a/_includes/tail.html b/_includes/tail.html index eda9dd1..a354340 100644 --- a/_includes/tail.html +++ b/_includes/tail.html @@ -1,5 +1,53 @@ {% include navigation.html %} - + + + + + + + + + + + + + + + + + + + + + + diff --git a/bower_components/signup-form/spec/signup-form.coffee b/bower_components/signup-form/spec/signup-form.coffee new file mode 100644 index 0000000..d7f777e --- /dev/null +++ b/bower_components/signup-form/spec/signup-form.coffee @@ -0,0 +1,439 @@ +{expect} = chai +SignupForm = require 'signup-form' + + +describe 'Signup form', -> + + afterEach -> + $('#dynamic').empty() + + + describe 'options', -> + + context 'when not specified', -> + + it 'should set a parent selector', -> + signupForm = new SignupForm + betaId: 1234 + + expect(signupForm.parentSelector).to.exist + + + context 'when specified', -> + + it 'should set the parent selector', -> + parentSelector = '#dynamic' + + signupForm = new SignupForm + betaId: 1234 + parentSelector: parentSelector + + expect(signupForm.parentSelector).to.equal parentSelector + + + describe 'callbacks', -> + + context 'when success is true', -> + + it 'should be called', (done) -> + signupForm = new SignupForm + betaId: 1234 + onSuccess: (emailAddress) -> + done() + + signupForm.viewModel.success.onNext true + + + it 'should receive the email address', (done) -> + signupForm = new SignupForm + betaId: 1234 + onSuccess: (emailAddress) -> + try + expect(emailAddress).to.equal signupForm.viewModel.emailAddress.value + done() + catch e + done e + + signupForm.viewModel.success.onNext true + + + context 'when genericError is true', -> + + it 'should be called', (done) -> + signupForm = new SignupForm + betaId: 1234 + onError: (emailAddress) -> + done() + + signupForm.viewModel.genericError.onNext true + + + context 'when invalidEmailError is true', -> + + it 'should be called', (done) -> + signupForm = new SignupForm + betaId: 1234 + onError: (emailAddress) -> + done() + + signupForm.viewModel.invalidEmailError.onNext true + + + describe 'template', -> + + context 'by default', -> + + it 'should be rendered', -> + signupForm = new SignupForm + betaId: 1234 + parentSelector: '#dynamic' + + expect(signupForm.template).to.exist + + + it 'should be added to the DOM', -> + signupForm = new SignupForm + betaId: 1234 + parentSelector: '#dynamic' + + $parent = $(signupForm.parentSelector) + $form = $("##{signupForm.view.id}") + parentContainsForm = $.contains $parent[0], $form[0] + + expect(parentContainsForm).to.be.true + + + context 'otherwise', -> + + it 'should not be rendered', -> + signupForm = new SignupForm + betaId: 1234 + id: 'static-signup-form' + parentSelector: '#static' + useTemplate: false + + expect(signupForm.template).to.not.exist + + + it 'should not be added to the DOM', -> + parentSelector = '#static' + + getHtml = -> $(parentSelector).html() + html = getHtml() + + signupForm = new SignupForm + betaId: 1234 + id: 'static-signup-form' + parentSelector: parentSelector + useTemplate: false + + expect(html).to.equal getHtml() + + + describe 'element getters', -> + + context 'form', -> + it 'should exist', -> + signupForm = new SignupForm + betaId: 1234 + parentSelector: '#dynamic' + + expect(signupForm.view.$form()[0]).to.exist + + + context 'input field', -> + it 'should exist', -> + signupForm = new SignupForm + betaId: 1234 + parentSelector: '#dynamic' + + expect(signupForm.view.$inputField()[0]).to.exist + + + context 'submit button', -> + it 'should exist', -> + signupForm = new SignupForm + betaId: 1234 + parentSelector: '#dynamic' + + expect(signupForm.view.$submitButton()[0]).to.exist + + + context 'error message', -> + it 'should exist', -> + signupForm = new SignupForm + betaId: 1234 + parentSelector: '#dynamic' + + expect(signupForm.view.$errorMessage()[0]).to.exist + + + context 'success message', -> + it 'should exist', -> + signupForm = new SignupForm + betaId: 1234 + parentSelector: '#dynamic' + + expect(signupForm.view.$successMessage()[0]).to.exist + + + describe 'view model', -> + + context 'options', -> + + it 'should set the beta ID', -> + betaId = 1234 + + signupForm = new SignupForm + betaId: betaId + parentSelector: '#dynamic' + + viewModel = signupForm.viewModel + expect(viewModel.betaId).to.equal betaId + + + describe 'view', -> + + context 'options', -> + + it 'should set the ID', -> + id = 'test-id' + + signupForm = new SignupForm + betaId: 1234 + parentSelector: '#dynamic' + id: id + + view = signupForm.view + expect(view.id).to.equal id + + + it 'should set the button text', -> + buttonText = 'test button text' + + signupForm = new SignupForm + betaId: 1234 + parentSelector: '#dynamic' + buttonText: buttonText + + view = signupForm.view + expect(view.buttonText).to.equal buttonText + + + it 'should set the placeholder text', -> + placeholderText = 'test placeholder text' + + signupForm = new SignupForm + betaId: 1234 + parentSelector: '#dynamic' + placeholderText: placeholderText + + view = signupForm.view + expect(view.placeholderText).to.equal placeholderText + + + it 'should set the generic error message', -> + errorMessage = 'test generic error message' + + signupForm = new SignupForm + betaId: 1234 + parentSelector: '#dynamic' + errorMessages: + generic: errorMessage + + view = signupForm.view + expect(view.errorMessages.generic).to.equal errorMessage + + + it 'should set the invalid email address error message', -> + errorMessage = 'test error message' + + signupForm = new SignupForm + betaId: 1234 + parentSelector: '#dynamic' + errorMessages: + invalidEmail: errorMessage + + view = signupForm.view + expect(view.errorMessages.invalidEmail).to.equal errorMessage + + + it 'should set the success message', -> + successMessage = 'test success message' + + signupForm = new SignupForm + betaId: 1234 + parentSelector: '#dynamic' + successMessage: successMessage + + view = signupForm.view + expect(view.successMessage).to.equal successMessage + + + describe 'submit button', -> + + context 'when email address is valid', -> + + it 'should be enabled', -> + signupForm = new SignupForm + betaId: 1234 + parentSelector: '#dynamic' + + $submitButton = signupForm.view.$submitButton() + + $submitButton.prop 'disabled', true + signupForm.viewModel.emailAddressIsValid.onNext true + + expect($submitButton.is(':disabled')).to.be.false + + + context 'when email address is invalid', -> + + it 'should be disabled', -> + signupForm = new SignupForm + betaId: 1234 + parentSelector: '#dynamic' + + $submitButton = signupForm.view.$submitButton() + + $submitButton.prop 'disabled', false + signupForm.viewModel.emailAddressIsValid.onNext false + + expect($submitButton.is(':disabled')).to.be.true + + + describe 'success message', -> + + context 'when success is true', -> + it 'should be visible', -> + signupForm = new SignupForm + betaId: 1234 + parentSelector: '#dynamic' + + $successMessage = signupForm.view.$successMessage() + + $successMessage.addClass 'hidden' + signupForm.viewModel.success.onNext true + + expect($successMessage.hasClass('hidden')).to.be.false + + + context 'when success is false', -> + it 'should not be visible', -> + signupForm = new SignupForm + betaId: 1234 + parentSelector: '#dynamic' + + $successMessage = signupForm.view.$successMessage() + + $successMessage.removeClass 'hidden' + signupForm.viewModel.success.onNext false + + expect($successMessage.hasClass('hidden')).to.be.true + + + describe 'error message', -> + + it 'should initially contain the longest error message to workaround GSS and volatile DOM', -> + longErrorMessage = 'test long error message' + + signupForm1 = new SignupForm + betaId: 1234 + parentSelector: '#dynamic' + errorMessages: + generic: longErrorMessage + invalidEmail: 'test' + + expect(signupForm1.view.$errorMessage().text()).to.equal longErrorMessage + + + signupForm2 = new SignupForm + betaId: 1234 + parentSelector: '#dynamic' + errorMessages: + generic: 'test' + invalidEmail: longErrorMessage + + expect(signupForm2.view.$errorMessage().text()).to.equal longErrorMessage + + + context 'when genericError is true', -> + + it 'should be visible', -> + signupForm = new SignupForm + betaId: 1234 + parentSelector: '#dynamic' + + $errorMessage = signupForm.view.$errorMessage() + + $errorMessage.addClass 'hidden' + signupForm.viewModel.genericError.onNext true + + expect($errorMessage.hasClass('hidden')).to.be.false + + + it 'should contain the generic error message', -> + signupForm = new SignupForm + betaId: 1234 + parentSelector: '#dynamic' + + signupForm.view.$errorMessage().text signupForm.view.errorMessages.invalidEmailError + signupForm.viewModel.genericError.onNext true + + expect(signupForm.view.$errorMessage().text()).to.equal signupForm.view.errorMessages.generic + + + context 'when genericError is false', -> + + it 'should not be visible', -> + signupForm = new SignupForm + betaId: 1234 + parentSelector: '#dynamic' + + $errorMessage = signupForm.view.$errorMessage() + + $errorMessage.removeClass 'hidden' + signupForm.viewModel.genericError.onNext false + + expect($errorMessage.hasClass('hidden')).to.be.true + + + context 'when invalidEmailError is true', -> + + it 'should be visible', -> + signupForm = new SignupForm + betaId: 1234 + parentSelector: '#dynamic' + + $errorMessage = signupForm.view.$errorMessage() + + $errorMessage.addClass 'hidden' + signupForm.viewModel.invalidEmailError.onNext true + + expect($errorMessage.hasClass('hidden')).to.be.false + + + it 'should contain the invalid email address error message', -> + signupForm = new SignupForm + betaId: 1234 + parentSelector: '#dynamic' + + signupForm.view.$errorMessage().text signupForm.view.errorMessages.genericError + signupForm.viewModel.invalidEmailError.onNext true + + expect(signupForm.view.$errorMessage().text()).to.equal signupForm.view.errorMessages.invalidEmail + + + context 'when invalidEmailError is false', -> + + it 'should not be visible', -> + signupForm = new SignupForm + betaId: 1234 + parentSelector: '#dynamic' + + $errorMessage = signupForm.view.$errorMessage() + + $errorMessage.removeClass 'hidden' + signupForm.viewModel.invalidEmailError.onNext false + + expect($errorMessage.hasClass('hidden')).to.be.true diff --git a/bower_components/signup-form/spec/view-model.coffee b/bower_components/signup-form/spec/view-model.coffee new file mode 100644 index 0000000..eb8aa94 --- /dev/null +++ b/bower_components/signup-form/spec/view-model.coffee @@ -0,0 +1,340 @@ +{expect} = chai +ViewModel = require 'signup-form-view-model' + + +describe 'View model', -> + + viewModel = null + + + beforeEach -> + viewModel = new ViewModel + betaId: Math.floor Math.random() + + + describe 'email address', -> + + context 'when valid', -> + + it 'validity should be true', (done) -> + viewModel.emailAddressIsValid.onNext false + viewModel.emailAddress.onNext 'email@address.com' + + viewModel.emailAddressIsValid.subscribe (isValid) -> + try + expect(isValid).to.be.true + done() + catch e + done e + + + context 'when invalid', -> + + it 'validity should be false', (done) -> + viewModel.emailAddressIsValid.onNext true + viewModel.emailAddress.onNext 'emailaddress.com' + + viewModel.emailAddressIsValid.subscribe (isValid) -> + try + expect(isValid).to.be.false + done() + catch e + done e + + + describe 'genericError', -> + + context 'when success is true', -> + + it 'should be false', (done) -> + viewModel.genericError.onNext true + viewModel.success.onNext true + + viewModel.genericError.subscribe (genericError) -> + try + expect(genericError).to.be.false + done() + catch e + done e + + + describe 'invalidEmailError', -> + + context 'when success is true', -> + + it 'should be false', (done) -> + viewModel.invalidEmailError.onNext true + viewModel.success.onNext true + + viewModel.invalidEmailError.subscribe (invalidEmailError) -> + try + expect(invalidEmailError).to.be.false + done() + catch e + done e + + + describe 'success', -> + + context 'when genericError is true', -> + + it 'should be false', (done) -> + viewModel.success.onNext true + viewModel.genericError.onNext true + + viewModel.success.subscribe (success) -> + try + expect(success).to.be.false + done() + catch e + done e + + + context 'when invalidEmailError is true', -> + + it 'should be false', (done) -> + viewModel.success.onNext true + viewModel.invalidEmailError.onNext true + + viewModel.success.subscribe (success) -> + try + expect(success).to.be.false + done() + catch e + done e + + + describe 'submit command', -> + + context 'when executed', -> + + it 'should make an ajax request', -> + stub = sinon.stub $, 'ajax', (options) -> + return new $.Deferred() + + viewModel.executeSubmitCommand() + stub.restore() + expect(stub.calledOnce).to.be.true + + + it 'should make a POST request', -> + stub = sinon.stub $, 'ajax', (options) -> + return new $.Deferred() + + viewModel.executeSubmitCommand() + stub.restore() + expect(stub.firstCall.args[0].type).to.equal 'POST' + + + it.skip 'should make a request to the "testers" resource url', -> + stub = sinon.stub $, 'ajax', (options) -> + return new $.Deferred() + + viewModel.executeSubmitCommand() + stub.restore() + + HOST = 'https://the-grid-prefinery.herokuapp.com' + url = "#{HOST}/api/v2/betas/#{viewModel.betaId}/testers" + expect(stub.firstCall.args[0].url).to.equal url + + + it 'should send an email address as payload', -> + stub = sinon.stub $, 'ajax', (options) -> + return new $.Deferred() + + viewModel.executeSubmitCommand() + stub.restore() + + data = JSON.stringify + tester: + email: viewModel.emailAddress.value + + expect(stub.firstCall.args[0].data).to.equal data + + + it 'should send the content type as JSON with character set as UTF-8', -> + stub = sinon.stub $, 'ajax', (options) -> + return new $.Deferred() + + viewModel.executeSubmitCommand() + stub.restore() + + contentType = 'application/json; charset=utf-8' + expect(stub.firstCall.args[0].contentType).to.equal contentType + + + it 'should expect the response to be JSON', -> + stub = sinon.stub $, 'ajax', (options) -> + return new $.Deferred() + + viewModel.executeSubmitCommand() + stub.restore() + expect(stub.firstCall.args[0].dataType).to.equal 'json' + + + it 'should cancel a previous request', -> + stub = sinon.stub $, 'ajax', (options) -> + fakeJqXHR = new $.Deferred() + fakeJqXHR.abort = -> + return fakeJqXHR + + viewModel.executeSubmitCommand() + spy = sinon.spy stub.firstCall.returnValue, 'abort' + + viewModel.executeSubmitCommand() + + stub.restore() + spy.restore() + + expect(spy.calledOnce).to.be.true + + + it 'should set success to false', (done) -> + viewModel.success.onNext true + + stub = sinon.stub $, 'ajax', (options) -> + return new $.Deferred() + + viewModel.executeSubmitCommand() + stub.restore() + + viewModel.success.subscribe (success) -> + try + expect(success).to.be.false + done() + catch e + done e + + + it 'should set genericError to false', (done) -> + viewModel.genericError.onNext true + + stub = sinon.stub $, 'ajax', (options) -> + return new $.Deferred() + + viewModel.executeSubmitCommand() + stub.restore() + + viewModel.genericError.subscribe (genericError) -> + try + expect(genericError).to.be.false + done() + catch e + done e + + + it 'should set invalidEmailError to false', (done) -> + viewModel.invalidEmailError.onNext true + + stub = sinon.stub $, 'ajax', (options) -> + return new $.Deferred() + + viewModel.executeSubmitCommand() + stub.restore() + + viewModel.invalidEmailError.subscribe (invalidEmailError) -> + try + expect(invalidEmailError).to.be.false + done() + catch e + done e + + + context 'when receiving a success response', -> + + it 'should set success to true', (done) -> + viewModel.success.onNext false + + stub = sinon.stub $, 'ajax', (options) -> + return new $.Deferred() + + viewModel.executeSubmitCommand() + stub.restore() + + stub.firstCall.returnValue.resolve + status: 200 + + viewModel.success.subscribe (success) -> + try + expect(success).to.be.true + done() + catch e + done e + + + context 'when receiving a generic error response', -> + + it 'should set genericError to true', (done) -> + viewModel.genericError.onNext false + + stub = sinon.stub $, 'ajax', (options) -> + return new $.Deferred() + + viewModel.executeSubmitCommand() + stub.restore() + + stub.firstCall.returnValue.reject + status: 500 + + viewModel.genericError.subscribe (genericError) -> + try + expect(genericError).to.be.true + done() + catch e + done e + + + context 'when receiving an "invalid email" error response', -> + + it 'should set invalidEmailError to true', (done) -> + stub = sinon.stub $, 'ajax', (options) -> + return new $.Deferred() + + viewModel.executeSubmitCommand() + stub.restore() + + stub.firstCall.returnValue.reject + status: 422 + responseJSON: + errors: [ + { + code: 2301 + message: 'is not a valid email address' + } + ] + + viewModel.invalidEmailError.subscribe (invalidEmailError) -> + try + expect(invalidEmailError).to.be.true + done() + catch e + done e + + + context 'when receiving a "tester exists" error response', -> + + it 'should set success to true', (done) -> + viewModel.success.onNext false + + stub = sinon.stub $, 'ajax', (options) -> + return new $.Deferred() + + viewModel.executeSubmitCommand() + stub.restore() + + stub.firstCall.returnValue.reject + status: 422 + responseJSON: + errors: [ + { + code: 2310 + message: 'Tester exists. Please use the update method.' + } + ] + + viewModel.success.subscribe (success) -> + try + expect(success).to.be.true + done() + catch e + done e diff --git a/bower_components/signup-form/spec/view.coffee b/bower_components/signup-form/spec/view.coffee new file mode 100644 index 0000000..9981be0 --- /dev/null +++ b/bower_components/signup-form/spec/view.coffee @@ -0,0 +1,96 @@ +{expect} = chai +View = require 'signup-form-view' + + +describe 'View', -> + + describe 'options', -> + + context 'when not specified', -> + + it 'should set an ID', -> + view = new View + expect(view.id).to.exist + + + it 'should set button text', -> + view = new View + expect(view.buttonText).to.exist + + + it 'should set placeholder text', -> + view = new View + expect(view.placeholderText).to.exist + + + it 'should set an error message for generic errors', -> + view = new View + expect(view.errorMessages.generic).to.exist + + + it 'should set a error message for invalid email addresses', -> + view = new View + expect(view.errorMessages.invalidEmail).to.exist + + + it 'should set a success message', -> + view = new View + expect(view.successMessage).to.exist + + + context 'when specified', -> + + it 'should set the ID', -> + id = 'test-id' + + view = new View + id: id + + expect(view.id).to.equal id + + + it 'should set the button text', -> + buttonText = 'test button text' + + view = new View + buttonText: buttonText + + expect(view.buttonText).to.equal buttonText + + + it 'should set the placeholder text', -> + placeholderText = 'test placeholder text' + + view = new View + placeholderText: placeholderText + + expect(view.placeholderText).to.equal placeholderText + + + it 'should set the error message for generic errors', -> + errorMessage = 'test generic error message' + + view = new View + errorMessages: + generic: errorMessage + + expect(view.errorMessages.generic).to.equal errorMessage + + + it 'should set the error message for invalid email addresses', -> + errorMessage = 'test invalid email error message' + + view = new View + errorMessages: + invalidEmail: errorMessage + + expect(view.errorMessages.invalidEmail).to.equal errorMessage + + + it 'should set the success message', -> + successMessage = 'test success message' + + view = new View + successMessage: successMessage + + expect(view.successMessage).to.equal successMessage diff --git a/bower_components/signup-form/src/signup-form.coffee b/bower_components/signup-form/src/signup-form.coffee new file mode 100644 index 0000000..809e5e6 --- /dev/null +++ b/bower_components/signup-form/src/signup-form.coffee @@ -0,0 +1,109 @@ +View = require 'signup-form-view' +ViewModel = require 'signup-form-view-model' + + +class SignupForm + + constructor: (options) -> + { + @parentSelector + onError + onSuccess + useTemplate + betaId + id + buttonText + placeholderText + errorMessages + successMessage + } = options + + @parentSelector ?= 'body' + useTemplate ?= true + + + @view = new View + id: id + buttonText: buttonText + placeholderText: placeholderText + errorMessages: errorMessages + successMessage: successMessage + + + if useTemplate + @template = @view.render() + $(@parentSelector).append @template + + + @viewModel = new ViewModel + betaId: betaId + + Rx.Observable.fromEvent(@view.$inputField()[0], 'keyup') + .map((e) -> + return e.target.value + ) + .startWith(@view.$inputField().val()) + .subscribe((emailAddress) => + @viewModel.emailAddress.onNext emailAddress + ) + + + @viewModel.success.subscribe((success) => + @view.$successMessage().toggleClass 'hidden', not success + ) + + @viewModel.success + .filter((success) -> + return success + ) + .subscribe((success) => + onSuccess? @viewModel.emailAddress.value + ) + + + errorObservable = @viewModel.genericError.combineLatest(@viewModel.invalidEmailError, (genericError, invalidEmailError) -> + return (genericError or invalidEmailError) + ) + + errorObservable.subscribe((error) => + @view.$errorMessage().toggleClass 'hidden', not error + ) + + errorObservable + .filter((error) -> + return error + ) + .subscribe((error) => + onError?() + ) + + + @viewModel.genericError + .filter((genericError) -> + return genericError + ) + .subscribe((genericError) => + @view.$errorMessage().text @view.errorMessages.genericError + ) + + @viewModel.invalidEmailError + .filter((invalidEmailError) -> + return invalidEmailError + ) + .subscribe((invalidEmailError) => + @view.$errorMessage().text @view.errorMessages.invalidEmail + ) + + + @viewModel.emailAddressIsValid.subscribe((valid) => + @view.$submitButton().prop 'disabled', not valid + ) + + + Rx.Observable.fromEvent(@view.$form()[0], 'submit').subscribe((e) => + e.preventDefault() + @viewModel.executeSubmitCommand() + ) + + +module.exports = SignupForm diff --git a/bower_components/signup-form/src/view-model.coffee b/bower_components/signup-form/src/view-model.coffee new file mode 100644 index 0000000..cc7f8f0 --- /dev/null +++ b/bower_components/signup-form/src/view-model.coffee @@ -0,0 +1,76 @@ +# HOST = 'https://the-grid-prefinery.herokuapp.com' +HOST = 'http://localhost:5000' + + +class SignUpFormViewModel + + constructor: (options)-> + {@betaId} = options + + @emailAddress = new Rx.BehaviorSubject + @emailAddressIsValid = new Rx.BehaviorSubject + @genericError = new Rx.BehaviorSubject false + @invalidEmailError = new Rx.BehaviorSubject false + @success = new Rx.BehaviorSubject false + + @emailAddress + .map((value) -> + return /^.+@.+\..+$/.test value + ) + .subscribe((isValid) => + @emailAddressIsValid.onNext isValid + ) + + @genericError + .combineLatest(@invalidEmailError, (genericError, invalidEmailError) -> + return (genericError or invalidEmailError) + ) + .filter((error) -> + return error + ) + .subscribe((error) => + @success.onNext false + ) + + @success + .filter((success) -> + return success + ) + .subscribe((success) => + @genericError.onNext false + @invalidEmailError.onNext false + ) + + + executeSubmitCommand: => + @_jqXHR?.abort() + + @genericError.onNext false + @invalidEmailError.onNext false + @success.onNext false + + @_jqXHR = $.ajax + type: 'POST' + url: "#{HOST}/api/v2/betas/#{@betaId}/testers", + data: JSON.stringify({ + tester: + email: @emailAddress.value + }) + contentType: 'application/json; charset=utf-8', + dataType: 'json' + + Rx.Observable.fromPromise(@_jqXHR.promise()).subscribe((data) => + @success.onNext true + , (error) => + if error.status is 422 and error.responseJSON.errors[0].code is 2310 + # Disregard errors about duplicate testers + @success.onNext true + else if error.status is 422 and error.responseJSON.errors[0].code is 2301 + # Handle invalid email address error + @invalidEmailError.onNext true + else + @genericError.onNext true + ) + + +module.exports = SignUpFormViewModel diff --git a/bower_components/signup-form/src/view.coffee b/bower_components/signup-form/src/view.coffee new file mode 100644 index 0000000..2be9c15 --- /dev/null +++ b/bower_components/signup-form/src/view.coffee @@ -0,0 +1,53 @@ +class SignupFormView + + constructor: (options = {}) -> + { + @id + @buttonText + @placeholderText + @errorMessages + @successMessage + } = options + + @id ?= 'signup-form' + @buttonText ?= 'Sign up' + @placeholderText ?= 'Email address' + @errorMessages ?= {} + @errorMessages.generic ?= 'There was an error processing your request. Please try again.' + @errorMessages.invalidEmail ?= 'Your email address is invalid. Please try again.' + @successMessage ?= 'Thanks!' + + + render: => + # Workaround for GSS and volatile DOM + # Set the error message to the longest message initially + if @errorMessages.generic.length > @errorMessages.invalidEmail.length + errorMessage = @errorMessages.generic + else + errorMessage = @errorMessages.invalidEmail + + + return """ +
+ + + + +
+ """ + + + $errorMessage: => $("##{@_errorMessageId()}") + $inputField: => $("##{@_inputFieldId()}") + $form: => $("##{@id}") + $submitButton: => $("##{@_submitButtonId()}") + $successMessage: => $("##{@_successMessageId()}") + + + _errorMessageId: => "#{@id}-error-message" + _inputFieldId: => "#{@id}-input-field" + _submitButtonId: => "#{@id}-submit-button" + _successMessageId: => "#{@id}-success-message" + + +module.exports = SignupFormView diff --git a/js/script.coffee b/js/script.coffee index b006657..c95c7c7 100644 --- a/js/script.coffee +++ b/js/script.coffee @@ -93,4 +93,4 @@ setupWayPoints = -> window.addEventListener "scroll", checkWayPoints -)() \ No newline at end of file +)() diff --git a/style/styles.css b/style/styles.css index 833ce31..8d30e8a 100644 --- a/style/styles.css +++ b/style/styles.css @@ -130,7 +130,8 @@ small { } -button, .button { +button, +.button { outline: none; box-shadow: none; padding: .2em .6em; @@ -139,6 +140,7 @@ button, .button { font-family: "adelle", georgia, serif; text-transform: uppercase; letter-spacing: 0.1em; + cursor: pointer; } .trans { @@ -175,9 +177,15 @@ ul.large-list a { color: hsl(58, 100%, 84%); } -button { +[disabled] { + opacity: .5; +} + +button, .button, +button[disabled], .button[disabled], +button[disabled]:hover, .button[disabled]:hover { color: hsl(282, 99%, 31%); - background-color: hsl(58, 100%, 84%); + background-color: hsl(58, 100%, 84%) !important; } button.transparent { background-color: transparent; @@ -277,3 +285,55 @@ hr { } +.alert { + border-radius: 0; + -moz-border-radius: 0; + -webkit-border-radius: 0; + + border-width: 0; + color: hsl(0, 0%, 100%); + font-family: "museo-sans", sans-serif; + font-size: 17px; + padding: 23px; + text-align: center; +} + +@media (min-width: 720px) { + .alert { + text-align: left; + } +} + +.alert.hidden { + visibility: hidden; +} + +.alert.error { + background: hsl(332, 54%, 47%); +} + +.alert.success { + background: hsl(178, 100%, 37%); +} + + +input[type=email] { + background: none; + border-color: hsl(282, 100%, 31%); + border-style: solid; + border-width: 5px; + color: hsl(0, 0%, 49%); + font-size: 17px; + font-family: "museo-sans", sans-serif; + height: 58px; + padding: 0 16px; + text-align: center; + text-transform: lowercase; +} + +@media (min-width: 720px) { + input[type=email] { + font-size: 23px; + text-align: left; + } +}