mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-27 17:45:40 -05:00
commit
57596d7c46
15 changed files with 381 additions and 25 deletions
11
app/collections/TrialRequests.coffee
Normal file
11
app/collections/TrialRequests.coffee
Normal 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)
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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=[]) =>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 (we’ll 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. We’ll 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"
|
||||
|
|
|
@ -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(', ')
|
||||
|
|
33
app/styles/request-quote-view.sass
Normal file
33
app/styles/request-quote-view.sass
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
150
app/templates/request-quote-view.jade
Normal file
150
app/templates/request-quote-view.jade
Normal 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")
|
|
@ -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
|
||||
|
|
96
app/views/RequestQuoteView.coffee
Normal file
96
app/views/RequestQuoteView.coffee
Normal 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()
|
|
@ -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')
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in a new issue