Merge branch 'master' into production
BIN
app/assets/images/pages/contribute/class_detail_adventurer.png
Normal file
After Width: | Height: | Size: 89 KiB |
BIN
app/assets/images/pages/contribute/class_detail_ambassador.png
Normal file
After Width: | Height: | Size: 81 KiB |
BIN
app/assets/images/pages/contribute/class_detail_archmage.png
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
app/assets/images/pages/contribute/class_detail_artisan.png
Normal file
After Width: | Height: | Size: 84 KiB |
BIN
app/assets/images/pages/contribute/class_detail_diplomat.png
Normal file
After Width: | Height: | Size: 92 KiB |
BIN
app/assets/images/pages/contribute/class_detail_scribe.png
Normal file
After Width: | Height: | Size: 78 KiB |
BIN
app/assets/images/pages/contribute/tile_adventurer.png
Normal file
After Width: | Height: | Size: 89 KiB |
BIN
app/assets/images/pages/contribute/tile_ambassador.png
Normal file
After Width: | Height: | Size: 64 KiB |
BIN
app/assets/images/pages/contribute/tile_archmage.png
Normal file
After Width: | Height: | Size: 61 KiB |
BIN
app/assets/images/pages/contribute/tile_artisan.png
Normal file
After Width: | Height: | Size: 57 KiB |
BIN
app/assets/images/pages/contribute/tile_diplomat.png
Normal file
After Width: | Height: | Size: 75 KiB |
BIN
app/assets/images/pages/contribute/tile_scribe.png
Normal file
After Width: | Height: | Size: 75 KiB |
|
@ -477,12 +477,13 @@
|
|||
contact:
|
||||
contact_us: "Contact CodeCombat"
|
||||
welcome: "Good to hear from you! Use this form to send us email. "
|
||||
contribute_prefix: "If you're interested in contributing, check out our "
|
||||
contribute_page: "contribute page"
|
||||
contribute_suffix: "!"
|
||||
forum_prefix: "For anything public, please try "
|
||||
forum_page: "our forum"
|
||||
forum_suffix: " instead."
|
||||
subscribe_prefix: "If you need help figuring out a level, please"
|
||||
subscribe: "buy a CodeCombat subscription"
|
||||
subscribe_suffix: "and we'll be happy to help you with your code."
|
||||
subscriber_support: "Since you're a CodeCombat subscriber, your email will get our priority support."
|
||||
where_reply: "Where should we reply?"
|
||||
send: "Send Feedback"
|
||||
contact_candidate: "Contact Candidate" # Deprecated
|
||||
|
|
25
app/styles/contribute.sass
Normal file
|
@ -0,0 +1,25 @@
|
|||
#contribute-view
|
||||
.class_tile
|
||||
position: relative
|
||||
width: 330px
|
||||
padding: 5px
|
||||
float: left
|
||||
|
||||
&:hover img
|
||||
outline: 3px solid #161a9e
|
||||
|
||||
.class_text
|
||||
position: absolute
|
||||
bottom: 5px
|
||||
width: 300px
|
||||
padding: 12px
|
||||
z-index: 1
|
||||
background-color: rgba(255,255,255,.5)
|
||||
|
||||
p
|
||||
color: black
|
||||
|
||||
h3
|
||||
color: black
|
||||
padding-top: 0px
|
||||
margin-top: 0px
|
|
@ -2,6 +2,13 @@
|
|||
|
||||
#homepage_screenshot
|
||||
margin: 20px 0px
|
||||
|
||||
.class_detail
|
||||
float: left
|
||||
|
||||
img
|
||||
width: 360px
|
||||
|
||||
.signature
|
||||
text-align: right
|
||||
|
||||
|
@ -13,21 +20,9 @@
|
|||
width: 150px
|
||||
margin: 10px 10px 20px 20px
|
||||
|
||||
#contribute-nav
|
||||
float: left
|
||||
max-width: 20%
|
||||
width: 250px
|
||||
box-sizing: border-box
|
||||
margin-left: 20px
|
||||
padding-top: 40px
|
||||
|
||||
li
|
||||
float: none
|
||||
width: 100%
|
||||
|
||||
.class-main
|
||||
margin-left: 25%
|
||||
padding: 40px
|
||||
margin-left: 33%
|
||||
padding: 0px 40px 40px 40px
|
||||
box-sizing: border-box
|
||||
|
||||
.header-scrolling-fix
|
||||
|
|
|
@ -206,16 +206,8 @@ $level-resize-transition-time: 0.5s
|
|||
@include opacity(1)
|
||||
|
||||
@media screen and (min-aspect-ratio: 17/10)
|
||||
display: none
|
||||
|
||||
.hour-of-code-explanation
|
||||
margin-top: 5px
|
||||
color: white
|
||||
font-size: 12px
|
||||
|
||||
a
|
||||
color: white
|
||||
text-decoration: underline
|
||||
&:not(.premium)
|
||||
display: none
|
||||
|
||||
#fullscreen-editor-background-screen
|
||||
background-color: black
|
||||
|
|
|
@ -104,13 +104,6 @@
|
|||
&.btn-#{nth($tuple, 1)}
|
||||
@include banner-button(nth($tuple, 2), #FFF)
|
||||
|
||||
.footer .footer-link-text a
|
||||
@include opacity(0.75)
|
||||
@include transition(opacity .10s linear)
|
||||
|
||||
&:hover, &:active
|
||||
@include opacity(1)
|
||||
|
||||
$GI: 0.5 // gradient intensity; can tweak this 0-1
|
||||
|
||||
.gradient
|
||||
|
@ -152,22 +145,3 @@
|
|||
top: 0
|
||||
height: 100%
|
||||
width: 2%
|
||||
|
||||
.footer
|
||||
@media screen and (min-aspect-ratio: 17/10)
|
||||
display: none
|
||||
|
||||
&:not(:hover)
|
||||
@include opacity(0.6)
|
||||
|
||||
.hour-of-code-explanation
|
||||
margin-top: 5px
|
||||
color: white
|
||||
font-size: 12px
|
||||
|
||||
&:not(:hover)
|
||||
@include opacity(0.75)
|
||||
|
||||
a
|
||||
color: white
|
||||
text-decoration: underline
|
||||
|
|
|
@ -4,12 +4,11 @@ block content
|
|||
|
||||
div.contribute_class
|
||||
|
||||
include /templates/contribute/contribute_nav
|
||||
.class_detail
|
||||
|
||||
img(src="/images/pages/contribute/class_detail_adventurer.png", alt="")
|
||||
|
||||
div.class-main#adventurer-main
|
||||
|
||||
.class_image
|
||||
img.img-responsive(src="/images/pages/contribute/adventurer.png", alt="")
|
||||
|
||||
h2
|
||||
span(data-i18n="classes.adventurer_title") Adventurer
|
||||
|
|
|
@ -4,12 +4,11 @@ block content
|
|||
|
||||
div.contribute_class
|
||||
|
||||
include /templates/contribute/contribute_nav
|
||||
.class_detail
|
||||
|
||||
img(src="/images/pages/contribute/class_detail_ambassador.png", alt="")
|
||||
|
||||
div.class-main#ambassador-main
|
||||
|
||||
.class_image
|
||||
img.img-responsive(src="/images/pages/contribute/ambassador.png", alt="")
|
||||
|
||||
h2
|
||||
span(data-i18n="classes.ambassador_title") Ambassador
|
||||
|
|
|
@ -4,13 +4,12 @@ block content
|
|||
|
||||
div.contribute_class
|
||||
|
||||
include /templates/contribute/contribute_nav
|
||||
.class_detail
|
||||
|
||||
img(src="/images/pages/contribute/class_detail_archmage.png", alt="")
|
||||
|
||||
div.class-main#archmage-main
|
||||
|
||||
.class_image
|
||||
img.img-responsive(src="/images/pages/contribute/archmage.png", alt="")
|
||||
|
||||
h2
|
||||
span(data-i18n="classes.archmage_title") Archmage
|
||||
span
|
||||
|
|
|
@ -4,12 +4,11 @@ block content
|
|||
|
||||
div.contribute_class
|
||||
|
||||
include /templates/contribute/contribute_nav
|
||||
.class_detail
|
||||
|
||||
img(src="/images/pages/contribute/class_detail_artisan.png", alt="")
|
||||
|
||||
div.class-main#artisan-main
|
||||
|
||||
.class_image
|
||||
img.img-responsive(src="/images/pages/contribute/artisan.png", alt="")
|
||||
|
||||
h2
|
||||
span(data-i18n="classes.artisan_title") Artisan
|
||||
|
|
|
@ -2,181 +2,77 @@ extends /templates/base
|
|||
|
||||
block content
|
||||
|
||||
div.contribute_class
|
||||
h2 Contributing
|
||||
p CodeCombat is 100% open source and hundreds of dedicated players have helped us build the games
|
||||
| into what it is today. Join us and write the next chapter in CodeCombat's quest to teach the
|
||||
| world to code!
|
||||
|
||||
include /templates/contribute/contribute_nav
|
||||
a(href="/contribute/archmage")
|
||||
div.class_tile
|
||||
img(src="/images/pages/contribute/tile_archmage.png", alt="")
|
||||
|
||||
div#contribute-main.class-main
|
||||
div#intro
|
||||
div.class_text
|
||||
h3 Archmage
|
||||
|
||||
h2(data-i18n="contribute.page_title") Contributing
|
||||
p(data-i18n="contribute.short_archmage")
|
||||
| If you are a developer interested in coding educational games, become an archmage
|
||||
| to help us build CodeCombat!
|
||||
|
||||
#homepage_screenshot
|
||||
img.img-responsive(src="/images/pages/contribute/contribute_header.png", alt="")
|
||||
a(href="/contribute/artisan")
|
||||
div.class_tile
|
||||
img.tile-img(src="/images/pages/contribute/tile_artisan.png", alt="")
|
||||
|
||||
p
|
||||
strong(data-i18n="contribute.introduction_desc_intro")
|
||||
| We have high hopes for CodeCombat.
|
||||
|
|
||||
span(data-i18n="contribute.introduction_desc_pref")
|
||||
| We want to be where programmers of all stripes come to learn and play together,
|
||||
| introduce others to the wonderful world of coding,
|
||||
| and reflect the best parts of the community.
|
||||
| We can't and don't want to do that alone;
|
||||
| what makes projects like GitHub, Stack Overflow and Linux great are the people who
|
||||
| use them and build on them.
|
||||
| To that end,
|
||||
a(href="https://github.com/codecombat/codecombat", data-i18n="contribute.introduction_desc_github_url")
|
||||
| CodeCombat is totally open source
|
||||
span(data-i18n="contribute.introduction_desc_suf")
|
||||
| , and we aim to provide as many ways as possible for you to take part and
|
||||
| make this project as much yours as ours.
|
||||
p(data-i18n="contribute.introduction_desc_ending")
|
||||
| We hope you'll join our party!
|
||||
p(data-i18n="contribute.introduction_desc_signature").signature
|
||||
| - Nick, George, Scott, Michael, and Matt
|
||||
hr
|
||||
div.class_text
|
||||
h3 Artisan
|
||||
|
||||
.contributor-signup-anonymous
|
||||
p(data-i18n="contribute.short_artisan")
|
||||
| Build and share levels for you and your friends to play. Become an Artisan to learn
|
||||
| the art of teaching others to program.
|
||||
|
||||
#archmage.header-scrolling-fix
|
||||
.class_image
|
||||
img.img-responsive(src="/images/pages/contribute/archmage.png", alt="")
|
||||
a(href="/contribute/adventurer")
|
||||
div.class_tile
|
||||
img.tile-img(src="/images/pages/contribute/tile_adventurer.png", alt="")
|
||||
|
||||
h3.header-scrolling-fix
|
||||
span(data-i18n="classes.archmage_title") Archmage
|
||||
span
|
||||
span(data-i18n="classes.archmage_title_description") (Coder)
|
||||
p(data-i18n="contribute.archmage_summary")
|
||||
| Interested in working on game graphics, user interface design, database and server organization,
|
||||
| multiplayer networking, physics, sound, or game engine performance? Want to help build a game to
|
||||
| help other people learn what you are good at? We have a lot to do and if you are an experienced
|
||||
| programmer and want to develop for CodeCombat, this class is for you. We would love your help
|
||||
| building the best programming game ever.
|
||||
div.class_text
|
||||
h3 Adventurer
|
||||
|
||||
a(href="/contribute/archmage")
|
||||
p.lead(data-i18n="contribute.more_about_archmage")
|
||||
| Learn More About Becoming an Archmage
|
||||
p(data-i18n="contribute.short_adventurer")
|
||||
| Get our new levels (even our subscriber content) for free one week early and help us
|
||||
| work out bugs before our public release.
|
||||
|
||||
.contributor-signup(data-contributor-class-id="developer", data-contributor-class-name="archmage")
|
||||
a(href="/contribute/scribe")
|
||||
div.class_tile
|
||||
img.tile-img(src="/images/pages/contribute/tile_scribe.png", alt="")
|
||||
|
||||
#artisan.header-scrolling-fix
|
||||
div.class_text
|
||||
h3 Scribe
|
||||
|
||||
.class_image
|
||||
img.img-responsive(src="/images/pages/contribute/artisan.png", alt="")
|
||||
p(data-i18n="contribute.short_scribe")
|
||||
| Good code needs good documentation. Write,
|
||||
| edit, and improve the docs read by millions of players across the globe.
|
||||
|
||||
h3.header-scrolling-fix
|
||||
span(data-i18n="classes.artisan_title") Artisan
|
||||
span
|
||||
span(data-i18n="classes.artisan_title_description") (Level Builder)
|
||||
p
|
||||
span(data-i18n="contribute.artisan_summary_pref")
|
||||
| Want to design levels and expand CodeCombat's arsenal? People are playing through our
|
||||
| content at a pace faster than we can build! Right now, our level editor is barebone,
|
||||
| so be wary. Making levels will be a little challenging and buggy. If you have visions
|
||||
| of campaigns spanning for-loops to
|
||||
span
|
||||
a(href="http://stackoverflow.com/questions/758088/seeking-contrived-example-code-continuations/758105#758105")
|
||||
| Mondo Bizzaro
|
||||
span(data-i18n="contribute.artisan_summary_suf")
|
||||
| , then this class is for you.
|
||||
|
||||
a(href="/contribute/artisan")
|
||||
p.lead(data-i18n="contribute.more_about_artisan")
|
||||
| Learn More About Becoming An Artisan
|
||||
a(href="/contribute/diplomat")
|
||||
|
||||
.contributor-signup(data-contributor-class-id="level_creator", data-contributor-class-name="artisan")
|
||||
div.class_tile
|
||||
img.tile-img(src="/images/pages/contribute/tile_diplomat.png", alt="")
|
||||
|
||||
#adventurer.header-scrolling-fix
|
||||
div.class_text
|
||||
h3 Diplomat
|
||||
|
||||
.class_image
|
||||
img.img-responsive(src="/images/pages/contribute/adventurer.png", alt="")
|
||||
p(data-i18n="contribute.short_diplomat")
|
||||
| CodeCombat is localized in 39 languages by our Diplomats. Help them
|
||||
| out and contribute translations.
|
||||
|
||||
h3.header-scrolling-fix
|
||||
span(data-i18n="classes.adventurer_title") Adventurer
|
||||
span
|
||||
span(data-i18n="classes.adventurer_title_description") (Level Playtester)
|
||||
p(data-i18n="contribute.adventurer_summary")
|
||||
| Let us be clear about your role: you are the tank. You are going to take heavy damage.
|
||||
| We need people to try out brand-new levels and help identify how to make things better.
|
||||
| The pain will be enormous; making good games is a long process and no one gets
|
||||
| it right the first time.
|
||||
| If you can endure and have a high constitution score, then this class is for you.
|
||||
|
||||
a(href="/contribute/adventurer")
|
||||
p.lead(data-i18n="contribute.more_about_adventurer")
|
||||
| Learn More About Becoming an Adventurer
|
||||
a(href="/contribute/ambassador")
|
||||
div.class_tile
|
||||
img.tile-img(src="/images/pages/contribute/tile_ambassador.png", alt="")
|
||||
|
||||
.contributor-signup(data-contributor-class-id="tester", data-contributor-class-name="adventurer")
|
||||
|
||||
#scribe.header-scrolling-fix
|
||||
div.class_text
|
||||
h3 Ambassador
|
||||
|
||||
.class_image
|
||||
img.img-responsive(src="/images/pages/contribute/scribe.png", alt="")
|
||||
p(data-i18n="contribute.short_ambassador")
|
||||
| Tame our forum users and provide direction for those with questions. Our ambassadors
|
||||
| represent CodeCombat to the world.
|
||||
|
||||
h3.header-scrolling-fix
|
||||
span(data-i18n="classes.scribe_title") Scribe
|
||||
span
|
||||
span(data-i18n="classes.scribe_title_description") (Article Editor)
|
||||
p
|
||||
span(data-i18n="contribute.scribe_summary_pref")
|
||||
| CodeCombat is not just going to be a bunch of levels. It will also be a resource of
|
||||
| programming knowledge that players can hook into. That way, each Artisan can link
|
||||
| to a detailed article that for the player's edification:
|
||||
| documentation akin to what the
|
||||
a(href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide", data-i18n="contribute.scribe_introduction_url_mozilla")
|
||||
| Mozilla Developer Network
|
||||
span(data-i18n="contribute.scribe_summary_suf")
|
||||
| has built. If you enjoy explaining programming concepts, then this class is for you.
|
||||
|
||||
a(href="/contribute/scribe")
|
||||
p.lead(data-i18n="contribute.more_about_scribe")
|
||||
| Learn More About Becoming a Scribe
|
||||
|
||||
.contributor-signup(data-contributor-class-id="article_editor", data-contributor-class-name="scribe")
|
||||
|
||||
#diplomat.header-scrolling-fix
|
||||
|
||||
.class_image
|
||||
img.img-responsive(src="/images/pages/contribute/diplomat.png", alt="")
|
||||
|
||||
h3.header-scrolling-fix
|
||||
span(data-i18n="classes.diplomat_title") Diplomat
|
||||
span
|
||||
span(data-i18n="classes.diplomat_title_description") (Translator)
|
||||
p
|
||||
span(data-i18n="contribute.diplomat_summary")
|
||||
| There is a large interest in CodeCombat in other countries that do not speak English!
|
||||
| We are looking for translators who are willing to spend their time translating the
|
||||
| site's corpus of words so that CodeCombat is accessible across the world as soon as
|
||||
| possible. If you'd like to help getting CodeCombat international, then this class is
|
||||
| for you.
|
||||
|
||||
a(href="/contribute/diplomat")
|
||||
p.lead(data-i18n="contribute.more_about_diplomat")
|
||||
| Learn More About Becoming a Diplomat
|
||||
|
||||
.contributor-signup(data-contributor-class-id="translator", data-contributor-class-name="diplomat")
|
||||
|
||||
#ambassador.header-scrolling-fix
|
||||
|
||||
.class_image
|
||||
img.img-responsive(src="/images/pages/contribute/ambassador.png", alt="")
|
||||
|
||||
h3.header-scrolling-fix
|
||||
span(data-i18n="classes.ambassador_title") Ambassador
|
||||
span
|
||||
span(data-i18n="classes.ambassador_title_description") (Support)
|
||||
p(data-i18n="contribute.ambassador_summary")
|
||||
| We are trying to build a community, and every community needs a support team when
|
||||
| there are troubles. We have got chats, emails, and social networks so that our users
|
||||
| can get acquainted with the game. If you want to help people get involved, have fun,
|
||||
| and learn some programming, then this c lass is for you.
|
||||
|
||||
a(href="/contribute/ambassador")
|
||||
p.lead(data-i18n="contribute.more_about_ambassador")
|
||||
| Learn More About Becoming an Ambassador
|
||||
|
||||
.contributor-signup(data-contributor-class-id="support", data-contributor-class-name="ambassador")
|
||||
|
||||
div.clearfix
|
||||
div.clearfix
|
|
@ -4,12 +4,11 @@ block content
|
|||
|
||||
div.contribute_class
|
||||
|
||||
include /templates/contribute/contribute_nav
|
||||
.class_detail
|
||||
|
||||
img(src="/images/pages/contribute/class_detail_diplomat.png", alt="")
|
||||
|
||||
div.class-main#diplomat-main
|
||||
|
||||
.class_image
|
||||
img.img-responsive(src="/images/pages/contribute/diplomat.png", alt="")
|
||||
|
||||
h2
|
||||
span(data-i18n="classes.diplomat_title") Diplomat
|
||||
|
|
|
@ -4,13 +4,12 @@ block content
|
|||
|
||||
div.contribute_class
|
||||
|
||||
include /templates/contribute/contribute_nav
|
||||
.class_detail
|
||||
|
||||
img(src="/images/pages/contribute/class_detail_scribe.png", alt="")
|
||||
|
||||
div.class-main#scribe-main
|
||||
|
||||
.class_image
|
||||
img.img-responsive(src="/images/pages/contribute/scribe.png", alt="")
|
||||
|
||||
h2
|
||||
span(data-i18n="classes.scribe_title") Scribe
|
||||
span
|
||||
|
|
|
@ -6,13 +6,16 @@ block modal-header-content
|
|||
block modal-body-content
|
||||
p
|
||||
span(data-i18n="contact.welcome") Good to hear from you! Use this form to send us email.
|
||||
span(data-i18n="contact.contribute_prefix") If you're interested in contributing, check out our
|
||||
a(href="/contribute", data-dismiss="modal", data-i18n="contact.contribute_page") contribute page
|
||||
span(data-i18n="contact.contribute_suffix") !
|
||||
p
|
||||
span(data-i18n="contact.forum_prefix") For anything public, please try
|
||||
span.spl(data-i18n="contact.forum_prefix") For anything public, please try
|
||||
a(href="http://discourse.codecombat.com/", data-i18n="contact.forum_page") our forum
|
||||
span(data-i18n="contact.forum_suffix") instead.
|
||||
if me.isPremium()
|
||||
p(data-i18n="contact.subscriber_support") Since you're a CodeCombat subscriber, your email will get our priority support.
|
||||
else
|
||||
p
|
||||
span(data-i18n="contact.subscribe_prefix") If you need help figuring out a level, please
|
||||
a.spl.spr(data-toggle="coco-modal", data-target="core/SubscribeModal", data-i18n="contact.subscribe") buy a CodeCombat subscription
|
||||
span(data-i18n="contact.subscribe_suffix") and we'll be happy to help you with your code.
|
||||
.form
|
||||
.form-group
|
||||
label.control-label(for="contact-email", data-i18n="general.email") Email
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
|
||||
button.btn.btn-lg.btn-warning.banner.header-font#stop-real-time-playback-button(title="Stop real-time playback", data-i18n="play_level.skip") Skip
|
||||
|
||||
#play-footer
|
||||
p(class='footer-link-text')
|
||||
a(title='Send CodeCombat a message', tabindex=-1, data-toggle="coco-modal", data-target="core/ContactModal", data-i18n="nav.contact") Contact
|
||||
if !me.get('anonymous')
|
||||
#play-footer(class=me.isPremium() ? "premium" : "")
|
||||
p(class='footer-link-text')
|
||||
a(title='Send CodeCombat a message', tabindex=-1, data-toggle="coco-modal", data-target="core/ContactModal", data-i18n="nav.contact") Contact
|
||||
|
|
|
@ -11,7 +11,3 @@
|
|||
#level-chat-view
|
||||
#playback-view
|
||||
#thang-hud
|
||||
.footer
|
||||
.content
|
||||
p(class='footer-link-text')
|
||||
a(title='Send CodeCombat a message', tabindex=-1, data-toggle="coco-modal", data-target="core/ContactModal", data-i18n="nav.contact") Contact
|
|
@ -83,6 +83,9 @@
|
|||
else
|
||||
span.player-name.spr= me.get('name')
|
||||
button#logout-button.btn.btn-illustrated.btn-warning(data-i18n="login.log_out") Log Out
|
||||
if me.isPremium()
|
||||
button.btn.btn-illustrated.btn-primary(data-i18n="nav.contact", data-toggle="coco-modal", data-target="core/ContactModal") Contact
|
||||
|
||||
|
||||
button.btn.btn-lg.btn-inverse#volume-button(title="Adjust volume")
|
||||
.glyphicon.glyphicon-volume-off
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
RootView = require 'views/core/RootView'
|
||||
template = require 'templates/account/subscription-view'
|
||||
CocoCollection = require 'collections/CocoCollection'
|
||||
SubscribeModal = require 'views/play/modal/SubscribeModal'
|
||||
SubscribeModal = require 'views/core/SubscribeModal'
|
||||
|
||||
module.exports = class SubscriptionView extends RootView
|
||||
id: "subscription-view"
|
||||
|
@ -33,7 +33,7 @@ module.exports = class SubscriptionView extends RootView
|
|||
c.cost = "$#{(subscription.plan.amount/100).toFixed(2)}"
|
||||
if card = @stripeInfo.cards?.data?[0]
|
||||
c.card = "#{card.brand}: x#{card.last4}"
|
||||
|
||||
|
||||
c.stripeInfo = @stripeInfo
|
||||
c.subscribed = me.get('stripe')?.planID
|
||||
c.active = me.isPremium()
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
ModalView = require 'views/core/ModalView'
|
||||
template = require 'templates/play/modal/subscribe-modal'
|
||||
template = require 'templates/core/subscribe-modal'
|
||||
stripeHandler = require 'core/services/stripe'
|
||||
utils = require 'core/utils'
|
||||
AuthModal = require 'views/core/AuthModal'
|
|
@ -9,7 +9,7 @@ ThangType = require 'models/ThangType'
|
|||
MusicPlayer = require 'lib/surface/MusicPlayer'
|
||||
storage = require 'core/storage'
|
||||
AuthModal = require 'views/core/AuthModal'
|
||||
SubscribeModal = require 'views/play/modal/SubscribeModal'
|
||||
SubscribeModal = require 'views/core/SubscribeModal'
|
||||
Level = require 'models/Level'
|
||||
|
||||
trackedHourOfCode = false
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
CocoView = require 'views/core/CocoView'
|
||||
template = require 'templates/play/level/level_loading'
|
||||
utils = require 'core/utils'
|
||||
SubscribeModal = require 'views/play/modal/SubscribeModal'
|
||||
SubscribeModal = require 'views/core/SubscribeModal'
|
||||
|
||||
module.exports = class LevelLoadingView extends CocoView
|
||||
id: 'level-loading-view'
|
||||
|
|
|
@ -7,30 +7,33 @@
|
|||
// For a given style:
|
||||
// - Video completion rates (Not too interesting unless each level has all styles available)
|
||||
// - Video completion rates, per-level too
|
||||
// TODO: The rest of these.
|
||||
// - Watched another video
|
||||
// - Level completion rates
|
||||
// - Subscription coversion rates
|
||||
// - Subscription coversion totals
|
||||
// TODO: The rest
|
||||
// - How many people who start a level click the help button, and which one?
|
||||
// - Need a hard start date when the help button presented
|
||||
|
||||
|
||||
// Intial production deploy completed at 12:42am 12/18/14 PST
|
||||
var testStartDate='2014-12-14T08:42:00.000Z';
|
||||
// 12:42am 12/18/14 PST - Intial production deploy completed
|
||||
var testStartDate = '2014-12-18T08:42:00.000Z';
|
||||
// 12:29pm 12/18/14 PST - 2nd deploy w/ originals for dungeons-of-kithgard and second-kithmaze
|
||||
// TODO: move this date up to avoid prod deploy transitional data messing with us.
|
||||
// testStartDate = '2014-12-18T20:29:00.000Z';
|
||||
testStartDate = '2014-12-18T22:29:00.000Z';
|
||||
|
||||
function printVideoCompletionRates() {
|
||||
print("Querying for help video events...");
|
||||
var videosCursor = db['analytics.log.events'].find({
|
||||
$and: [
|
||||
{"created": { $gte: ISODate(testStartDate)}},
|
||||
{$or : [
|
||||
{"event": "Start help video"},
|
||||
{"event": "Finish help video"}
|
||||
]}
|
||||
{"created": { $gte: ISODate(testStartDate)}},
|
||||
{$or : [
|
||||
{"event": "Start help video"},
|
||||
{"event": "Finish help video"}
|
||||
]}
|
||||
]
|
||||
});
|
||||
|
||||
|
||||
print("Building video progression data...");
|
||||
// Build: <style><level><userID><event> counts
|
||||
var videoProgression = {};
|
||||
|
@ -131,14 +134,242 @@ function printVideoCompletionRates() {
|
|||
styleLevelCompletionRates.push(data);
|
||||
}
|
||||
}
|
||||
styleLevelCompletionRates.sort(function(a,b) {return b['rate'] && a['rate'] ? b.rate - a.rate : 0;});
|
||||
|
||||
styleLevelCompletionRates.sort(function(a,b) {
|
||||
if (a.level !== b.level) {
|
||||
if (a.level < b.level) return -1;
|
||||
else return 1;
|
||||
}
|
||||
return b['rate'] && a['rate'] ? b.rate - a.rate : 0;
|
||||
});
|
||||
|
||||
print("Per-level style completion rates:");
|
||||
for (var i = 0; i < styleLevelCompletionRates.length; i++) {
|
||||
var item = styleLevelCompletionRates[i];
|
||||
var msg = item.level + "\t" + item.style + "\t" + item.started + "\t" + item.finished;
|
||||
var msg = item.level + "\t" + item.style + (item.style === 'edited' ? "\t\t" : "\t") + item.started + "\t" + item.finished;
|
||||
if (item['rate']) msg += "\t" + item.rate + "%";
|
||||
print(msg);
|
||||
}
|
||||
}
|
||||
printVideoCompletionRates();
|
||||
|
||||
function printWatchedAnotherVideoRates() {
|
||||
// How useful is a style/level in yielding more video starts
|
||||
// Algorithm:
|
||||
// 1. Fetch all start/finish video events after test start date
|
||||
// 2. Create a per-userID dictionary of user event history arrays
|
||||
// 3. Sort each user event history array in ascending order. Now we have a video watching history, per-user.
|
||||
// 4. Walk through each user's history
|
||||
// a. Increment global count for level/style/event, for each level/style event in past history.
|
||||
// b. Save current entry in the past history.
|
||||
// 5. Sort by ascending level name, descending started count
|
||||
|
||||
// TODO: only attribute one start/finish per level to a user?
|
||||
|
||||
print("Querying for help video events...");
|
||||
var videosCursor = db['analytics.log.events'].find({
|
||||
$and: [
|
||||
{"created": { $gte: ISODate(testStartDate)}},
|
||||
{$or : [
|
||||
{"event": "Start help video"},
|
||||
{"event": "Finish help video"}
|
||||
]}
|
||||
]
|
||||
});
|
||||
|
||||
print("Building per-user video progression data...");
|
||||
// Find video progression per-user
|
||||
// Build: <userID>[sorted style/event/level/date events]
|
||||
var videoProgression = {};
|
||||
while (videosCursor.hasNext()) {
|
||||
var doc = videosCursor.next();
|
||||
var event = doc.event;
|
||||
var userID = doc.user.valueOf();
|
||||
var created = doc.created
|
||||
var levelID = doc.properties.level;
|
||||
var style = doc.properties.style;
|
||||
|
||||
if (!videoProgression[userID]) videoProgression[userID] = [];
|
||||
videoProgression[userID].push({
|
||||
style: style,
|
||||
level: levelID,
|
||||
event: event,
|
||||
created: created.toISOString()
|
||||
})
|
||||
}
|
||||
// printjson(videoProgression);
|
||||
|
||||
print("Sorting per-user video progression data...");
|
||||
for (userID in videoProgression) videoProgression[userID].sort(function (a,b) {return a.created < b.created ? -1 : 1});
|
||||
|
||||
print("Building per-level/style additional watched videos..");
|
||||
var additionalWatchedVideos = {};
|
||||
for (userID in videoProgression) {
|
||||
|
||||
// Walk user's history, and tally what preceded each historical entry
|
||||
var userHistory = videoProgression[userID];
|
||||
// printjson(userHistory);
|
||||
var previouslyWatched = {};
|
||||
for (var i = 0; i < userHistory.length; i++) {
|
||||
|
||||
// Walk previously watched events, and attribute to correct additionally watched entry
|
||||
var item = userHistory[i];
|
||||
var level = item.level;
|
||||
var style = item.style;
|
||||
var event = item.event;
|
||||
var created = item.created;
|
||||
for (previousLevel in previouslyWatched) {
|
||||
for (previousStyle in previouslyWatched[previousLevel]) {
|
||||
if (previousLevel === level) continue;
|
||||
var previous = previouslyWatched[previousLevel];
|
||||
// For previous level and style, 'event' followed it
|
||||
if (!additionalWatchedVideos[previousLevel]) additionalWatchedVideos[previousLevel] = {};
|
||||
if (!additionalWatchedVideos[previousLevel][previousStyle]) {
|
||||
additionalWatchedVideos[previousLevel][previousStyle] = {};
|
||||
}
|
||||
// TODO: care which video watched next?
|
||||
if (!additionalWatchedVideos[previousLevel][previousStyle][event]) {
|
||||
additionalWatchedVideos[previousLevel][previousStyle][event] = 0;
|
||||
}
|
||||
additionalWatchedVideos[previousLevel][previousStyle][event]++;
|
||||
|
||||
if (previousLevel === 'the-second-kithmaze') {
|
||||
print("Followed the-second-kithmaze " + userID + " " + level + " " + event + " " + created);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add level/style to previouslyWatched for this user
|
||||
if (!previouslyWatched[level]) previouslyWatched[level] = {};
|
||||
if (!previouslyWatched[level][style]) previouslyWatched[level][style] = true;
|
||||
}
|
||||
}
|
||||
|
||||
print("Sorting additional watched videos by started event counts...");
|
||||
var additionalWatchedVideoByStarted = [];
|
||||
for (levelID in additionalWatchedVideos) {
|
||||
for (style in additionalWatchedVideos[levelID]) {
|
||||
var started = 0;
|
||||
var finished = 0;
|
||||
for (event in additionalWatchedVideos[levelID][style]) {
|
||||
if (event === "Start help video") started += additionalWatchedVideos[levelID][style][event];
|
||||
else if (event === "Finish help video") finished += additionalWatchedVideos[levelID][style][event];
|
||||
else throw new Error("Unknown event " + event);
|
||||
}
|
||||
var data = {
|
||||
level: levelID,
|
||||
style: style,
|
||||
started: started,
|
||||
finished: finished
|
||||
};
|
||||
if (finished > 0) data['rate'] = finished / started * 100;
|
||||
additionalWatchedVideoByStarted.push(data);
|
||||
}
|
||||
}
|
||||
additionalWatchedVideoByStarted.sort(function(a,b) {
|
||||
if (a.level !== b.level) {
|
||||
if (a.level < b.level) return -1;
|
||||
else return 1;
|
||||
}
|
||||
return b.started - a.started;
|
||||
});
|
||||
|
||||
print("Per-level additional videos watched:");
|
||||
print("For a given level and style, this is how many more videos were started and finished.");
|
||||
print("Columns: level, style, started, finished, additionals completion rate");
|
||||
for (var i = 0; i < additionalWatchedVideoByStarted.length; i++) {
|
||||
var item = additionalWatchedVideoByStarted[i];
|
||||
var msg = item.level + "\t" + item.style + (item.style === 'edited' ? "\t\t" : "\t") + item.started + "\t" + item.finished;
|
||||
if (item['rate']) msg += "\t" + item.rate + "%";
|
||||
print(msg);
|
||||
}
|
||||
}
|
||||
|
||||
function printSubConversionTotals() {
|
||||
// For a user, who started a video, did they subscribe afterwards?
|
||||
|
||||
// Find each started event, per user
|
||||
print("Querying for help video start events...");
|
||||
var eventsCursor = db['analytics.log.events'].find({
|
||||
$and: [
|
||||
{"created": { $gte: ISODate(testStartDate)}},
|
||||
{$or : [
|
||||
{"event": "Start help video"},
|
||||
{"event": "Finished subscription purchase"}
|
||||
]}
|
||||
]
|
||||
});
|
||||
|
||||
print("Building per-user events progression data...");
|
||||
// Find event progression per-user
|
||||
var eventsProgression = {};
|
||||
while (eventsCursor.hasNext()) {
|
||||
var doc = eventsCursor.next();
|
||||
var event = doc.event;
|
||||
var userID = doc.user.valueOf();
|
||||
var created = doc.created
|
||||
var levelID = doc.properties.level;
|
||||
var style = doc.properties.style;
|
||||
|
||||
if (!eventsProgression[userID]) eventsProgression[userID] = [];
|
||||
eventsProgression[userID].push({
|
||||
style: style,
|
||||
level: levelID,
|
||||
event: event,
|
||||
created: created.toISOString()
|
||||
})
|
||||
// if (event === 'Finished subscription purchase')
|
||||
// printjson(eventsProgression[userID]);
|
||||
}
|
||||
// printjson(eventsProgression);
|
||||
|
||||
print("Sorting per-user events progression data...");
|
||||
for (userID in eventsProgression) eventsProgression[userID].sort(function (a,b) {return a.created < b.created ? -1 : 1});
|
||||
|
||||
|
||||
print("Building per-level/style sub purchases..");
|
||||
// Build: <level><style><count>
|
||||
var subPurchaseCounts = {};
|
||||
for (userID in eventsProgression) {
|
||||
var history = eventsProgression[userID];
|
||||
for (var i = 0; i < history.length; i++) {
|
||||
if (history[i].event === 'Finished subscription purchase') {
|
||||
var item = i > 0 ? history[i - 1] : {level: 'unknown', style: 'unknown'};
|
||||
// if (i === 0) {
|
||||
// print(userID);
|
||||
// printjson(history[i]);
|
||||
// }
|
||||
if (!subPurchaseCounts[item.level]) subPurchaseCounts[item.level] = {};
|
||||
if (!subPurchaseCounts[item.level][item.style]) subPurchaseCounts[item.level][item.style] = 0;
|
||||
subPurchaseCounts[item.level][item.style]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
// printjson(subPurchaseCounts);
|
||||
|
||||
print("Sorting per-level/style sub purchase counts...");
|
||||
var subPurchasesByTotal = [];
|
||||
for (levelID in subPurchaseCounts) {
|
||||
for (style in subPurchaseCounts[levelID]) {
|
||||
subPurchasesByTotal.push({
|
||||
level: levelID,
|
||||
style: style,
|
||||
total: subPurchaseCounts[levelID][style]
|
||||
})
|
||||
}
|
||||
}
|
||||
subPurchasesByTotal.sort(function (a,b) {
|
||||
if (a.level !== b.level) return a.level < b.level ? -1 : 1;
|
||||
return b.total - a.total;
|
||||
});
|
||||
|
||||
print("Per-level/style following sub purchases:");
|
||||
print("Columns: level, style, following sub purchases.");
|
||||
print("'unknown' means no preceding start help video event.");
|
||||
for (var i = 0; i < subPurchasesByTotal.length; i++) {
|
||||
var item = subPurchasesByTotal[i];
|
||||
print(item.level + "\t" + item.style + (item.style === 'edited' ? "\t\t" : "\t") + item.total);
|
||||
}
|
||||
}
|
||||
|
||||
printVideoCompletionRates();
|
||||
printWatchedAnotherVideoRates();
|
||||
printSubConversionTotals();
|
|
@ -14,17 +14,25 @@ module.exports.setup = (app) ->
|
|||
return res.end()
|
||||
|
||||
createMailContext = (sender, message, user, recipientID, subject, done) ->
|
||||
level = if user?.get('points') > 0 then Math.floor(5 * Math.log((1 / 100) * (xp + 100))) + 1 else 0
|
||||
premium = user?.isPremium()
|
||||
content = """
|
||||
#{message}
|
||||
|
||||
#{user.get('name') or 'Anonymous'} - Level #{level}#{if premium then ' - Subscriber' else ''} - #{user._id}
|
||||
"""
|
||||
|
||||
context =
|
||||
email_id: sendwithus.templates.plain_text_email
|
||||
recipient:
|
||||
address: config.mail.username
|
||||
address: if premium then config.mail.supportPremium else config.mail.supportPrimary
|
||||
sender:
|
||||
address: config.mail.username
|
||||
reply_to: sender
|
||||
name: user.get('name')
|
||||
email_data:
|
||||
subject: "[CodeCombat] #{subject ? ('Feedback - ' + sender)}"
|
||||
content: "#{message}\n\nUsername: #{user.get('name') or 'Anonymous'}\nID: #{user._id}"
|
||||
content: content
|
||||
|
||||
if recipientID and (user.isAdmin() or ('employer' in (user.get('permissions') ? [])))
|
||||
User.findById(recipientID, 'email').exec (err, document) ->
|
||||
|
|
|
@ -36,6 +36,9 @@ else
|
|||
config.mongo.password = process.env.COCO_MONGO_PASSWORD or ''
|
||||
|
||||
config.mail =
|
||||
username: process.env.COCO_MAIL_SERVICE_USERNAME or ''
|
||||
supportPrimary: process.env.COCO_MAIL_SUPPORT_PRIMARY or ''
|
||||
supportPremium: process.env.COCO_MAIL_SUPPORT_PREMIUM or ''
|
||||
username: process.env.COCO_MAIL_SERVICE_USERNAME or ''
|
||||
mailchimpAPIKey: process.env.COCO_MAILCHIMP_API_KEY or ''
|
||||
mailchimpWebhook: process.env.COCO_MAILCHIMP_WEBHOOK or '/mail/webhook'
|
||||
|
|
|
@ -58,11 +58,12 @@ setupExpressMiddleware = (app) ->
|
|||
express.logger.format('prod', productionLogging)
|
||||
app.use(express.logger('prod'))
|
||||
app.use express.compress filter: (req, res) ->
|
||||
return false if req.headers.host is 'codecombat.com' # CloudFlare will gzip it for us on codecombat.com
|
||||
compressible res.getHeader('Content-Type')
|
||||
else
|
||||
express.logger.format('dev', developmentLogging)
|
||||
app.use(express.logger('dev'))
|
||||
app.use(express.static(path.join(__dirname, 'public')))
|
||||
app.use(express.static(path.join(__dirname, 'public'), maxAge: 30 * 60 * 1000))
|
||||
app.use(useragent.express())
|
||||
|
||||
app.use(express.favicon())
|
||||
|
|