Merge pull request #3363 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')
'teachers': go('TeachersView')
'teachers/freetrial': go('TeachersFreeTrialView')
'teachers/freetrial': go('RequestQuoteView')
'teachers/quote': go('RequestQuoteView')
'test(/*subpath)': go('TestView')

View file

@ -93,7 +93,7 @@ module.exports = class Tracker
ga? 'send', 'pageview', url
# 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
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 = {}
inputs = $('input', el).add('textarea', el)
inputs = $('input, textarea, select', $el)
for input in inputs
input = $(input)
continue unless name = input.attr('name')
obj[name] = input.val()
obj[name] = obj[name].trim() if obj[name]?.trim
if input.attr('type') is 'checkbox'
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
module.exports.applyErrorsToForm = (el, errors, warning=false) ->
errors = [errors] if not $.isArray(errors)
missingErrors = []
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..]
console.log prop
message = error.message
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
kind = if warning then 'warning' else 'error'
afterEl = $(formGroup.find('.help-block, .form-control, input, select, textarea')[0])
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) ->
input = $("[name='#{property}']", el)

View file

@ -622,6 +622,37 @@
hear_about: "How did you hear about CodeCombat?"
fill_fields: "Please fill out all fields."
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:
save_version_title: "Save New Version"

View file

@ -5,3 +5,17 @@ module.exports = class TrialRequest extends CocoModel
@className: 'TrialRequest'
@schema: schema
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 School
th Location
th Age
th Age / Level
th Students
th How Found
th How Found / Notes
th Status
tbody
- var numReviewed = 0
@ -35,13 +35,14 @@ block content
td.reviewed
if trialRequest.get('reviewDate')
span= trialRequest.get('reviewDate').substring(0, 10)
- var props = trialRequest.get('properties')
td
a(href="/user/#{trialRequest.get('applicant')}")= trialRequest.get('properties').email
td= trialRequest.get('properties').school
td= trialRequest.get('properties').location
td= trialRequest.get('properties').age
td= trialRequest.get('properties').numStudents
td= trialRequest.get('properties').heardAbout
a(href="/user/#{trialRequest.get('applicant')}")= trialRequest.nameString()
td= props.school || props.organization
td= props.location || trialRequest.locationString()
td= props.age || trialRequest.educationLevelString()
td= props.numStudents
td= props.heardAbout || props.notes
td.status-cell
if trialRequest.get('status') === 'submitted'
button.btn.btn-xs.btn-success.btn-approve(data-trial-request-id=trialRequest.id) Approve

View file

@ -50,7 +50,7 @@
| :
.input-border
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
input#name.input-large.form-control(name="name", type="text", value="", placeholder="e.g. Alex W the Skater")
.col-md-6
@ -62,7 +62,7 @@
span(data-i18n="signup.optional") optional
| ):
.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
label.control-label(for="subscribe")
.input-border

View file

@ -43,7 +43,7 @@ block content
span.spl(data-i18n="courses.educator_wiki_suff")
li
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")
li
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
.text-right
button.btn-contact-us(href='/teachers/freetrial')
button.btn-contact-us(href='/teachers/quote')
img(src='/images/pages/sales/chat_icon.png')
div contact us for pricing and a free trial
div contact us for a quote
table
tr
@ -216,5 +216,5 @@ block content
br
p.text-center
button.btn-contact-us contact us for a free trial
button.btn-contact-us contact us for a quote
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'
onClickContactUs: (e) ->
app.router.navigate '/teachers/freetrial', trigger: true
app.router.navigate '/teachers/quote', trigger: true
onClickLogin: (e) ->
@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
super options
@mode = options.mode if options.mode
@previousFormInputs = options.initialValues or {}
getRenderData: ->
c = super()