Merge branch 'master' into production

This commit is contained in:
Nick Winter 2016-02-02 16:57:43 -08:00
commit 46b184765c
46 changed files with 1283 additions and 63 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -47,9 +47,11 @@
(window,document,"script","51a79585ee207206390002a2"); (window,document,"script","51a79585ee207206390002a2");
</script> </script>
<!-- start Mixpanel --><script type="text/javascript">(function(e,b){if(!b.__SV){var a,f,i,g;window.mixpanel=b;b._i=[];b.init=function(a,e,d){function f(b,h){var a=h.split(".");2==a.length&&(b=b[a[0]],h=a[1]);b[h]=function(){b.push([h].concat(Array.prototype.slice.call(arguments,0)))}}var c=b;"undefined"!==typeof d?c=b[d]=[]:d="mixpanel";c.people=c.people||[];c.toString=function(b){var a="mixpanel";"mixpanel"!==d&&(a+="."+d);b||(a+=" (stub)");return a};c.people.toString=function(){return c.toString(1)+".people (stub)"};i="disable time_event track track_pageview track_links track_forms register register_once alias unregister identify name_tag set_config people.set people.set_once people.increment people.append people.union people.track_charge people.clear_charges people.delete_user".split(" "); <!-- Mixpanel -->
for(g=0;g<i.length;g++)f(c,i[g]);b._i.push([a,e,d])};b.__SV=1.2;a=e.createElement("script");a.type="text/javascript";a.async=!0;a.src="undefined"!==typeof MIXPANEL_CUSTOM_LIB_URL?MIXPANEL_CUSTOM_LIB_URL:"file:"===e.location.protocol&&"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js".match(/^\/\//)?"https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js":"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js";f=e.getElementsByTagName("script")[0];f.parentNode.insertBefore(a,f)}})(document,window.mixpanel||[]); <script type="text/javascript">
mixpanel.init("e71a4e60db7e1dc5e685be96776280f9");</script><!-- end Mixpanel --> (function(e,b){if(!b.__SV){var a,f,i,g;window.mixpanel=b;b._i=[];b.init=function(a,e,d){function f(b,h){var a=h.split(".");2==a.length&&(b=b[a[0]],h=a[1]);b[h]=function(){b.push([h].concat(Array.prototype.slice.call(arguments,0)))}}var c=b;"undefined"!==typeof d?c=b[d]=[]:d="mixpanel";c.people=c.people||[];c.toString=function(b){var a="mixpanel";"mixpanel"!==d&&(a+="."+d);b||(a+=" (stub)");return a};c.people.toString=function(){return c.toString(1)+".people (stub)"};i="disable time_event track track_pageview track_links track_forms register register_once alias unregister identify name_tag set_config people.set people.set_once people.increment people.append people.union people.track_charge people.clear_charges people.delete_user".split(" ");for(g=0;g<i.length;g++)f(c,i[g]);b._i.push([a,e,d])};b.__SV=1.2;a=e.createElement("script");a.type="text/javascript";a.async=!0;a.src="undefined"!==typeof MIXPANEL_CUSTOM_LIB_URL?MIXPANEL_CUSTOM_LIB_URL:"file:"===e.location.protocol&&"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js".match(/^\/\//)?"https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js":"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js";f=e.getElementsByTagName("script")[0];f.parentNode.insertBefore(a,f)}})(document,window.mixpanel||[]);
mixpanel.init("e71a4e60db7e1dc5e685be96776280f9");
</script>
<script src="https://checkout.stripe.com/checkout.js"></script> <script src="https://checkout.stripe.com/checkout.js"></script>
@ -104,7 +106,7 @@ mixpanel.init("e71a4e60db7e1dc5e685be96776280f9");</script><!-- end Mixpanel -->
<div id="page-container" class="nano-content"> <div id="page-container" class="nano-content">
</div> </div>
<div id="modal-wrapper" class="modal-content"></div> <div id="modal-wrapper" class="modal-content hide"></div>
<div class="progress" id="module-load-progress"> <div class="progress" id="module-load-progress">
<div class="progress-bar"></div> <div class="progress-bar"></div>

View file

@ -13,7 +13,13 @@ module.exports = class CocoRouter extends Backbone.Router
@initializeSocialMediaServices = _.once @initializeSocialMediaServices @initializeSocialMediaServices = _.once @initializeSocialMediaServices
routes: routes:
'': go('HomeView') '': ->
# Testing new home page
group = me.getHomepageGroup()
return @routeDirectly('HomeView', [], { withTeacherNote: true }) if group is 'home-with-note'
return @routeDirectly('NewHomeView', [], { jumbotron: 'student' }) if group is 'new-home-student'
return @routeDirectly('NewHomeView', [], { jumbotron: 'characters' }) if group is 'new-home-characters'
return @routeDirectly('HomeView', [])
'about': go('AboutView') 'about': go('AboutView')
@ -93,6 +99,7 @@ module.exports = class CocoRouter extends Backbone.Router
'github/*path': 'routeToServer' 'github/*path': 'routeToServer'
'hoc': go('courses/HourOfCodeView') 'hoc': go('courses/HourOfCodeView')
'home': go('NewHomeView')
'i18n': go('i18n/I18NHomeView') 'i18n': go('i18n/I18NHomeView')
'i18n/thang/:handle': go('i18n/I18NEditThangTypeView') 'i18n/thang/:handle': go('i18n/I18NEditThangTypeView')
@ -118,6 +125,8 @@ module.exports = class CocoRouter extends Backbone.Router
'preview': go('HomeView') 'preview': go('HomeView')
'privacy': go('PrivacyView')
'schools': go('SalesView') 'schools': go('SalesView')
'teachers': go('TeachersView') 'teachers': go('TeachersView')
@ -137,15 +146,15 @@ module.exports = class CocoRouter extends Backbone.Router
removeTrailingSlash: (e) -> removeTrailingSlash: (e) ->
@navigate e, {trigger: true} @navigate e, {trigger: true}
routeDirectly: (path, args) -> routeDirectly: (path, args, options={}) ->
path = "views/#{path}" if not _.string.startsWith(path, 'views/') path = "views/#{path}" if not _.string.startsWith(path, 'views/')
ViewClass = @tryToLoadModule path ViewClass = @tryToLoadModule path
if not ViewClass and application.moduleLoader.load(path) if not ViewClass and application.moduleLoader.load(path)
@listenToOnce application.moduleLoader, 'load-complete', -> @listenToOnce application.moduleLoader, 'load-complete', ->
@routeDirectly(path, args) @routeDirectly(path, args, options)
return return
return @openView @notFoundView() if not ViewClass return @openView @notFoundView() if not ViewClass
view = new ViewClass({}, args...) # options, then any path fragment args view = new ViewClass(options, args...) # options, then any path fragment args
view.render() view.render()
@openView(view) @openView(view)

View file

@ -1,12 +1,17 @@
{me} = require 'core/auth' {me} = require 'core/auth'
SuperModel = require 'models/SuperModel' SuperModel = require 'models/SuperModel'
utils = require 'core/utils' utils = require 'core/utils'
CocoClass = require 'core/CocoClass'
debugAnalytics = false debugAnalytics = false
targetInspectJSLevelSlugs = ['cupboards-of-kithgard'] targetInspectJSLevelSlugs = ['cupboards-of-kithgard']
module.exports = class Tracker module.exports = class Tracker extends CocoClass
subscriptions:
'application:service-loaded': 'onServiceLoaded'
constructor: -> constructor: ->
super()
if window.tracker if window.tracker
console.error 'Overwrote our Tracker!', window.tracker console.error 'Overwrote our Tracker!', window.tracker
window.tracker = @ window.tracker = @
@ -14,6 +19,7 @@ module.exports = class Tracker
@trackReferrers() @trackReferrers()
@identify() @identify()
@supermodel = new SuperModel() @supermodel = new SuperModel()
@updateRole() if me.get 'role'
enableInspectletJS: (levelSlug) -> enableInspectletJS: (levelSlug) ->
# InspectletJS loading is delayed and targeting specific levels for more focused investigations # InspectletJS loading is delayed and targeting specific levels for more focused investigations
@ -30,7 +36,7 @@ module.exports = class Tracker
insp.async = true insp.async = true
insp.id = 'inspsync' insp.id = 'inspsync'
insp.src = (if 'https:' == document.location.protocol then 'https' else 'http') + '://cdn.inspectlet.com/inspectlet.js' insp.src = (if 'https:' == document.location.protocol then 'https' else 'http') + '://cdn.inspectlet.com/inspectlet.js'
insp.onreadystatechange = => scriptLoaded() if insp.readyState is 'complete' insp.onreadystatechange = -> scriptLoaded() if insp.readyState is 'complete'
insp.onload = scriptLoaded insp.onload = scriptLoaded
x = document.getElementsByTagName('script')[0] x = document.getElementsByTagName('script')[0]
@inspectletScriptNode = x.parentNode.insertBefore insp, x @inspectletScriptNode = x.parentNode.insertBefore insp, x
@ -62,8 +68,11 @@ module.exports = class Tracker
@explicitTraits ?= {} @explicitTraits ?= {}
@explicitTraits[key] = value for key, value of traits @explicitTraits[key] = value for key, value of traits
for userTrait in ['email', 'anonymous', 'dateCreated', 'name', 'testGroupNumber', 'gender', 'lastLevel', 'siteref', 'ageRange', 'schoolName', 'coursePrepaidID'] for userTrait in ['email', 'anonymous', 'dateCreated', 'name', 'testGroupNumber', 'gender', 'lastLevel', 'siteref', 'ageRange', 'schoolName', 'coursePrepaidID', 'role']
traits[userTrait] ?= me.get(userTrait) traits[userTrait] ?= me.get(userTrait)
if @isTeacher()
traits.teacher = true
console.log 'Would identify', me.id, traits if debugAnalytics console.log 'Would identify', me.id, traits if debugAnalytics
return unless @isProduction and not me.isAdmin() return unless @isProduction and not me.isAdmin()
@ -81,7 +90,11 @@ module.exports = class Tracker
mixpanel.identify(me.id) mixpanel.identify(me.id)
mixpanel.register(traits) mixpanel.register(traits)
trackPageView: -> if @isTeacher() and @segmentLoaded
traits.createdAt = me.get 'dateCreated' # Intercom, at least, wants this
analytics.identify me.id, traits
trackPageView: (includeIntegrations=[]) ->
name = Backbone.history.getFragment() name = Backbone.history.getFragment()
url = "/#{name}" url = "/#{name}"
console.log "Would track analytics pageview: #{url}" if debugAnalytics console.log "Would track analytics pageview: #{url}" if debugAnalytics
@ -93,9 +106,17 @@ 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', 'teachers/quote'] mixpanelIncludes = ['', 'courses', 'courses/purchase', 'courses/teachers', 'courses/students', 'schools', 'teachers', 'teachers/freetrial', 'teachers/quote', 'play', 'play/level/dungeons-of-kithgard']
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
if @isTeacher() and @segmentLoaded
options = {}
if includeIntegrations?.length
options.integrations = All: false
for integration in includeIntegrations
options.integrations[integration] = true
analytics.page url, {}, options
trackEvent: (action, properties={}, includeIntegrations=[]) => trackEvent: (action, properties={}, includeIntegrations=[]) =>
@trackEventInternal action, _.cloneDeep properties unless me?.isAdmin() and @isProduction @trackEventInternal action, _.cloneDeep properties unless me?.isAdmin() and @isProduction
console.log 'Tracking external analytics event:', action, properties, includeIntegrations if debugAnalytics console.log 'Tracking external analytics event:', action, properties, includeIntegrations if debugAnalytics
@ -119,9 +140,18 @@ module.exports = class Tracker
# Only log explicit events for now # Only log explicit events for now
mixpanel.track(action, properties) if 'Mixpanel' in includeIntegrations mixpanel.track(action, properties) if 'Mixpanel' in includeIntegrations
if @isTeacher() and @segmentLoaded
options = {}
if includeIntegrations
# https://segment.com/docs/libraries/analytics.js/#selecting-integrations
options.integrations = All: false
for integration in includeIntegrations
options.integrations[integration] = true
analytics?.track action, {}, options
trackEventInternal: (event, properties) => trackEventInternal: (event, properties) =>
# Skipping heavily logged actions we don't use internally # Skipping heavily logged actions we don't use internally
unless event in ['Simulator Result', 'Started Level Load', 'Finished Level Load'] return if event in ['Simulator Result', 'Started Level Load', 'Finished Level Load']
# Trimming properties we don't use internally # Trimming properties we don't use internally
# TODO: delete properites.level for 'Saw Victory' after 2/8/15. Should be using levelID instead. # TODO: delete properites.level for 'Saw Victory' after 2/8/15. Should be using levelID instead.
if event in ['Clicked Start Level', 'Inventory Play', 'Heard Sprite', 'Started Level', 'Saw Victory', 'Click Play', 'Choose Inventory', 'Homepage Loaded', 'Change Hero'] if event in ['Clicked Start Level', 'Inventory Play', 'Heard Sprite', 'Started Level', 'Saw Victory', 'Click Play', 'Choose Inventory', 'Homepage Loaded', 'Change Hero']
@ -155,3 +185,18 @@ module.exports = class Tracker
console.log 'Would track timing event:', arguments if debugAnalytics console.log 'Would track timing event:', arguments if debugAnalytics
return unless me and @isProduction and not me.isAdmin() return unless me and @isProduction and not me.isAdmin()
ga? 'send', 'timing', category, variable, duration, label ga? 'send', 'timing', category, variable, duration, label
isTeacher: ->
return me.get('role') in ['teacher', 'technology coordinator', 'advisor', 'principal', 'superintendent']
updateRole: ->
return unless @isTeacher()
return require('core/services/segment')() unless @segmentLoaded
@identify()
#analytics.page() # It looks like we don't want to call this here because it somehow already gets called once in addition to this.
# TODO: record any events and pageviews that have built up before we knew we were a teacher.
onServiceLoaded: (e) ->
return unless e.service is 'segment'
@segmentLoaded = true
@updateRole()

View file

@ -0,0 +1,46 @@
module.exports = initializeSegmentio = ->
analytics = window.analytics = window.analytics or []
return if analytics.initialize
return console?.error 'Segment snippet included twice.' if analytics.invoked
analytics.invoked = true
analytics.methods = [
'trackSubmit'
'trackClick'
'trackLink'
'trackForm'
'pageview'
'identify'
'reset'
'group'
'track'
'ready'
'alias'
'page'
'once'
'off'
'on'
]
analytics.factory = (t) ->
->
e = Array::slice.call(arguments)
e.unshift t
analytics.push e
analytics
for method in analytics.methods
analytics[method] = analytics.factory method
analytics.load = (t) ->
e = document.createElement 'script'
e.type = 'text/javascript'
e.async = true
e.src = (if document.location.protocol is 'https:' then 'https://' else 'http://') + 'cdn.segment.com/analytics.js/v1/' + t + '/analytics.min.js'
n = document.getElementsByTagName('script')[0]
n.parentNode.insertBefore e, n
Backbone.Mediator.publish 'application:service-loaded', service: 'segment'
return
analytics.SNIPPET_VERSION = '3.1.0'
analytics.load 'yJpJZWBw68fEj0aPSv8ffMMgof5kFnU9'
#analytics.page() # Don't track the page view on initial inclusion

View file

@ -59,6 +59,15 @@ module.exports = class User extends CocoModel
isEmailSubscriptionEnabled: (name) -> (@get('emails') or {})[name]?.enabled isEmailSubscriptionEnabled: (name) -> (@get('emails') or {})[name]?.enabled
setRole: (role, force=false) ->
return if me.isAdmin()
oldRole = @get 'role'
return if oldRole is role or (oldRole and not force)
@set 'role', role
@patch()
application.tracker?.updateRole()
return @get 'role'
a = 5 a = 5
b = 100 b = 100
c = b c = b
@ -125,6 +134,21 @@ module.exports = class User extends CocoModel
application.tracker.identify announcesActionAudioGroup: @announcesActionAudioGroup unless me.isAdmin() application.tracker.identify announcesActionAudioGroup: @announcesActionAudioGroup unless me.isAdmin()
@announcesActionAudioGroup @announcesActionAudioGroup
getHomepageGroup: ->
# return 'control'
# return 'home-with-note'
# return 'new-home-student'
# return 'new-home-characters'
return @homepageGroup if @homepageGroup
group = me.get('testGroupNumber') % 4
@homepageGroup = switch group
when 0 then 'control'
when 1 then 'home-with-note'
when 2 then 'new-home-student'
when 3 then 'new-home-characters'
application.tracker.identify newHomepageGroup: group unless me.isAdmin()
return @homepageGroup
# Signs and Portents was receiving updates after test started, and also had a big bug on March 4, so just look at test from March 5 on. # Signs and Portents was receiving updates after test started, and also had a big bug on March 4, so just look at test from March 5 on.
# ... and stopped working well until another update on March 10, so maybe March 11+... # ... and stopped working well until another update on March 10, so maybe March 11+...
# ... and another round, and then basically it just isn't completing well, so we pause the test until we can fix it. # ... and another round, and then basically it just isn't completing well, so we pause the test until we can fix it.

View file

@ -327,6 +327,7 @@ _.extend UserSchema.properties,
description: 'Prepaid which has paid for this user\'s course access' description: 'Prepaid which has paid for this user\'s course access'
}) })
schoolName: {type: 'string'} schoolName: {type: 'string'}
role: {type: 'string'} # unset, 'student', 'teacher', 'parent', 'technology coordinator', 'advisor', 'principal', 'superintendent', ...
c.extendBasicProperties UserSchema, 'user' c.extendBasicProperties UserSchema, 'user'

View file

@ -67,3 +67,6 @@ module.exports =
'store:hero-purchased': c.object {required: ['hero', 'heroSlug']}, 'store:hero-purchased': c.object {required: ['hero', 'heroSlug']},
hero: {type: 'object'} hero: {type: 'object'}
heroSlug: {type: 'string'} heroSlug: {type: 'string'}
'application:service-loaded': c.object {required: ['service']},
service: {type: 'string'} # 'segment'

View file

@ -6,6 +6,8 @@
// TYPOGRAPHY // TYPOGRAPHY
// ----------------------------------------------------- // -----------------------------------------------------
@import url(//fonts.googleapis.com/css?family=Arvo:400,700);
@import url(//fonts.googleapis.com/css?family=Open+Sans:400,300,700&subset=latin,latin-ext,cyrillic,vietnamese,cyrillic-ext,greek-ext,greek);
@import url(//fonts.googleapis.com/css?family=Open+Sans+Condensed:700&subset=latin,latin-ext,cyrillic-ext,greek-ext,greek,vietnamese,cyrillic); @import url(//fonts.googleapis.com/css?family=Open+Sans+Condensed:700&subset=latin,latin-ext,cyrillic-ext,greek-ext,greek,vietnamese,cyrillic);
// SCAFFOLDING // SCAFFOLDING

View file

@ -120,6 +120,21 @@
top: 213px top: 213px
border: 5px solid darkred border: 5px solid darkred
.style-flat .bg-navy
padding: 20px
position: absolute
top: 400px
width: 380px
z-index: 1
.btn
margin: 10px 0
@media screen and ( max-width: 1000px )
display: none
@media screen and ( max-height: 800px )
top: 200px
body[lang='ru'], body[lang^='de'], body[lang^='pt-BR'], body[lang='pl'], body[lang='tr'], body[lang^='nl'], body[lang^='cs'], body[lang^='sv'], body[lang^='el'], body[lang^='hu'], body[lang^='bg'] body[lang='ru'], body[lang^='de'], body[lang^='pt-BR'], body[lang='pl'], body[lang='tr'], body[lang^='nl'], body[lang^='cs'], body[lang^='sv'], body[lang^='el'], body[lang^='hu'], body[lang^='bg']
#home-view #slogan #home-view #slogan
font-size: 22px font-size: 22px

View file

@ -0,0 +1,482 @@
@import "app/styles/bootstrap/variables"
@import "app/styles/mixins"
// TODO: Move flat style into probably several files and Bootstrap variables
// Variables
$headline-font: 'Arvo', serif
$body-font: 'Open Sans', sans-serif
$burgandy: #7D0101
$gold: #F2BE19
$navy: #0E4C60
$forest: #20572B
.style-flat
background: white
// Fonts
h1, h2, h3, h4, h5, h6
// Unsetting game styles
font-variant: normal
color: black
margin: 0
h1
font-family: $headline-font
font-weight: normal
font-size: 46px
line-height: 62px
h2
font-family: $body-font
font-weight: lighter
font-size: 30px
line-height: 42px
h3
font-family: $headline-font
font-weight: normal
font-size: 33px
line-height: 45px
h4
font-family: $body-font
font-weight: lighter
font-size: 22px
line-height: 32px
h5
font-family: $headline-font
font-weight: bold
font-size: 20px
line-height: 31px
h6
font-family: $body-font
font-weight: bold
font-size: 14px
line-height: 20px
p
margin: 0 0 14px
.small
font-weight: normal
font-size: 14px
line-height: 20px
font-family: $body-font
font-size: 18px
line-height: 29px
blockquote
border: none
&:before
font-family: "Monaco"
content: "\201C"
position: absolute
left: 0px
top: 20px
font-size: 40px
opacity: 0.5
// Navbar
.navbar
background: white
margin-bottom: 0
a.navbar-brand
#logo-img
width: 210px
height: 65px
margin-right: 10px
.glyphicon-home
position: relative
top: 4px
color: $burgandy
&:hover
color: white
background: $burgandy
.navbar-toggle
color: black
margin: 30px 25px 0
.nav > li > a
// TODO: Move this to bootstrap variables for navbars
font-weight: bold
font-family: $body-font
font-size: 16px
padding: 38px 15px 37px
color: $burgandy
text-shadow: 0 0 0
&:hover
background: $burgandy
color: white
#language-dropdown-wrapper
display: inline-block
padding: 30px 20px
.language-dropdown
width: 200px
.language-dropdown
width: 250px
@media (max-width: $screen-sm-min)
.nav > li > a
padding: 10px 20px
#language-dropdown-wrapper
display: inline-block
padding: 10px 10px
.language-dropdown
width: 150px
// Buttons
.btn
border: none
border-radius: 5px
font-family: $body-font
font-weight: normal
background-image: none // overrides legacy buttons
.btn-primary, .btn-navy
background-color: $navy
color: white
.btn-primary-alt, .btn-navy-alt
background-color: white
border: 1px solid $navy
color: $navy
.btn-forest
background-color: $forest
color: white
.btn-forest-alt
background-color: white
border: 1px solid $forest
color: $forest
.btn-gold
background-color: $gold
color: white
.btn-gold-alt
background-color: white
border: 1px solid $gold
color: $gold
.btn-lg
font-size: 18px
padding: 13px 35px
// Classes
.text-navy
color: $navy
.bg-navy
background-color: $navy
color: white
h1, h2, h3, h4, h5, h6, a
color: white
a.btn-primary-alt
color: $navy
#new-home-view
#jumbotron-container-fluid
background-image: url("/images/pages/home/character_jumbotron.png")
background-position: 50% 55%
@media (min-width : 1200px)
background-size: 100% auto
@media (max-width : 1200px)
background-size: 1200px auto
.container
min-height: 750px
background-repeat: no-repeat
.btn
margin: 10px 0
h1
color: white
margin-top: 130px
@media (max-width: $screen-md-min)
margin-top: 170px
font-size: 33px
line-height: 45px
.well
border-radius: 8px
background: rgba(255, 255, 255, 0.5)
margin-top: 170px
#classroom-edition-header
margin-top: 40px
#learn-to-code-header
margin-top: 80px
#learn-more-row
margin-top: 80px
h2, h6
color: white
h6
margin-top: 10px
&.alt-image
background-image: url("/images/pages/home/student_jumbotron.png")
background-position: 60% 35%
.container h1
margin-top: 420px
@media (max-width: $screen-md-min)
margin-top: 380px
@media (max-width: $screen-sm-min)
margin-top: 360px
#classroom-in-box-row
margin: 40px 0
#feature-spread-row
.col-sm-4
padding: 40px
img
margin-bottom: 20px
.testimonials-rows
background: $navy
color: white
position: relative
padding: 60px 0 40px
margin: 100px 0
h3, h6
color: white
.testimonials-filler-left
position: absolute
width: 2000px
left: -2000px
top: 0px
background: $navy
height: 100%
.testimonials-filler-right
position: absolute
width: 2000px
right: -2000px
top: 0px
background: $navy
height: 100%
img
margin: 0 auto 10px
.row
margin: 20px 0
#benefit-row-1, #benefit-row-2, #benefit-row-3
margin: 50px 0
#benefit-graphic-1, #benefit-graphic-2, #benefit-graphic-3
padding: 30px 40px
position: relative
min-height: 250px
h2
color: white
width: 50%
#benefit-graphic-1
background: $burgandy
img
position: absolute
right: 0
bottom: 0
#benefit-graphic-1-filler
background: $burgandy
height: 100%
width: 2000px
position: absolute
left: 100%
top: 0
#benefit-graphic-2
background: $navy
h2
float: right
img
position: absolute
left: 0
bottom: 0
#benefit-graphic-2-filler
background: $navy
height: 100%
width: 2000px
position: absolute
right: 100%
top: 0
#benefit-graphic-3
background: $forest
img
position: absolute
right: 0
bottom: 0
#benefit-graphic-3-filler
background: $forest
height: 100%
width: 2000px
position: absolute
left: 100%
top: 0
#school-level-label
margin: 15px 15px 0 0
display: inline-block
#school-level-dropdown
display: inline-block
width: 250px
#request-demo-row
margin: 100px 0
.btn
margin: 20px 10px
#courses-container
display: flex
flex-wrap: wrap
justify-content: space-between
.media
width: 222px
margin: 20px 0 0 0
border: 1px solid navy
border-radius: 8px
padding: 50px 15px 0
text-align: center
position: relative
height: 350px
color: $navy
&:first-child
border-color: $forest
color: $forest
h6
color: $forest
&.disabled
$disabled-color: rgb(155, 155, 155)
border-color: $disabled-color
color: $disabled-color
h6
color: $disabled-color
.media-object
@include filter(grayscale(100%))
h6
padding: 0 5px
color: $navy
.media-object
width: 147px
height: 147px
border-radius: 74px
background-position: -30px -26px
background-repeat: no-repeat
background-size: 312px auto
margin: 15px auto
position: absolute
left: 38px
left: calc((100% - 147px) / 2)
bottom: 50px
.course-duration
position: absolute
bottom: 25px
width: 192px
width: calc(100% - 30px)
padding: 0
.free-course
background-color: $forest
width: 100%
height: 33px
position: absolute
top: 0
left: 0px
border-top-left-radius: 7px
border-top-right-radius: 7px
h6
margin-top: 6px
color: white
.text-center
width: 100%
margin-top: 30px
img
margin-right: 20px
#footer
background-image: url("/images/pages/home/footer_background.png")
height: 229px
margin: -22px auto 0
color: white
@media (max-width: $screen-sm-min)
background-color: #201a15
background-image: none
height: auto
ul
margin: 30px
li:first-child
border-bottom: 1px solid white
margin-bottom: 10px
a
color: white
#final-footer
position: absolute
left: 0
right: 0
height: 60px
color: white
background-color: #463a2c
@media (max-width: $screen-sm-min)
position: inherit
padding: 20px
height: auto
a
color: white
img
width: 150px
margin: 0 10px

View file

@ -64,6 +64,7 @@ block footer
a(href='http://blog.codecombat.com/', data-i18n="nav.blog") a(href='http://blog.codecombat.com/', data-i18n="nav.blog")
a(href='https://jobs.lever.co/codecombat', tabindex=-1, data-i18n="nav.careers") Careers a(href='https://jobs.lever.co/codecombat', tabindex=-1, data-i18n="nav.careers") Careers
a(href='/legal', tabindex=-1, data-i18n="nav.legal") Legal a(href='/legal', tabindex=-1, data-i18n="nav.legal") Legal
a(href='/privacy', tabindex=-1, data-i18n="legal.privacy_title") Privacy
a(href='/contribute', tabindex=-1, data-i18n="nav.contribute") Contribute a(href='/contribute', tabindex=-1, data-i18n="nav.contribute") Contribute
a(href='/play/ladder', tabindex=-1, data-i18n="home.multiplayer") a(href='/play/ladder', tabindex=-1, data-i18n="home.multiplayer")
if me.isAdmin() if me.isAdmin()

View file

@ -38,3 +38,14 @@ block outer_content
strong(data-i18n="home.old_browser") Uh oh, your browser is too old to run CodeCombat. Sorry! strong(data-i18n="home.old_browser") Uh oh, your browser is too old to run CodeCombat. Sorry!
br br
span(data-i18n="home.old_browser_suffix") You can try anyway, but it probably won't work. span(data-i18n="home.old_browser_suffix") You can try anyway, but it probably won't work.
if view.withTeacherNote
.style-flat
.bg-navy
h3 Teachers!
h4 Want the most engaging way to teach programming at your school?
.text-center
a.btn.btn-primary-alt.btn-lg(href="/teachers/quote") Request a Quote
h6
a.small(href="/home") Learn More

View file

@ -29,8 +29,8 @@ block content
| Respectful Best Practices | Respectful Best Practices
p(data-i18n="legal.practices_description") p(data-i18n="legal.practices_description")
| These are our promises to you, the player, in slightly less legalese. | These are our promises to you, the player, in slightly less legalese.
h4(data-i18n="legal.privacy_title") h4
| Privacy a(href="/privacy", data-i18n="legal.privacy_title") Privacy
p(data-i18n="legal.privacy_description") p(data-i18n="legal.privacy_description")
| We will not sell any of your personal information. | We will not sell any of your personal information.
h4(data-i18n="legal.security_title") h4(data-i18n="legal.security_title")

View file

@ -0,0 +1,311 @@
nav.navbar.navbar-default
.container-fluid
.navbar-header
button.navbar-toggle.collapsed(data-toggle='collapse', data-target='#navbar-collapse' aria-expanded='false')
span.sr-only Toggle navigation
span.icon-bar
span.icon-bar
span.icon-bar
a.navbar-brand(href="/")
img#logo-img(src="/images/pages/base/logo.png")
#navbar-collapse.collapse.navbar-collapse
ul.nav.navbar-nav
li
a(href="/about") About
li
a(href="/courses/teachers") Teachers
li
a(href="https://discourse.codecombat.com/") Forum
li
a#create-account-link.signup-button Signup
li
a#login-link.login-button Login
#language-dropdown-wrapper.pull-right.hidden-xs.hidden-sm
select.language-dropdown.form-control
.container-fluid#jumbotron-container-fluid(class=view.jumbotron === 'student' ? 'alt-image' : '')
.container
.row
.col-lg-7.col-md-8.col-sm-8.col-xs-6
h1 The most engaging game for learning programming.
.col-lg-3.col-lg-offset-2.col-md-4.col-sm-4.col-xs-6
.well.text-center
h6#classroom-edition-header Classroom Edition:
div
button#teacher-btn.btn.btn-primary.btn-lg.btn-block I'm a Teacher
div
a.btn.btn-forest.btn-lg.btn-block(href="/courses") I'm a Student
h6#learn-to-code-header Learn to code:
a.btn.btn-gold.btn-lg.btn-block(href=view.playURL) Play Now
.row#learn-more-row
.col-xs-12.text-center
a#learn-more-link
h6 Learn more
h2
span.glyphicon.glyphicon-chevron-down
.container#classroom-in-box-container
#classroom-in-box-row.row
.col-sm-6
h2.text-navy A classroom in-a-box for teaching computer science.
.col-sm-6
p CodeCombat is a platform for students to learn computer science while playing through a real game.
p Our courses have been specifically playtested to excel in a classroom setting, even by teachers with little to no prior programming experience.
#feature-spread-row.row.text-center
h3 Designed with teachers in mind
.col-sm-4
img.img-circle(src="/images/pages/home/F1_typedcode.png")
h4
| Real, typed code
br
| from the first level
p.small Getting students to typed code as quickly as possible is critical to learning programming syntax and proper structure.
.col-sm-4
img.img-circle(src="/images/pages/home/F2_teacherguides.png")
h4
| Educator resources
br
| and course guides
p.small Teaching computer science does not require a costly degree, because we provide tools to support educators of all backgrounds.
.col-sm-4
img.img-circle(src="/images/pages/home/F3_accessible.png")
h4
| Accessible to
br
| everyone
p.small Democratizing the process of learning coding is at the core of our philosophy. Everyone should be able to learn to code.
.testimonials-rows
.testimonials-filler-left
.testimonials-filler-right
.row
.col-lg-offset-2.col-lg-7.col-sm-8
blockquote
h3 I think they actually forgot that they were actually learning something.
.col-lg-2.col-sm-3.text-center
img.img-circle(src="/images/pages/home/timmaki.png")
h6 Tim Maki
.small Director of Technology, Tilton School
.row
.col-lg-2.col-sm-3.col-lg-offset-1.text-center
img.img-circle(src="/images/pages/home/dylan.png")
h6 Dylan
.small 3rd Grader
.col-lg-7.col-sm-8
blockquote
h3 Coding is something I've always wanted to do, and I never thought I would be able to learn it in school.
h3.text-center Why is learning through games important?
#benefit-row-1.row
.col-sm-5
p
| Gaming is a medium that encourages interaction, discovery, and trial-and-error.
| A good game challenges the player to master skills over time,
| which is the same critical process students go through as they learn.
p
| Games excel at rewarding “
a(href="http://blog.mindresearch.org/blog/game-based-learning-infographic-strong-math-practices" target="_blank") productive struggle
span.spr ” - the kind of struggle that results in learning thats engaging and
a(href="http://www.gamesandlearning.org/2014/06/09/teachers-on-using-games-in-class/" target="_blank") motivating
| , not tedious.
.col-sm-6.col-sm-offset-1#benefit-graphic-1
h2 Games reward the productive struggle.
img(src="/images/pages/home/G1_reward.png")
#benefit-graphic-1-filler
#benefit-row-2.row
.col-sm-6#benefit-graphic-2
h2 Studies suggest gaming is good for childrens brains. (its true!)
img(src="/images/pages/home/G2_brains.png")
#benefit-graphic-2-filler
.col-sm-5.col-sm-offset-1
p
span.spr When game-based learning systems are
a(href="http://schoolsweek.co.uk/gaming-is-good-for-childrens-brains-study-suggests/" target="_blank") compared
span.spl.spr against conventional assessment methods, the difference is clear: games are better at helping students retain knowledge, concentrate and
a(href="http://dev.k-12techdecisions.com/article/game_based_learning_is_where_vygotsky_meets_dweck/P3" target="_blank") perform at a higher level of achievement
| .
p
| Games also provide real-time feedback that allows students to adjust their solution path and understand concepts more holistically, instead of being limited to just “correct” or “incorrect” answers.
#benefit-row-3.row
.col-sm-5
p
| A great game is more than just badges and achievements - its about a players journey, well-designed puzzles, and the ability to tackle challenges with agency and confidence.
p
| CodeCombat is a game that gives players that agency and confidence with our robust typed code engine, which helps beginner and advanced students alike write proper, valid code.
.col-sm-6.col-sm-offset-1#benefit-graphic-3
h2 A real game, played with real coding.
img(src="/images/pages/home/G3_game.png")
#benefit-graphic-3-filler
#request-demo-row.text-center
h3 Curious? Request a demo and we'll show you the ropes
h4 Or create a class and see it for yourself!
p
a.btn.btn-primary.btn-lg(href="/teachers/freetrial") Request a Demo
a.btn.btn-primary-alt.btn-lg(href="/courses/teachers") Create a Class
h3.text-center Computer science courses for all ages
h4.text-center
span#school-level-label Show me lesson time estimates for:
select#school-level-dropdown.form-control.text-navy
option(value='elementary') Elementary School
option(value='middle', selected=true) Middle School
option(value='high') High School
h4#semester-duration.text-center
#courses-container
- var conceptsSeen = {};
- var lastScreenshot = "";
for course, courseIndex in view.courses.models
.col-md-3.col-sm-4
.media.course-details(data-course-slug=course.get('slug'))
if courseIndex === 0
.free-course
h6 Free for all students
.media-body(title=course.get('description'))
h6.course-name= course.get('name') + ':'
p.small
- var pastFirstConcept = false;
each concept in course.get('concepts')
- if (conceptsSeen[concept]) continue;
- conceptsSeen[concept] = true;
if pastFirstConcept
span.spr ,
- pastFirstConcept = true;
span(data-i18n="concepts." + concept)
.media-object(style="background-image: url(" + course.get('screenshot') + ")")
- lastScreenshot = course.get('screenshot');
h6.course-duration
span.spr Lesson time:
span.course-hours= course.get('duration') || 0
span.spl.unit(data-i18n="units.hours")
for upcomingCourse in ['Computer Science 6', 'Computer Science 7', 'Computer Science 8']
.col-md-3.col-sm-4
.media.disabled
.media-body
h6.course-name= upcomingCourse + ':'
p.small Coming soon!
img.media-object(src="/images/pages/home/inprogress.png")
h6.course-duration
span.spr Lesson time:
span.course-hours 5
span.spl(data-i18n="units.hours")
.text-center
h4
img(src="/images/pages/home/course_languages.png")
| Courses are available in JavaScript, Python, and Java (coming soon!)
.testimonials-rows
.testimonials-filler-left
.testimonials-filler-right
.row
.col-lg-offset-2.col-lg-7.col-sm-8
blockquote
h3 Boasts riddles that are complex enough to fascinate gamers and coders alike.
.col-lg-2.col-sm-3.text-center
img.img-circle(src="/images/pages/home/opensource.png")
h6 Open Source
.small opensource.com
.row
.col-lg-2.col-sm-3.col-lg-offset-1.text-center
img.img-circle(src="/images/pages/home/pcmag.png")
h6 PC Mag
.small pcmag.com
.col-lg-7.col-sm-8
blockquote
h3 A winning combination of RPG gameplay and programming homework that pulls off making kid-friendly education legitimately enjoyable.
.request-demo-row.text-center
h3 Everything you need to run a computer science class in your school today, no CS background required.
p
a.btn.btn-primary.btn-lg(href="/teachers/freetrial") Request a Demo
a.btn.btn-primary-alt.btn-lg(href="/courses/teachers") Create a Class
.text-center
img(src="/images/pages/home/character_lineup.png")
.container-fluid
#footer.small
.container
.row
.col-sm-3
ul.list-unstyled
li
strong CodeCombat
li
a(href="/about") About
li
a(href="/Careers") Jobs
li
a(href="http://blog.codecombat.com/", data-i18n="nav.blog")
li
a(href="/legal") Legal
.col-sm-3
ul.list-unstyled
li
strong Schools
li
a(href="/courses/teachers") Teachers
li
a(href="https://sites.google.com/a/codecombat.com/teacher-guides/") Educator Wiki
li
a(href="/courses/quote") Request a Quote
.col-sm-3
ul.list-unstyled
li
strong Get Involved
li
a(href='/community', data-i18n="nav.community")
li
a(href="/contribute") Contribute
li
a(href=view.forumLink(), data-i18n="nav.forum")
li
a(href="/play/ladder") Multiplayer
li
a(href="https://github.com/codecombat/codecombat") Open source (GitHub)
.col-sm-3
ul.list-unstyled
li
strong Support
li
a(href="https://discourse.codecombat.com/t/faq-check-before-posting/1027") FAQs
li
a(tabindex=-1, data-toggle="coco-modal", data-target="core/ContactModal", data-i18n="nav.contact")
li
a(href="https://www.facebook.com/codecombat", data-i18n="nav.facebook")
li
a(href="https://twitter.com/codecombat", data-i18n="nav.twitter")
#final-footer.small.text-center
| Copyright ©2016 CodeCombat. All Rights Reserved.
img(src="/images/pages/base/logo.png" alt="CodeCombat")
span.spr Need help? Email
a(href="mailto:team@codecombat.com") team@codecombat.com
span.spl and we'll get in touch!

170
app/templates/privacy.jade Normal file
View file

@ -0,0 +1,170 @@
extends /templates/base
block content
h1
| Privacy Policy
p
em
span.spr (see also our
a(href="/legal") legal page
span )
hr
p This privacy policy has been compiled to better serve those who are concerned with how their 'Personally identifiable information' (PII) is being used online. PII, as used in US privacy law and information security, is information that can be used on its own or with other information to identify, contact, or locate a single person, or to identify an individual in context. Please read our privacy policy carefully to get a clear understanding of how we collect, use, protect or otherwise handle your Personally Identifiable Information in accordance with our website.
p
strong What personal information do we collect from the people that visit our blog, website or app?
p When ordering or registering on our site, as appropriate, you may be asked to enter your email address, credit card information, school name, or other details to help you with your experience.
p
strong When do we collect information?
p We collect information from you when you register on our site, place an order, fill out a form, or enter information on our site.
p
strong How do we use your information?
p We may use the information we collect from you when you register, make a purchase, sign up for our newsletter, respond to a survey or marketing communication, play the game, or use certain other site features in the following ways:
ul
li To personalize user's experience and to allow us to deliver the type of content and product offerings in which you are most interested.
li To improve our website in order to better serve you.
li To allow us to better service you in responding to your customer service requests.
li To quickly process your transactions.
li To send periodic emails regarding your order or other products and services.
p
strong How do we protect visitor information?
p Your personal information is contained behind secured networks and is only accessible by a limited number of persons who have special access rights to such systems, and are required to keep the information confidential. In addition, all sensitive/credit information you supply is encrypted via Secure Socket Layer (SSL) technology.
p We implement a variety of security measures when a user places an order enters, submits, or accesses their information to maintain the safety of your personal information.
p All transactions are processed through a gateway provider and are not stored or processed on our servers.
p
strong Do we use 'cookies'?
p Yes. Cookies are small files that a site or its service provider transfers to your computer's hard drive through your Web browser (if you allow) that enables the site's or service provider's systems to recognize your browser and capture and remember certain information. For instance, we use cookies to help us remember and process the items in your shopping cart. They are also used to help us understand your preferences based on previous or current site activity, which enables us to provide you with improved services. We also use cookies to help us compile aggregate data about site traffic and site interaction so that we can offer better site experiences and tools in the future.
p We use cookies to:
ul
li Understand and save user's preferences for future visits.
li Compile aggregate data about site traffic and site interactions in order to offer better site experiences and tools in the future. We may also use trusted third party services that track this information on our behalf.
p You can choose to have your computer warn you each time a cookie is being sent, or you can choose to turn off all cookies. You do this through your browser (like Internet Explorer) settings. Each browser is a little different, so look at your browser's Help menu to learn the correct way to modify your cookies.
p
strong If users disable cookies in their browser:
p If you disable cookies, it will turn off some of the features that make your site experience more efficient and some of our services will not function properly.
p
strong Third Party Disclosure
p We do not sell, trade, or otherwise transfer to outside parties your personally identifiable information unless we provide you with advance notice. This does not include website hosting partners and other parties who assist us in operating our website, conducting our business, or servicing you, so long as those parties agree to keep this information confidential. We may also release your information when we believe release is appropriate to comply with the law, enforce our site policies, or protect ours or others' rights, property, or safety.
p However, non-personally identifiable visitor information may be provided to other parties for marketing, advertising, or other uses.
p
strong Third party links
p Occasionally, at our discretion, we may include or offer third party products or services on our website. These third party sites have separate and independent privacy policies. We therefore have no responsibility or liability for the content and activities of these linked sites. Nonetheless, we seek to protect the integrity of our site and welcome any feedback about these sites.
p
strong Google
p
span.spr Google's advertising requirements can be summed up by
a(href="https://support.google.com/adwordspolicy/answer/1316548?hl=en") Google's Advertising Principles
span . They are put in place to provide a positive experience for users.
p Google, as a third party vendor, uses cookies to serve ads on our site. Google's use of the DART cookie enables it to serve ads to our users based on their visit to our site and other sites on the Internet. Users may opt out of the use of the DART cookie by visiting the Google ad and content network privacy policy. We have implemented Google's Demographics and Interests Reporting feature.
p We along with third-party vendors, such as Google use first-party cookies (such as the Google Analytics cookies) and third-party cookies (such as the DoubleClick cookie) or other third-party identifiers together to compile data regarding user interactions with ad impressions, and other ad service functions as they relate to our website.
p Opting out: Users can set preferences for how Google advertises to you using the Google Ad Settings page. Alternatively, you can opt out by visiting the Network Advertising initiative opt out page or permanently using the Google Analytics Opt Out Browser add on.
p
strong California Online Privacy Protection Act
p
span.spr CalOPPA is the first state law in the nation to require commercial websites and online services to post a privacy policy. The law's reach stretches well beyond California to require a person or company in the United States (and conceivably the world) that operates websites collecting personally identifiable information from California consumers to post a conspicuous privacy policy on its website stating exactly the information being collected and those individuals with whom it is being shared, and to comply with this policy. See more
a(href="http://consumercal.org/california-online-privacy-protection-act-caloppa/#sthash.0FdRbT51.dpuf") here
| .
p According to CalOPPA: users can visit our site anonymously; we link to this Privacy Policy on the home page; and our Privacy Policy link includes the word 'Privacy', and can be easily be found on the home page.
p Users will be notified of any privacy policy changes on our Privacy Policy Page. Users are able to change their personal information by logging into their account or by emailing us.
p
strong How does our site handle do not track signals?
p We generally attempt to honor Do Not Track (DNT) browser signals when possible but cannot guarantee 100% coverage.
p
strong Does our site allow third party behavioral tracking?
p Yeah.
p
strong COPPA (Children Online Privacy Protection Act)
p When it comes to the collection of personal information from children under 13, the Children's Online Privacy Protection Act (COPPA) puts parents in control. The Federal Trade Commission, the nation's consumer protection agency, enforces the COPPA Rule, which spells out what operators of websites and online services must do to protect children's privacy and safety online.
p We do not specifically market to children under 13. (We market to teachers, parents, and older students.)
p
strong Fair Information Practices
p The Fair Information Practices Principles form the backbone of privacy law in the United States and the concepts they include have played a significant role in the development of data protection laws around the globe. Understanding the Fair Information Practice Principles and how they should be implemented is critical to comply with the various privacy laws that protect personal information.
p In order to be in line with Fair Information Practices, should a data breach occur, we will notify the affected users via email within 7 business days.
p We also agree to the individual redress principle, which requires that individuals have a right to pursue legally enforceable rights against data collectors and processors who fail to adhere to the law. This principle requires not only that individuals have enforceable rights against data users, but also that individuals have recourse to courts or a government agency to investigate and/or prosecute non-compliance by data processors.
p
strong CAN-SPAM Act
p The CAN-SPAM Act is a law that sets the rules for commercial email, establishes requirements for commercial messages, gives recipients the right to have emails stopped from being sent to them, and spells out tough penalties for violations.
p We collect your email address in order to:
ul
li Send information, respond to inquiries, and/or other requests or questions.
li Process orders and to send information and updates pertaining to orders
li We may also send you additional information related to your product and/or service.
li Market to our mailing list or continue to send emails to our clients after the original transaction has occurred
p To be in accordance with CANSPAM we agree to the following:
ul
li NOT use false, or misleading subjects or email addresses
li Identify the message as an advertisement in some reasonable way
li Include the physical address of our business or site headquarters
li Monitor third party email marketing services for compliance, if one is used.
li Honor opt-out/unsubscribe requests quickly
li Allow users to unsubscribe by using the link at the bottom of each email
p If at any time you would like to unsubscribe from receiving future emails, you can change your email settings in your account preferences, email us, or follow the instructions at the bottom of each email, and we will promptly remove you from ALL correspondence.
p
strong Contacting Us
p If there are any questions regarding this privacy policy you may contact us using the information below.
p
span CodeCombat Inc.
br
span 360 3rd St Suite 700 (Livefyre)
br
span San Francisco, CA 94107
br
a(href='mailto:team@codecombat.com') team@codecombat.com
p
em Last Edited on 2016-02-01

View file

@ -8,8 +8,9 @@ module.exports = class HomeView extends RootView
events: events:
'click #play-button': 'onClickPlayButton' 'click #play-button': 'onClickPlayButton'
constructor: -> constructor: (options={}) ->
super() super()
@withTeacherNote = options.withTeacherNote
window.tracker?.trackEvent 'Homepage Loaded', category: 'Homepage' window.tracker?.trackEvent 'Homepage Loaded', category: 'Homepage'
if @getQueryVariable 'hour_of_code' if @getQueryVariable 'hour_of_code'
application.router.navigate "/hoc", trigger: true application.router.navigate "/hoc", trigger: true

View file

@ -0,0 +1,75 @@
RootView = require 'views/core/RootView'
template = require 'templates/new-home-view'
CocoCollection = require 'collections/CocoCollection'
Course = require 'models/Course'
# TODO: auto margin feature paragraphs
module.exports = class NewHomeView extends RootView
id: 'new-home-view'
className: 'style-flat'
template: template
events:
'click #play-btn': 'onClickPlayButton'
'change #school-level-dropdown': 'onChangeSchoolLevelDropdown'
'click #teacher-btn': 'onClickTeacherButton'
'click #learn-more-link': 'onClickLearnMoreLink'
initialize: (options) ->
@jumbotron = options.jumbotron or 'student' # or 'characters'
@courses = new CocoCollection [], {url: "/db/course", model: Course}
@supermodel.loadCollection(@courses, 'courses')
window.tracker?.trackEvent 'Homepage Loaded', category: 'Homepage'
if @getQueryVariable 'hour_of_code'
application.router.navigate "/hoc", trigger: true
isHourOfCodeWeek = false # Temporary: default to /hoc flow during the main event week
if isHourOfCodeWeek and (@isNewPlayer() or (@justPlaysCourses() and me.isAnonymous()))
# Go/return straight to playing single-player HoC course on Play click
@playURL = '/hoc?go=true'
@alternatePlayURL = '/play'
@alternatePlayText = 'home.play_campaign_version'
else if @justPlaysCourses()
# Save players who might be in a classroom from getting into the campaign
@playURL = '/courses'
@alternatePlayURL = '/play'
@alternatePlayText = 'home.play_campaign_version'
else
@playURL = '/play'
onClickPlayButton: (e) ->
@playSound 'menu-button-click'
return if @playURL isnt '/play'
window.tracker?.trackEvent 'Click Play', category: 'Homepage'
afterRender: ->
@onChangeSchoolLevelDropdown()
super()
onChangeSchoolLevelDropdown: (e) ->
levels =
elementary: {'introduction-to-computer-science': '2-4', 'computer-science-5': '15-20', default: '10-15', total: '50-70 hours (about one year)'}
middle: {'introduction-to-computer-science': '1-3', 'computer-science-5': '7-10', default: '5-8', total: '25-35 hours (about one semester)'}
high: {'introduction-to-computer-science': '1', 'computer-science-5': '6-9', default: '5-6', total: '22-28 hours (about one semester)'}
level = if e then $(e.target).val() else 'middle'
@$el.find('#courses-container .course-details').each ->
slug = $(@).data('course-slug')
duration = levels[level][slug] or levels[level].default
$(@).find('.course-duration .course-hours').text duration
$(@).find('.course-duration .unit').text($.i18n.t(if duration is '1' then 'units.hour' else 'units.hours'))
@$el.find('#semester-duration').text levels[level].total
justPlaysCourses: ->
# This heuristic could be better, but currently we don't add to me.get('courseInstances') for single-player anonymous intro courses, so they have to beat a level without choosing a hero.
return me.get('stats')?.gamesCompleted and not me.get('heroConfig')
isNewPlayer: ->
not me.get('stats')?.gamesCompleted and not me.get('heroConfig')
onClickLearnMoreLink: ->
@scrollToLink('#classroom-in-box-container')
onClickTeacherButton: ->
@scrollToLink('#request-demo-row', 600)

View file

@ -0,0 +1,6 @@
RootView = require 'views/core/RootView'
template = require 'templates/privacy'
module.exports = class PrivacyView extends RootView
id: 'privacy-view'
template: template

View file

@ -42,7 +42,7 @@ module.exports = class RequestQuoteView extends RootView
onLoaded: -> onLoaded: ->
if @trialRequests.size() if @trialRequests.size()
@trialRequest = @trialRequests.first() @trialRequest = @trialRequests.first()
me.setRole 'teacher'
super() super()
onSubmitForm: (e) -> onSubmitForm: (e) ->
@ -73,6 +73,7 @@ module.exports = class RequestQuoteView extends RootView
@trialRequest.save() @trialRequest.save()
@trialRequest.on 'sync', @onTrialRequestSubmit, @ @trialRequest.on 'sync', @onTrialRequestSubmit, @
@trialRequest.on 'error', @onTrialRequestError, @ @trialRequest.on 'error', @onTrialRequestError, @
me.setRole attrs.role.toLowerCase(), true
onTrialRequestError: -> onTrialRequestError: ->
@$('#submit-request-btn').text('Submit').attr('disabled', false) @$('#submit-request-btn').text('Submit').attr('disabled', false)

View file

@ -34,3 +34,7 @@ module.exports = class SalesView extends RootView
scrollTop: $('[name="' + $(e.target).closest('a').attr('href').substr(1) + '"]').offset().top scrollTop: $('[name="' + $(e.target).closest('a').attr('href').substr(1) + '"]').offset().top
}, 300) }, 300)
false false
constructor: ->
super arguments...
me.setRole 'teacher'

View file

@ -204,7 +204,7 @@ module.exports = class CocoView extends Backbone.View
return visibleModal.hide() if visibleModal.$el.is(':visible') # close, then this will get called again return visibleModal.hide() if visibleModal.$el.is(':visible') # close, then this will get called again
return @modalClosed(visibleModal) # was closed, but modalClosed was not called somehow return @modalClosed(visibleModal) # was closed, but modalClosed was not called somehow
modalView.render() modalView.render()
$('#modal-wrapper').empty().append modalView.el $('#modal-wrapper').removeClass('hide').empty().append modalView.el
modalView.afterInsert() modalView.afterInsert()
visibleModal = modalView visibleModal = modalView
modalOptions = {show: true, backdrop: if modalView.closesOnClickOutside then true else 'static'} modalOptions = {show: true, backdrop: if modalView.closesOnClickOutside then true else 'static'}
@ -219,6 +219,7 @@ module.exports = class CocoView extends Backbone.View
visibleModal = null visibleModal = null
window.currentModal = null window.currentModal = null
#$('#modal-wrapper .modal').off 'hidden.bs.modal', @modalClosed #$('#modal-wrapper .modal').off 'hidden.bs.modal', @modalClosed
$('#modal-wrapper').addClass('hide')
if waitingModal if waitingModal
wm = waitingModal wm = waitingModal
waitingModal = null waitingModal = null
@ -439,6 +440,11 @@ module.exports = class CocoView extends Backbone.View
slider.on('slidechange', changeCallback) slider.on('slidechange', changeCallback)
slider slider
scrollToLink: (link, speed=300) ->
$('#page-container').animate({
scrollTop: $(link).offset().top
}, speed)
toggleFullscreen: (e) -> toggleFullscreen: (e) ->
# https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Using_full_screen_mode?redirectlocale=en-US&redirectslug=Web/Guide/DOM/Using_full_screen_mode # https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Using_full_screen_mode?redirectlocale=en-US&redirectslug=Web/Guide/DOM/Using_full_screen_mode
# Whoa, even cooler: https://developer.mozilla.org/en-US/docs/WebAPI/Pointer_Lock # Whoa, even cooler: https://developer.mozilla.org/en-US/docs/WebAPI/Pointer_Lock

View file

@ -40,6 +40,7 @@ module.exports = class PurchaseCoursesView extends RootView
onLoaded: -> onLoaded: ->
@pricePerStudent = @products.findWhere({name: 'course'}).get('amount') @pricePerStudent = @products.findWhere({name: 'course'}).get('amount')
me.setRole 'teacher'
super() super()
getPriceString: -> '$' + (@getPrice()/100).toFixed(2) getPriceString: -> '$' + (@getPrice()/100).toFixed(2)

View file

@ -90,6 +90,7 @@ module.exports = class TeacherCoursesView extends RootView
onLoaded: -> onLoaded: ->
super() super()
me.setRole 'teacher'
@addFreeCourseInstances() @addFreeCourseInstances()
addFreeCourseInstances: -> addFreeCourseInstances: ->

View file

@ -31,9 +31,11 @@ schoolCounts.sort(function(a, b) {
return 1; return 1;
}); });
var count = 0;
for (var i = 0; i < schoolCounts.length; i++) { for (var i = 0; i < schoolCounts.length; i++) {
if (schoolCounts[i].count >= 10) if (schoolCounts[i].count >= 2) {
print(schoolCounts[i].count, schoolCounts[i].schoolName); print(++count, '\t', schoolCounts[i].count, schoolCounts[i].schoolName);
}
} }
log("Script runtime: " + (new Date() - scriptStartTime)); log("Script runtime: " + (new Date() - scriptStartTime));

View file

@ -32,6 +32,7 @@ UserSchema.index({'stripe.subscriptionID': 1}, {unique: true, sparse: true})
UserSchema.index({'siteref': 1}, {name: 'siteref index', sparse: true}) UserSchema.index({'siteref': 1}, {name: 'siteref index', sparse: true})
UserSchema.index({'schoolName': 1}, {name: 'schoolName index', sparse: true}) UserSchema.index({'schoolName': 1}, {name: 'schoolName index', sparse: true})
UserSchema.index({'country': 1}, {name: 'country index', sparse: true}) UserSchema.index({'country': 1}, {name: 'country index', sparse: true})
UserSchema.index({'role': 1}, {name: 'role index', sparse: true})
UserSchema.post('init', -> UserSchema.post('init', ->
@set('anonymous', false) if @get('email') @set('anonymous', false) if @get('email')
@ -315,7 +316,7 @@ UserSchema.statics.privateProperties = [
'permissions', 'email', 'mailChimp', 'firstName', 'lastName', 'gender', 'facebookID', 'permissions', 'email', 'mailChimp', 'firstName', 'lastName', 'gender', 'facebookID',
'gplusID', 'music', 'volume', 'aceConfig', 'employerAt', 'signedEmployerAgreement', 'gplusID', 'music', 'volume', 'aceConfig', 'employerAt', 'signedEmployerAgreement',
'emailSubscriptions', 'emails', 'activity', 'stripe', 'stripeCustomerID', 'chinaVersion', 'country', 'emailSubscriptions', 'emails', 'activity', 'stripe', 'stripeCustomerID', 'chinaVersion', 'country',
'schoolName', 'ageRange' 'schoolName', 'ageRange', 'role'
] ]
UserSchema.statics.jsonSchema = jsonschema UserSchema.statics.jsonSchema = jsonschema
UserSchema.statics.editableProperties = [ UserSchema.statics.editableProperties = [
@ -323,7 +324,7 @@ UserSchema.statics.editableProperties = [
'firstName', 'lastName', 'gender', 'ageRange', 'facebookID', 'gplusID', 'emails', 'firstName', 'lastName', 'gender', 'ageRange', 'facebookID', 'gplusID', 'emails',
'testGroupNumber', 'music', 'hourOfCode', 'hourOfCodeComplete', 'preferredLanguage', 'testGroupNumber', 'music', 'hourOfCode', 'hourOfCodeComplete', 'preferredLanguage',
'wizard', 'aceConfig', 'autocastDelay', 'lastLevel', 'jobProfile', 'savedEmployerFilterAlerts', 'wizard', 'aceConfig', 'autocastDelay', 'lastLevel', 'jobProfile', 'savedEmployerFilterAlerts',
'heroConfig', 'iosIdentifierForVendor', 'siteref', 'referrer', 'schoolName' 'heroConfig', 'iosIdentifierForVendor', 'siteref', 'referrer', 'schoolName', 'role'
] ]
UserSchema.plugin plugins.NamedPlugin UserSchema.plugin plugins.NamedPlugin