diff --git a/app/assets/images/pages/home/F1_typedcode.png b/app/assets/images/pages/home/F1_typedcode.png new file mode 100755 index 000000000..dcf337a05 Binary files /dev/null and b/app/assets/images/pages/home/F1_typedcode.png differ diff --git a/app/assets/images/pages/home/F2_teacherguides.png b/app/assets/images/pages/home/F2_teacherguides.png new file mode 100755 index 000000000..748a66f3d Binary files /dev/null and b/app/assets/images/pages/home/F2_teacherguides.png differ diff --git a/app/assets/images/pages/home/F3_accessible.png b/app/assets/images/pages/home/F3_accessible.png new file mode 100755 index 000000000..557e712e5 Binary files /dev/null and b/app/assets/images/pages/home/F3_accessible.png differ diff --git a/app/assets/images/pages/home/G1_reward.png b/app/assets/images/pages/home/G1_reward.png new file mode 100755 index 000000000..505580924 Binary files /dev/null and b/app/assets/images/pages/home/G1_reward.png differ diff --git a/app/assets/images/pages/home/G2_brains.png b/app/assets/images/pages/home/G2_brains.png new file mode 100755 index 000000000..6e39bc510 Binary files /dev/null and b/app/assets/images/pages/home/G2_brains.png differ diff --git a/app/assets/images/pages/home/G3_game.png b/app/assets/images/pages/home/G3_game.png new file mode 100755 index 000000000..bc0f4e8b1 Binary files /dev/null and b/app/assets/images/pages/home/G3_game.png differ diff --git a/app/assets/images/pages/home/character_jumbotron.png b/app/assets/images/pages/home/character_jumbotron.png new file mode 100755 index 000000000..800e5d6a0 Binary files /dev/null and b/app/assets/images/pages/home/character_jumbotron.png differ diff --git a/app/assets/images/pages/home/character_lineup.png b/app/assets/images/pages/home/character_lineup.png new file mode 100755 index 000000000..8975556f8 Binary files /dev/null and b/app/assets/images/pages/home/character_lineup.png differ diff --git a/app/assets/images/pages/home/course1.png b/app/assets/images/pages/home/course1.png new file mode 100755 index 000000000..da12c404d Binary files /dev/null and b/app/assets/images/pages/home/course1.png differ diff --git a/app/assets/images/pages/home/course2.png b/app/assets/images/pages/home/course2.png new file mode 100755 index 000000000..1657d3283 Binary files /dev/null and b/app/assets/images/pages/home/course2.png differ diff --git a/app/assets/images/pages/home/course3.png b/app/assets/images/pages/home/course3.png new file mode 100755 index 000000000..05a4ebf7e Binary files /dev/null and b/app/assets/images/pages/home/course3.png differ diff --git a/app/assets/images/pages/home/course4.png b/app/assets/images/pages/home/course4.png new file mode 100755 index 000000000..2f86b8e21 Binary files /dev/null and b/app/assets/images/pages/home/course4.png differ diff --git a/app/assets/images/pages/home/course5.png b/app/assets/images/pages/home/course5.png new file mode 100755 index 000000000..8b90d34b3 Binary files /dev/null and b/app/assets/images/pages/home/course5.png differ diff --git a/app/assets/images/pages/home/course_languages.png b/app/assets/images/pages/home/course_languages.png new file mode 100755 index 000000000..2f3601b68 Binary files /dev/null and b/app/assets/images/pages/home/course_languages.png differ diff --git a/app/assets/images/pages/home/dylan.png b/app/assets/images/pages/home/dylan.png new file mode 100755 index 000000000..4ff1442b3 Binary files /dev/null and b/app/assets/images/pages/home/dylan.png differ diff --git a/app/assets/images/pages/home/footer_background.png b/app/assets/images/pages/home/footer_background.png new file mode 100755 index 000000000..d3bf68b5a Binary files /dev/null and b/app/assets/images/pages/home/footer_background.png differ diff --git a/app/assets/images/pages/home/inprogress.png b/app/assets/images/pages/home/inprogress.png new file mode 100755 index 000000000..4cb254af7 Binary files /dev/null and b/app/assets/images/pages/home/inprogress.png differ diff --git a/app/assets/images/pages/home/opensource.png b/app/assets/images/pages/home/opensource.png new file mode 100755 index 000000000..789e3a8c8 Binary files /dev/null and b/app/assets/images/pages/home/opensource.png differ diff --git a/app/assets/images/pages/home/pcmag.png b/app/assets/images/pages/home/pcmag.png new file mode 100755 index 000000000..e963f5e3a Binary files /dev/null and b/app/assets/images/pages/home/pcmag.png differ diff --git a/app/assets/images/pages/home/student_jumbotron.png b/app/assets/images/pages/home/student_jumbotron.png new file mode 100755 index 000000000..ec00a91ed Binary files /dev/null and b/app/assets/images/pages/home/student_jumbotron.png differ diff --git a/app/assets/images/pages/home/timmaki.png b/app/assets/images/pages/home/timmaki.png new file mode 100755 index 000000000..97d7118c2 Binary files /dev/null and b/app/assets/images/pages/home/timmaki.png differ diff --git a/app/assets/main.html b/app/assets/main.html index 8d3018d5f..7ad61f2cd 100644 --- a/app/assets/main.html +++ b/app/assets/main.html @@ -40,16 +40,18 @@ - + + @@ -104,7 +106,7 @@ mixpanel.init("e71a4e60db7e1dc5e685be96776280f9");
- +
diff --git a/app/core/Router.coffee b/app/core/Router.coffee index 77ae180a5..76d3a38fd 100644 --- a/app/core/Router.coffee +++ b/app/core/Router.coffee @@ -13,7 +13,13 @@ module.exports = class CocoRouter extends Backbone.Router @initializeSocialMediaServices = _.once @initializeSocialMediaServices 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') @@ -93,6 +99,7 @@ module.exports = class CocoRouter extends Backbone.Router 'github/*path': 'routeToServer' 'hoc': go('courses/HourOfCodeView') + 'home': go('NewHomeView') 'i18n': go('i18n/I18NHomeView') 'i18n/thang/:handle': go('i18n/I18NEditThangTypeView') @@ -118,6 +125,8 @@ module.exports = class CocoRouter extends Backbone.Router 'preview': go('HomeView') + 'privacy': go('PrivacyView') + 'schools': go('SalesView') 'teachers': go('TeachersView') @@ -137,15 +146,15 @@ module.exports = class CocoRouter extends Backbone.Router removeTrailingSlash: (e) -> @navigate e, {trigger: true} - routeDirectly: (path, args) -> + routeDirectly: (path, args, options={}) -> path = "views/#{path}" if not _.string.startsWith(path, 'views/') ViewClass = @tryToLoadModule path if not ViewClass and application.moduleLoader.load(path) @listenToOnce application.moduleLoader, 'load-complete', -> - @routeDirectly(path, args) + @routeDirectly(path, args, options) return 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() @openView(view) diff --git a/app/core/Tracker.coffee b/app/core/Tracker.coffee index 7f59242a7..85fa41ffc 100644 --- a/app/core/Tracker.coffee +++ b/app/core/Tracker.coffee @@ -1,12 +1,17 @@ {me} = require 'core/auth' SuperModel = require 'models/SuperModel' utils = require 'core/utils' +CocoClass = require 'core/CocoClass' debugAnalytics = false targetInspectJSLevelSlugs = ['cupboards-of-kithgard'] -module.exports = class Tracker +module.exports = class Tracker extends CocoClass + subscriptions: + 'application:service-loaded': 'onServiceLoaded' + constructor: -> + super() if window.tracker console.error 'Overwrote our Tracker!', window.tracker window.tracker = @ @@ -14,6 +19,7 @@ module.exports = class Tracker @trackReferrers() @identify() @supermodel = new SuperModel() + @updateRole() if me.get 'role' enableInspectletJS: (levelSlug) -> # InspectletJS loading is delayed and targeting specific levels for more focused investigations @@ -30,7 +36,7 @@ module.exports = class Tracker insp.async = true insp.id = 'inspsync' 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 x = document.getElementsByTagName('script')[0] @inspectletScriptNode = x.parentNode.insertBefore insp, x @@ -62,8 +68,11 @@ module.exports = class Tracker @explicitTraits ?= {} @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) + if @isTeacher() + traits.teacher = true + console.log 'Would identify', me.id, traits if debugAnalytics return unless @isProduction and not me.isAdmin() @@ -81,7 +90,11 @@ module.exports = class Tracker mixpanel.identify(me.id) 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() url = "/#{name}" console.log "Would track analytics pageview: #{url}" if debugAnalytics @@ -93,9 +106,17 @@ module.exports = class Tracker ga? 'send', 'pageview', url # 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 + 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=[]) => @trackEventInternal action, _.cloneDeep properties unless me?.isAdmin() and @isProduction console.log 'Tracking external analytics event:', action, properties, includeIntegrations if debugAnalytics @@ -119,35 +140,44 @@ module.exports = class Tracker # Only log explicit events for now 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) => # Skipping heavily logged actions we don't use internally - unless event in ['Simulator Result', 'Started Level Load', 'Finished Level Load'] - # Trimming properties we don't use internally - # 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'] - delete properties.category - delete properties.label - else if event in ['Loaded World Map', 'Started Signup', 'Finished Signup', 'Login', 'Facebook Login', 'Google Login', 'Show subscription modal'] - delete properties.category + return if event in ['Simulator Result', 'Started Level Load', 'Finished Level Load'] + # Trimming properties we don't use internally + # 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'] + delete properties.category + delete properties.label + else if event in ['Loaded World Map', 'Started Signup', 'Finished Signup', 'Login', 'Facebook Login', 'Google Login', 'Show subscription modal'] + delete properties.category - properties[key] = value for key, value of @explicitTraits if @explicitTraits? - console.log 'Tracking internal analytics event:', event, properties if debugAnalytics - if @isProduction - eventObject = {} - eventObject["event"] = event - eventObject["properties"] = properties unless _.isEmpty properties - eventObject["user"] = me.id - dataToSend = JSON.stringify eventObject - # console.log dataToSend if debugAnalytics - $.post("#{window.location.protocol or 'http:'}//analytics.codecombat.com/analytics", dataToSend).fail -> - console.error "Analytics post failed!" - else - request = @supermodel.addRequestResource { - url: '/db/analytics.log.event/-/log_event' - data: {event: event, properties: properties} - method: 'POST' - }, 0 - request.load() + properties[key] = value for key, value of @explicitTraits if @explicitTraits? + console.log 'Tracking internal analytics event:', event, properties if debugAnalytics + if @isProduction + eventObject = {} + eventObject["event"] = event + eventObject["properties"] = properties unless _.isEmpty properties + eventObject["user"] = me.id + dataToSend = JSON.stringify eventObject + # console.log dataToSend if debugAnalytics + $.post("#{window.location.protocol or 'http:'}//analytics.codecombat.com/analytics", dataToSend).fail -> + console.error "Analytics post failed!" + else + request = @supermodel.addRequestResource { + url: '/db/analytics.log.event/-/log_event' + data: {event: event, properties: properties} + method: 'POST' + }, 0 + request.load() trackTiming: (duration, category, variable, label) -> # https://developers.google.com/analytics/devguides/collection/analyticsjs/user-timings @@ -155,3 +185,18 @@ module.exports = class Tracker console.log 'Would track timing event:', arguments if debugAnalytics return unless me and @isProduction and not me.isAdmin() 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() diff --git a/app/core/services/segment.coffee b/app/core/services/segment.coffee new file mode 100644 index 000000000..80bb38907 --- /dev/null +++ b/app/core/services/segment.coffee @@ -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 diff --git a/app/models/User.coffee b/app/models/User.coffee index 4c03a68f1..701da621a 100644 --- a/app/models/User.coffee +++ b/app/models/User.coffee @@ -59,6 +59,15 @@ module.exports = class User extends CocoModel 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 b = 100 c = b @@ -125,6 +134,21 @@ module.exports = class User extends CocoModel application.tracker.identify announcesActionAudioGroup: @announcesActionAudioGroup unless me.isAdmin() @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. # ... 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. diff --git a/app/schemas/models/user.coffee b/app/schemas/models/user.coffee index c1b1766d5..70e3c718b 100644 --- a/app/schemas/models/user.coffee +++ b/app/schemas/models/user.coffee @@ -327,6 +327,7 @@ _.extend UserSchema.properties, description: 'Prepaid which has paid for this user\'s course access' }) schoolName: {type: 'string'} + role: {type: 'string'} # unset, 'student', 'teacher', 'parent', 'technology coordinator', 'advisor', 'principal', 'superintendent', ... c.extendBasicProperties UserSchema, 'user' diff --git a/app/schemas/subscriptions/misc.coffee b/app/schemas/subscriptions/misc.coffee index c94a1a67d..9fb7ace35 100644 --- a/app/schemas/subscriptions/misc.coffee +++ b/app/schemas/subscriptions/misc.coffee @@ -67,3 +67,6 @@ module.exports = 'store:hero-purchased': c.object {required: ['hero', 'heroSlug']}, hero: {type: 'object'} heroSlug: {type: 'string'} + + 'application:service-loaded': c.object {required: ['service']}, + service: {type: 'string'} # 'segment' diff --git a/app/styles/bootstrap/_bootswatch.scss b/app/styles/bootstrap/_bootswatch.scss index dcb68f657..9ca7bea96 100644 --- a/app/styles/bootstrap/_bootswatch.scss +++ b/app/styles/bootstrap/_bootswatch.scss @@ -6,6 +6,8 @@ // 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); // SCAFFOLDING diff --git a/app/styles/home.sass b/app/styles/home.sass index 03f647511..61458923f 100644 --- a/app/styles/home.sass +++ b/app/styles/home.sass @@ -119,6 +119,21 @@ .alert top: 213px 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'] #home-view #slogan diff --git a/app/styles/new-home-view.sass b/app/styles/new-home-view.sass new file mode 100644 index 000000000..5afcefe2e --- /dev/null +++ b/app/styles/new-home-view.sass @@ -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 \ No newline at end of file diff --git a/app/templates/base.jade b/app/templates/base.jade index c8bd3c72b..52f81770d 100644 --- a/app/templates/base.jade +++ b/app/templates/base.jade @@ -64,6 +64,7 @@ block footer 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='/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='/play/ladder', tabindex=-1, data-i18n="home.multiplayer") if me.isAdmin() diff --git a/app/templates/home-view.jade b/app/templates/home-view.jade index 073116616..af53357e4 100644 --- a/app/templates/home-view.jade +++ b/app/templates/home-view.jade @@ -38,3 +38,14 @@ block outer_content strong(data-i18n="home.old_browser") Uh oh, your browser is too old to run CodeCombat. Sorry! br 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 + \ No newline at end of file diff --git a/app/templates/legal.jade b/app/templates/legal.jade index 64bac794d..3a1554d33 100644 --- a/app/templates/legal.jade +++ b/app/templates/legal.jade @@ -29,8 +29,8 @@ block content | Respectful Best Practices p(data-i18n="legal.practices_description") | These are our promises to you, the player, in slightly less legalese. - h4(data-i18n="legal.privacy_title") - | Privacy + h4 + a(href="/privacy", data-i18n="legal.privacy_title") Privacy p(data-i18n="legal.privacy_description") | We will not sell any of your personal information. h4(data-i18n="legal.security_title") diff --git a/app/templates/new-home-view.jade b/app/templates/new-home-view.jade new file mode 100644 index 000000000..416c5a896 --- /dev/null +++ b/app/templates/new-home-view.jade @@ -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 that’s 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 children’s brains. (it’s 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 - it’s about a player’s 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! + \ No newline at end of file diff --git a/app/templates/privacy.jade b/app/templates/privacy.jade new file mode 100644 index 000000000..f388fd55d --- /dev/null +++ b/app/templates/privacy.jade @@ -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 diff --git a/app/views/HomeView.coffee b/app/views/HomeView.coffee index a27d89c10..63062281d 100644 --- a/app/views/HomeView.coffee +++ b/app/views/HomeView.coffee @@ -8,8 +8,9 @@ module.exports = class HomeView extends RootView events: 'click #play-button': 'onClickPlayButton' - constructor: -> + constructor: (options={}) -> super() + @withTeacherNote = options.withTeacherNote window.tracker?.trackEvent 'Homepage Loaded', category: 'Homepage' if @getQueryVariable 'hour_of_code' application.router.navigate "/hoc", trigger: true diff --git a/app/views/NewHomeView.coffee b/app/views/NewHomeView.coffee new file mode 100644 index 000000000..a8806d1e8 --- /dev/null +++ b/app/views/NewHomeView.coffee @@ -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) diff --git a/app/views/PrivacyView.coffee b/app/views/PrivacyView.coffee new file mode 100644 index 000000000..071f4bba3 --- /dev/null +++ b/app/views/PrivacyView.coffee @@ -0,0 +1,6 @@ +RootView = require 'views/core/RootView' +template = require 'templates/privacy' + +module.exports = class PrivacyView extends RootView + id: 'privacy-view' + template: template diff --git a/app/views/RequestQuoteView.coffee b/app/views/RequestQuoteView.coffee index 7f39fb966..4d1a2b4f2 100644 --- a/app/views/RequestQuoteView.coffee +++ b/app/views/RequestQuoteView.coffee @@ -27,24 +27,24 @@ formSchema = { 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() - + me.setRole 'teacher' super() - + onSubmitForm: (e) -> e.preventDefault() form = @$('form') @@ -73,6 +73,7 @@ module.exports = class RequestQuoteView extends RootView @trialRequest.save() @trialRequest.on 'sync', @onTrialRequestSubmit, @ @trialRequest.on 'error', @onTrialRequestError, @ + me.setRole attrs.role.toLowerCase(), true onTrialRequestError: -> @$('#submit-request-btn').text('Submit').attr('disabled', false) @@ -87,16 +88,16 @@ module.exports = class RequestQuoteView extends RootView }) @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: { + initialValues: { email: props.email schoolName: props.organization } }) @openModalView(modal) - window.nextURL = '/courses/teachers' unless @trialRequest.isNew() \ No newline at end of file + window.nextURL = '/courses/teachers' unless @trialRequest.isNew() diff --git a/app/views/SalesView.coffee b/app/views/SalesView.coffee index 410ce7f09..e0fa7961c 100644 --- a/app/views/SalesView.coffee +++ b/app/views/SalesView.coffee @@ -34,3 +34,7 @@ module.exports = class SalesView extends RootView scrollTop: $('[name="' + $(e.target).closest('a').attr('href').substr(1) + '"]').offset().top }, 300) false + + constructor: -> + super arguments... + me.setRole 'teacher' diff --git a/app/views/core/CocoView.coffee b/app/views/core/CocoView.coffee index fd6e7f0d5..fc6175994 100644 --- a/app/views/core/CocoView.coffee +++ b/app/views/core/CocoView.coffee @@ -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 @modalClosed(visibleModal) # was closed, but modalClosed was not called somehow modalView.render() - $('#modal-wrapper').empty().append modalView.el + $('#modal-wrapper').removeClass('hide').empty().append modalView.el modalView.afterInsert() visibleModal = modalView modalOptions = {show: true, backdrop: if modalView.closesOnClickOutside then true else 'static'} @@ -219,6 +219,7 @@ module.exports = class CocoView extends Backbone.View visibleModal = null window.currentModal = null #$('#modal-wrapper .modal').off 'hidden.bs.modal', @modalClosed + $('#modal-wrapper').addClass('hide') if waitingModal wm = waitingModal waitingModal = null @@ -438,6 +439,11 @@ module.exports = class CocoView extends Backbone.View slider.on('slide', changeCallback) slider.on('slidechange', changeCallback) slider + + scrollToLink: (link, speed=300) -> + $('#page-container').animate({ + scrollTop: $(link).offset().top + }, speed) 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 diff --git a/app/views/courses/PurchaseCoursesView.coffee b/app/views/courses/PurchaseCoursesView.coffee index bf721b031..2d10e98ef 100644 --- a/app/views/courses/PurchaseCoursesView.coffee +++ b/app/views/courses/PurchaseCoursesView.coffee @@ -37,9 +37,10 @@ module.exports = class PurchaseCoursesView extends RootView events: 'input #students-input': 'onInputStudentsInput' 'click #purchase-btn': 'onClickPurchaseButton' - + onLoaded: -> @pricePerStudent = @products.findWhere({name: 'course'}).get('amount') + me.setRole 'teacher' super() getPriceString: -> '$' + (@getPrice()/100).toFixed(2) @@ -72,8 +73,8 @@ module.exports = class PurchaseCoursesView extends RootView updatePrice: -> @renderSelectors '#price-form-group' - - numberOfStudentsIsValid: -> @numberOfStudents > 0 and @numberOfStudents < 100000 + + numberOfStudentsIsValid: -> @numberOfStudents > 0 and @numberOfStudents < 100000 onClickPurchaseButton: -> return @openModalView new AuthModal() if me.isAnonymous() diff --git a/app/views/courses/TeacherCoursesView.coffee b/app/views/courses/TeacherCoursesView.coffee index 4b27c5c0c..93a5b3659 100644 --- a/app/views/courses/TeacherCoursesView.coffee +++ b/app/views/courses/TeacherCoursesView.coffee @@ -90,6 +90,7 @@ module.exports = class TeacherCoursesView extends RootView onLoaded: -> super() + me.setRole 'teacher' @addFreeCourseInstances() addFreeCourseInstances: -> diff --git a/scripts/analytics/mongodb/queries/schoolCounts.js b/scripts/analytics/mongodb/queries/schoolCounts.js index b8aa2f054..58ee08671 100644 --- a/scripts/analytics/mongodb/queries/schoolCounts.js +++ b/scripts/analytics/mongodb/queries/schoolCounts.js @@ -31,9 +31,11 @@ schoolCounts.sort(function(a, b) { return 1; }); +var count = 0; for (var i = 0; i < schoolCounts.length; i++) { - if (schoolCounts[i].count >= 10) - print(schoolCounts[i].count, schoolCounts[i].schoolName); + if (schoolCounts[i].count >= 2) { + print(++count, '\t', schoolCounts[i].count, schoolCounts[i].schoolName); + } } log("Script runtime: " + (new Date() - scriptStartTime)); diff --git a/server/users/User.coffee b/server/users/User.coffee index 62953b0cd..887d16584 100644 --- a/server/users/User.coffee +++ b/server/users/User.coffee @@ -32,6 +32,7 @@ UserSchema.index({'stripe.subscriptionID': 1}, {unique: true, sparse: true}) UserSchema.index({'siteref': 1}, {name: 'siteref index', sparse: true}) UserSchema.index({'schoolName': 1}, {name: 'schoolName index', sparse: true}) UserSchema.index({'country': 1}, {name: 'country index', sparse: true}) +UserSchema.index({'role': 1}, {name: 'role index', sparse: true}) UserSchema.post('init', -> @set('anonymous', false) if @get('email') @@ -315,7 +316,7 @@ UserSchema.statics.privateProperties = [ 'permissions', 'email', 'mailChimp', 'firstName', 'lastName', 'gender', 'facebookID', 'gplusID', 'music', 'volume', 'aceConfig', 'employerAt', 'signedEmployerAgreement', 'emailSubscriptions', 'emails', 'activity', 'stripe', 'stripeCustomerID', 'chinaVersion', 'country', - 'schoolName', 'ageRange' + 'schoolName', 'ageRange', 'role' ] UserSchema.statics.jsonSchema = jsonschema UserSchema.statics.editableProperties = [ @@ -323,7 +324,7 @@ UserSchema.statics.editableProperties = [ 'firstName', 'lastName', 'gender', 'ageRange', 'facebookID', 'gplusID', 'emails', 'testGroupNumber', 'music', 'hourOfCode', 'hourOfCodeComplete', 'preferredLanguage', 'wizard', 'aceConfig', 'autocastDelay', 'lastLevel', 'jobProfile', 'savedEmployerFilterAlerts', - 'heroConfig', 'iosIdentifierForVendor', 'siteref', 'referrer', 'schoolName' + 'heroConfig', 'iosIdentifierForVendor', 'siteref', 'referrer', 'schoolName', 'role' ] UserSchema.plugin plugins.NamedPlugin