Merge pull request from codecombat/quote-request

Quote request
This commit is contained in:
Scott Erickson 2016-01-28 17:29:14 -08:00
commit 57596d7c46
15 changed files with 381 additions and 25 deletions

View file

@ -0,0 +1,11 @@
CocoCollection = require 'collections/CocoCollection'
TrialRequest = require 'models/TrialRequest'
module.exports = class TrialRequestCollection extends CocoCollection
url: '/db/trial.request'
model: TrialRequest
fetchOwn: (options) ->
options = _.extend({data: {}}, options)
options.url = _.result(@, 'url') + '/-/own'
@fetch(options)

View file

@ -121,7 +121,8 @@ module.exports = class CocoRouter extends Backbone.Router
'schools': go('SalesView') 'schools': go('SalesView')
'teachers': go('TeachersView') 'teachers': go('TeachersView')
'teachers/freetrial': go('TeachersFreeTrialView') 'teachers/freetrial': go('RequestQuoteView')
'teachers/quote': go('RequestQuoteView')
'test(/*subpath)': go('TestView') 'test(/*subpath)': go('TestView')

View file

@ -93,7 +93,7 @@ module.exports = class Tracker
ga? 'send', 'pageview', url ga? 'send', 'pageview', url
# Mixpanel # Mixpanel
mixpanelIncludes = ['', 'courses', 'courses/purchase', 'courses/teachers', 'courses/students', 'schools', 'teachers', 'teachers/freetrial'] mixpanelIncludes = ['', 'courses', 'courses/purchase', 'courses/teachers', 'courses/students', 'schools', 'teachers', 'teachers/freetrial', 'teachers/quote']
mixpanel.track('page viewed', 'page name' : name, url : url) if name in mixpanelIncludes mixpanel.track('page viewed', 'page name' : name, url : url) if name in mixpanelIncludes
trackEvent: (action, properties={}, includeIntegrations=[]) => trackEvent: (action, properties={}, includeIntegrations=[]) =>

View file

@ -1,22 +1,35 @@
module.exports.formToObject = (el) -> module.exports.formToObject = ($el, options) ->
options = _.extend({ trim: true, ignoreEmptyString: true }, options)
obj = {} obj = {}
inputs = $('input', el).add('textarea', el) inputs = $('input, textarea, select', $el)
for input in inputs for input in inputs
input = $(input) input = $(input)
continue unless name = input.attr('name') continue unless name = input.attr('name')
obj[name] = input.val() if input.attr('type') is 'checkbox'
obj[name] = obj[name].trim() if obj[name]?.trim obj[name] ?= []
if input.is(':checked')
obj[name].push(input.val())
else if input.attr('type') is 'radio'
continue unless input.is('checked')
obj[name] = input.val()
else
value = input.val() or ''
value = _.string.trim(value) if options.trim
if value or (not options.ignoreEmptyString)
obj[name] = value
obj obj
module.exports.applyErrorsToForm = (el, errors, warning=false) -> module.exports.applyErrorsToForm = (el, errors, warning=false) ->
errors = [errors] if not $.isArray(errors) errors = [errors] if not $.isArray(errors)
missingErrors = [] missingErrors = []
for error in errors for error in errors
if error.dataPath if error.code is tv4.errorCodes.OBJECT_REQUIRED
prop = _.last(_.string.words(error.message)) # hack
message = 'Required field'
else if error.dataPath
prop = error.dataPath[1..] prop = error.dataPath[1..]
console.log prop
message = error.message message = error.message
else else
@ -35,8 +48,13 @@ module.exports.setErrorToField = setErrorToField = (el, message, warning=false)
return console.error el, " did not contain a form group, so couldn't show message:", message return console.error el, " did not contain a form group, so couldn't show message:", message
kind = if warning then 'warning' else 'error' kind = if warning then 'warning' else 'error'
afterEl = $(formGroup.find('.help-block, .form-control, input, select, textarea')[0])
formGroup.addClass "has-#{kind}" formGroup.addClass "has-#{kind}"
formGroup.append $("<span class='help-block #{kind}-help-block'>#{message}</span>") helpBlock = $("<span class='help-block #{kind}-help-block'>#{message}</span>")
if afterEl.length
afterEl.before helpBlock
else
formGroup.append helpBlock
module.exports.setErrorToProperty = setErrorToProperty = (el, property, message, warning=false) -> module.exports.setErrorToProperty = setErrorToProperty = (el, property, message, warning=false) ->
input = $("[name='#{property}']", el) input = $("[name='#{property}']", el)

View file

@ -622,6 +622,37 @@
hear_about: "How did you hear about CodeCombat?" hear_about: "How did you hear about CodeCombat?"
fill_fields: "Please fill out all fields." fill_fields: "Please fill out all fields."
thanks: "Thanks! We'll send you setup instructions shortly." thanks: "Thanks! We'll send you setup instructions shortly."
teachers_quote:
name: "Quote Form"
title: "Request a Quote"
subtitle: "Get CodeCombat in your classroom, club, school or district!"
phone_number: "Phone number"
phone_number_help: "Where can we reach you during the workday?"
role_label: "Your role"
role_help: "Select your primary role."
tech_coordinator: "Technology coordinator"
advisor: "Advisor"
principal: "Principal"
superintendent: "Superintendent"
parent: "Parent"
organization_label: "Name of School/District"
city: "City"
state: "State"
country: "Country"
num_students_help: "How many do you anticipate enrolling in CodeCombat?"
education_level_label: "Education Level of Students"
education_level_help: "Choose as many as apply."
elementary_school: "Elementary School"
high_school: "High School"
please_explain: "(please explain)"
middle_school: "Middle School"
college_plus: "College or higher"
anything_else: "Anything else we should know?"
thanks_header: "Thanks for requesting a quote!"
thanks_p: "We'll be in touch soon. Questions? Email us:"
thanks_anon: "Login or sign up with your account below to access your two free enrollments (well notify you by email when they have been approved, which usually takes less than 48 hours). As always, the first hour of content is free for an unlimited number of students."
thanks_logged_in: "Your two free enrollments are pending approval. Well notify you by email when they have been approved (usually within 48 hours). As always, the first hour of content is free for an unlimited number of students."
versions: versions:
save_version_title: "Save New Version" save_version_title: "Save New Version"

View file

@ -5,3 +5,17 @@ module.exports = class TrialRequest extends CocoModel
@className: 'TrialRequest' @className: 'TrialRequest'
@schema: schema @schema: schema
urlRoot: '/db/trial.request' urlRoot: '/db/trial.request'
nameString: ->
props = @get('properties')
values = _.filter(_.at(props, 'name', 'email'))
return values.join(' / ')
locationString: ->
props = @get('properties')
values = _.filter(_.at(props, 'city', 'state', 'country'))
return values.join(' ')
educationLevelString: ->
levels = @get('properties').educationLevel or []
return levels.join(', ')

View file

@ -0,0 +1,33 @@
#request-quote-view
label
margin-bottom: 2px
.row
margin: 10px 0
.help-block
margin: 0
p
margin: 0 0 20px
hr
margin: 30px 0
.checkbox, .checkbox-inline
margin: 0
#anything-else-row
margin: 50px 0 20px
#other-education-level-input
display: inline-block
width: 200px
margin-left: 5px
#submit-request-btn
margin-left: 10px
#login-btn
margin-right: 10px

View file

@ -18,9 +18,9 @@ block content
th Applicant th Applicant
th School th School
th Location th Location
th Age th Age / Level
th Students th Students
th How Found th How Found / Notes
th Status th Status
tbody tbody
- var numReviewed = 0 - var numReviewed = 0
@ -35,13 +35,14 @@ block content
td.reviewed td.reviewed
if trialRequest.get('reviewDate') if trialRequest.get('reviewDate')
span= trialRequest.get('reviewDate').substring(0, 10) span= trialRequest.get('reviewDate').substring(0, 10)
- var props = trialRequest.get('properties')
td td
a(href="/user/#{trialRequest.get('applicant')}")= trialRequest.get('properties').email a(href="/user/#{trialRequest.get('applicant')}")= trialRequest.nameString()
td= trialRequest.get('properties').school td= props.school || props.organization
td= trialRequest.get('properties').location td= props.location || trialRequest.locationString()
td= trialRequest.get('properties').age td= props.age || trialRequest.educationLevelString()
td= trialRequest.get('properties').numStudents td= props.numStudents
td= trialRequest.get('properties').heardAbout td= props.heardAbout || props.notes
td.status-cell td.status-cell
if trialRequest.get('status') === 'submitted' if trialRequest.get('status') === 'submitted'
button.btn.btn-xs.btn-success.btn-approve(data-trial-request-id=trialRequest.id) Approve button.btn.btn-xs.btn-success.btn-approve(data-trial-request-id=trialRequest.id) Approve

View file

@ -50,7 +50,7 @@
| : | :
.input-border .input-border
if me.get('name') if me.get('name')
input#name.input-large.form-control(name="name", type="text", value="#{me.get('name')}") input#name.input-large.form-control(name="name", type="text", value=me.get('name'))
else else
input#name.input-large.form-control(name="name", type="text", value="", placeholder="e.g. Alex W the Skater") input#name.input-large.form-control(name="name", type="text", value="", placeholder="e.g. Alex W the Skater")
.col-md-6 .col-md-6
@ -62,7 +62,7 @@
span(data-i18n="signup.optional") optional span(data-i18n="signup.optional") optional
| ): | ):
.input-border .input-border
input#school-input.input-large.form-control(name="schoolName", data-i18n="[placeholder]signup.school_name_placeholder") input#school-input.input-large.form-control(name="schoolName", data-i18n="[placeholder]signup.school_name_placeholder", value=formValues.schoolName || '')
.form-group.checkbox .form-group.checkbox
label.control-label(for="subscribe") label.control-label(for="subscribe")
.input-border .input-border

View file

@ -43,7 +43,7 @@ block content
span.spl(data-i18n="courses.educator_wiki_suff") span.spl(data-i18n="courses.educator_wiki_suff")
li li
span.spr(data-i18n="courses.additional_resources_2_pref") span.spr(data-i18n="courses.additional_resources_2_pref")
a(href='/teachers/freetrial', data-i18n="teachers_survey.title") a(href='/teachers/quote', data-i18n="teachers_quote.name")
span.spl(data-i18n="courses.additional_resources_2_suff") span.spl(data-i18n="courses.additional_resources_2_suff")
li li
span.spr(data-i18n="courses.additional_resources_3_pref") span.spr(data-i18n="courses.additional_resources_3_pref")

View file

@ -0,0 +1,150 @@
extends /templates/base
block content
form.form(class=view.trialRequest.isNew() ? '' : 'hide')
h1.text-center(data-i18n="teachers_quote.title")
p.text-center(data-i18n="teachers_quote.subtitle")
.row
.col-sm-offset-2.col-sm-4
.form-group
label.control-label(data-i18n="general.name")
input.form-control(name="name")
.col-sm-4
.form-group
label.control-label(data-i18n="general.email")
input.form-control(name="email")
.row
.col-sm-offset-2.col-sm-4
.form-group
label.control-label
span(data-i18n="teachers_quote.phone_number")
span.spl.text-muted(data-i18n="signup.optional")
.help-block
em.text-info(data-i18n="teachers_quote.phone_number_help")
input.form-control(name="phoneNumber")
.col-sm-4
.form-group
label.control-label(data-i18n="teachers_quote.role_label")
.help-block
em.text-info(data-i18n="teachers_quote.role_help")
select.form-control(name="role")
option
option(data-i18n="courses.teacher", value="Teacher")
option(data-i18n="teachers_quote.tech_coordinator", value="Technology coordinator")
option(data-i18n="teachers_quote.advisor", value="Advisor")
option(data-i18n="teachers_quote.principal", value="Principal")
option(data-i18n="teachers_quote.superintendent", value="Superintendent")
option(data-i18n="teachers_quote.parent", value="Parent")
.row
.col-sm-offset-2.col-sm-8
hr
.row
.col-sm-offset-2.col-sm-4
.form-group
label.control-label(data-i18n="teachers_quote.organization_label")
input.form-control(name="organization")
.col-sm-4
.form-group
label.control-label(data-i18n="teachers_quote.city")
input.form-control(name="city")
.row
.col-sm-offset-2.col-sm-4
.form-group
label.control-label(data-i18n="teachers_quote.state")
input.form-control(name="state")
.col-sm-4
.form-group
label.control-labellabel.control-label(data-i18n="teachers_quote.country")
input.form-control(name="country")
.row
.col-sm-offset-2.col-sm-8
hr
.row
.col-sm-offset-2.col-sm-5
.form-group
label.control-label(data-i18n="courses.number_students")
.help-block
em.text-info(data-i18n="teachers_quote.num_students_help")
select.form-control(name="numStudents")
option
option 1-10
option 11-50
option 51-100
option 101-200
option 201-500
option 501-1000
option 1000+
.form-group
.row
.col-sm-offset-2.col-sm-4
label.control-label(data-i18n="teachers_quote.education_level_label")
.help-block
em.text-info(data-i18n="teachers_quote.education_level_help")
.row
.col-sm-offset-2.col-sm-2
label.control-label.checkbox
input(type="checkbox" name="educationLevel" value="Elementary")
span(data-i18n="teachers_quote.elementary_school")
.col-sm-2
label.control-label.checkbox
input(type="checkbox" name="educationLevel" value="High")
span(data-i18n="teachers_quote.high_school")
.col-sm-6
.checkbox-inline
label.control-label
input#other-education-level-checkbox(type="checkbox")
span(data-i18n="nav.other")
br
span(data-i18n="teachers_quote.please_explain")
input#other-education-level-input.form-control
.row
.col-sm-offset-2.col-sm-2
label.control-label.checkbox
input(type="checkbox" name="educationLevel" value="Middle")
span(data-i18n="teachers_quote.middle_school")
.col-sm-2
label.control-label.checkbox
input(type="checkbox" name="educationLevel" value="College+")
span(data-i18n="teachers_quote.college_plus")
#anything-else-row.row
.col-sm-offset-2.col-sm-8
label.control-label
span(data-i18n="teachers_quote.anything_else")
span.spl.text-muted(data-i18n="signup.optional")
textarea.form-control(rows=8, name="notes")
#buttons-row.row.text-center
input#submit-request-btn.btn.btn-primary(type="submit" data-i18n="[value]common.send")
#form-submit-success.text-center(class=view.trialRequest.isNew() ? 'hide' : '')
h1.text-center(data-i18n="teachers_quote.thanks_header")
p.text-center
span.spr(data-i18n="teachers_quote.thanks_p")
a.spl(href="mailto:team@codecombat.com") team@codecombat.com
if me.isAnonymous()
p.text-center(data-i18n="teachers_quote.thanks_anon")
p.text-center
button#login-btn.btn.btn-info(data-i18n="login.log_in")
button#signup-btn.btn.btn-info(data-i18n="login.sign_up")
else
p.text-center(data-i18n="teachers_quote.thanks_logged_in")

View file

@ -7,9 +7,9 @@ block content
br br
br br
.text-right .text-right
button.btn-contact-us(href='/teachers/freetrial') button.btn-contact-us(href='/teachers/quote')
img(src='/images/pages/sales/chat_icon.png') img(src='/images/pages/sales/chat_icon.png')
div contact us for pricing and a free trial div contact us for a quote
table table
tr tr
@ -216,5 +216,5 @@ block content
br br
p.text-center p.text-center
button.btn-contact-us contact us for a free trial button.btn-contact-us contact us for a quote
br br

View file

@ -0,0 +1,96 @@
RootView = require 'views/core/RootView'
forms = require 'core/forms'
TrialRequest = require 'models/TrialRequest'
TrialRequests = require 'collections/TrialRequests'
AuthModal = require 'views/core/AuthModal'
formSchema = {
type: 'object'
required: ['name', 'email', 'organization', 'role', 'numStudents']
properties:
name: { type: 'string', minLength: 1 }
email: { type: 'string', format: 'email' }
phoneNumber: { type: 'string' }
role: { type: 'string' }
organization: { type: 'string' }
city: { type: 'string' }
state: { type: 'string' }
country: { type: 'string' }
numStudents: { type: 'string' }
educationLevel: {
type: 'array'
items: { type: 'string' }
}
notes: { type: 'string' }
}
module.exports = class RequestQuoteView extends RootView
id: 'request-quote-view'
template: require 'templates/request-quote-view'
events:
'submit form': 'onSubmitForm'
'click #login-btn': 'onClickLoginButton'
'click #signup-btn': 'onClickSignupButton'
initialize: ->
@trialRequest = new TrialRequest()
@trialRequests = new TrialRequests()
@trialRequests.fetchOwn()
@supermodel.loadCollection(@trialRequests)
onLoaded: ->
if @trialRequests.size()
@trialRequest = @trialRequests.first()
super()
onSubmitForm: (e) ->
e.preventDefault()
form = @$('form')
attrs = forms.formToObject(form)
if @$('#other-education-level-checkbox').is(':checked')
attrs.educationLevel.push(@$('#other-education-level-input').val())
forms.clearFormAlerts(form)
result = tv4.validateMultiple(attrs, formSchema)
if not result.valid
return forms.applyErrorsToForm(form, result.errors)
if not /^.+@.+\..+$/.test(attrs.email)
return forms.setErrorToProperty(form, 'email', 'Invalid email.')
if not _.size(attrs.educationLevel)
return forms.setErrorToProperty(form, 'educationLevel', 'Check at least one.')
@trialRequest = new TrialRequest({
type: 'course'
properties: attrs
})
@$('#submit-request-btn').text('Sending').attr('disabled', true)
@trialRequest.save()
@trialRequest.on 'sync', @onTrialRequestSubmit, @
@trialRequest.on 'error', @onTrialRequestError, @
onTrialRequestError: ->
@$('#submit-request-btn').text('Submit').attr('disabled', false)
onTrialRequestSubmit: ->
@$('form, #form-submit-success').toggleClass('hide')
onClickLoginButton: ->
modal = new AuthModal({
mode: 'login'
initialValues: { email: @trialRequest.get('properties')?.email }
})
@openModalView(modal)
window.nextURL = '/courses/teachers' unless @trialRequest.isNew()
onClickSignupButton: ->
props = @trialRequest.get('properties') or {}
me.set('name', props.name)
modal = new AuthModal({
mode: 'signup'
initialValues: {
email: props.email
schoolName: props.organization
}
})
@openModalView(modal)
window.nextURL = '/courses/teachers' unless @trialRequest.isNew()

View file

@ -17,7 +17,7 @@ module.exports = class SalesView extends RootView
'CodeCombat' 'CodeCombat'
onClickContactUs: (e) -> onClickContactUs: (e) ->
app.router.navigate '/teachers/freetrial', trigger: true app.router.navigate '/teachers/quote', trigger: true
onClickLogin: (e) -> onClickLogin: (e) ->
@openModalView new AuthModal(mode: 'login') if me.get('anonymous') @openModalView new AuthModal(mode: 'login') if me.get('anonymous')

View file

@ -30,6 +30,7 @@ module.exports = class AuthModal extends ModalView
@onNameChange = _.debounce @checkNameExists, 500 @onNameChange = _.debounce @checkNameExists, 500
super options super options
@mode = options.mode if options.mode @mode = options.mode if options.mode
@previousFormInputs = options.initialValues or {}
getRenderData: -> getRenderData: ->
c = super() c = super()