Merge branch 'master' into production

This commit is contained in:
Nick Winter 2014-11-25 16:54:36 -08:00
commit 98d8a2cfb3
43 changed files with 587 additions and 805 deletions

View file

@ -21,10 +21,10 @@ module.exports = class CocoRouter extends Backbone.Router
'about': go('AboutView')
'account': go('account/MainAccountView')
'account/settings': go('account/AccountSettingsView')
'account/settings': go('account/AccountSettingsRootView')
'account/unsubscribe': go('account/UnsubscribeView')
'account/profile': go('user/JobProfileView') # legacy URL, sent in emails
#'account/payment'
'account/payments': go('account/PaymentsView')
'admin': go('admin/MainAdminView')
'admin/candidates': go('admin/CandidatesView')
@ -99,9 +99,7 @@ module.exports = class CocoRouter extends Backbone.Router
'test(/*subpath)': go('TestView')
'user/:slugOrID': go('user/MainUserView')
'user/:slugOrID/stats': go('user/AchievementsView')
'user/:slugOrID/profile': go('user/JobProfileView')
#'user/:slugOrID/code': go('user/CodeView')
'*name': 'showNotFoundView'

View file

@ -6,10 +6,13 @@ module.exports = class ThangNamesCollection extends CocoCollection
model: ThangType
isCachable: false
constructor: (@ids) -> super()
constructor: (@ids) ->
super()
@ids.sort()
if @ids.length > 55
console.error 'Too many ids, we\'ll likely go over the GET url kind-of-limit of 2000 characters.'
fetch: (options) ->
options ?= {}
method = if application.isIPadApp then 'GET' else 'POST' # Not sure why this was required that one time.
_.extend options, {type: method, data: {ids: @ids}}
_.extend options, {data: {ids: @ids}}
super(options)

View file

@ -210,7 +210,7 @@ module.exports = LevelOptions =
hidesRealTimePlayback: true
hidesCodeToolbar: true
requiredGear: {feet: 'leather-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses'}
restrictedGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer'}
restrictedGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-i'}
'village-guard':
hidesCodeToolbar: true
requiredGear: {feet: 'leather-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses'}
@ -244,7 +244,7 @@ module.exports = LevelOptions =
# Ranger branch
'munchkin-harvest':
requiredGear: {torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'}
requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'}
restrictedGear: {}
'swift-dagger':
requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'crude-crossbow', 'left-hand': 'crude-dagger', wrists: 'sundial-wristwatch'}
@ -255,7 +255,7 @@ module.exports = LevelOptions =
# Wizard branch
'arcane-ally':
requiredGear: {torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'}
requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'}
restrictedGear: {eyes: 'crude-glasses'}
'touch-of-death':
requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'enchanted-stick', 'left-hand': 'unholy-tome-i', wrists: 'sundial-wristwatch'}

View file

@ -791,6 +791,13 @@
account:
recently_played: "Recently Played"
no_recent_games: "No games played during the past two weeks."
payments: "Payments"
service_apple: "Apple"
service_web: "Web"
paid_on: "Paid On"
service: "Service"
price: "Price"
gems: "Gems"
loading_error:
could_not_load: "Error loading from server"

View file

@ -0,0 +1,5 @@
CocoModel = require('./CocoModel')
module.exports = class Payment extends CocoModel
@className: "Payment"
urlRoot: "/db/payment"

View file

@ -232,7 +232,9 @@ module.exports = class ThangType extends CocoModel
stage?.toDataURL()
getPortraitStage: (spriteOptionsOrKey, size=100) ->
return unless @isFullyLoaded()
canvas = $("<canvas width='#{size}' height='#{size}'></canvas>")
stage = new createjs.Stage(canvas[0])
return stage unless @isFullyLoaded()
key = spriteOptionsOrKey
key = if _.isString(key) then key else @spriteSheetKey(@fillOptions(key))
spriteSheet = @spriteSheets[key]
@ -242,8 +244,6 @@ module.exports = class ThangType extends CocoModel
spriteSheet = @buildSpriteSheet(options)
return if _.isString spriteSheet
return unless spriteSheet
canvas = $("<canvas width='#{size}' height='#{size}'></canvas>")
stage = new createjs.Stage(canvas[0])
sprite = new createjs.Sprite(spriteSheet)
pt = @actions.portrait?.positions?.registration
sprite.regX = pt?.x or 0

View file

@ -0,0 +1,49 @@
#account-settings-root-view
//- Fixed save button
#site-content-area
padding-bottom: 44px
#save-button-container
position: fixed
bottom: 0
left: 0
right: 0
z-index: 10
background: gray
padding: 5px
#save-button
width: 100%
&.btn-info, &.btn-danger
opacity: 1.0
#account-settings-view
.row
padding-top: 20px
//- Panels
.panel-heading
font-family: Open Sans Condensed
font-weight: bold
.panel-title
font-size: 20px
//- Panel specific stuff
.profile-photo
max-width: 100%
max-height: 200px
display: block
margin-bottom: 10px
#email-panel
#specific-notification-settings
padding-left: 20px
margin-left: 20px
border-left: 1px solid gray

View file

@ -1,32 +0,0 @@
@import "app/styles/bootstrap/variables"
@import "app/styles/mixins"
#account-home
dl
margin-bottom: 0px
img#picture
max-width: 100%
.panel
margin-bottom: 10px
h2
margin-bottom: 0px
a
font-size: 28px
margin-left: 5px
.panel-title > a
margin-left: 5px
color: rgb(11, 99, 188)
.panel-me
td
padding-left: 15px
.panel-emails
h4
font-family: $font-family-base

View file

@ -0,0 +1,9 @@
@import "app/styles/bootstrap/variables"
@import "app/styles/mixins"
#main-account-view
#account-links
width: 300px
#account-links .btn
width: 100%

View file

@ -1,91 +0,0 @@
#account-settings-view
.nav
margin-bottom: 10px
.tab-content
border: 1px solid #aaa
padding: 20px
background: #eee
border-radius: 5px
#save-button-container
position: fixed
top: 100px
width: 1000px
z-index: 10
#save-button
float: right
&.btn-info, &.btn-danger
opacity: 1.0
.gravatar-fallback
margin-top: 10px
input.range
position: relative
top: 4px
div.range-color
position: relative
top: 6px
height: 16px
width: 16px
display: inline-block
margin-left: 10px
.help-inline
position: relative
top: 3px
left: 10px
font-size: 12px
.form
max-width: 600px
#email-pane
#specific-notification-settings
padding-left: 20px
margin-left: 20px
border-left: 1px solid gray
#job-profile-view
.profile-preview-button
&.bottom-preview
margin: 15px 0 0 0
.sample-profile-thumbnail
margin-top: -60px
.profile-completion-progress
width: 100%
display: inline-block
height: 33px
.progress-bar
line-height: 33px
.progress-next-item
margin-top: -20px
margin-bottom: 15px
#job-profile-treema
background-color: white
input
width: 790px
.treema-description
font-size: 14px
line-height: 22px
opacity: 1
.treema-row
padding-top: 6px
.treema-image-file
img
display: block
clear: both
max-width: 300px

View file

@ -66,37 +66,6 @@ $user-achievements-scale: 0.8
overflow: hidden
text-overflow: ellipsis
// Specific to the user stats page
#user-achievements-view
.achievement-body
width: 335px
height: 120px
margin: 10px 0px
.achievement-icon
width: $overall-scale * $icon-size * $user-achievements-scale
height: $overall-scale * $icon-size * $user-achievements-scale
top: -5px
.achievement-image
img
width: $overall-scale * $user-achievements-scale * $icon-image-size
.achievement-content
margin-left: 60px
margin-right: 5px
width: 260px
height: 100px
padding: 15px 10px 20px 60px
.achievement-title
font-size: 20px
.achievement-description
font-size: 12px
line-height: 1.3em
max-height: 2.6em
.achievement-popup
padding: $overall-scale * 20px 0px
position: relative

View file

@ -75,6 +75,7 @@ a
.progress
width: 50%
margin: 0 25%
margin-bottom: 20px
// all loading screens
.loading-container

View file

@ -6,10 +6,17 @@
&.show-background
background: url(/images/pages/base/background.jpg) top center no-repeat
background-color: rgb(150,202,68)
@media screen and ( max-height: 800px )
background-position: center -226px
padding-top: 185px
max-width: 1920px
margin: 0 auto
@media screen and ( max-height: 800px )
padding-top: 50px
//- Nav
#site-nav
@ -22,7 +29,10 @@
text-align: center
min-width: 1024px
z-index: 1
@media screen and ( max-height: 800px )
top: -80px
#nav-logo
position: absolute
margin-right: auto
@ -31,22 +41,31 @@
right: 0
top: -45px
@media screen and ( max-height: 800px )
display: none
#small-nav-logo
display: none
@media screen and ( max-height: 800px )
display: inline-block
height: 30px
#site-nav-links
position: absolute
bottom: 21px
left: 0
right: 0
a
color: rgb(158,135,119)
&:hover
color: $white
& > a
color: rgb(158,135,119)
&:hover
color: $white
a, button, select
font-size: 18px
text-transform: uppercase
font-family: Open Sans Condensed
margin: 0 7px
& > a, button, select
font-size: 18px
text-transform: uppercase
font-family: Open Sans Condensed
margin: 0 7px
button, select
position: relative
@ -81,57 +100,44 @@
width: 18px
.dropdown-menu
//left: auto // this busts it, not sure why it's in
width: 280px
width: 180px
padding: 0px
border-radius: 0px
font-family: Open Sans Condensed
font-variant: small-caps
> .user-dropdown-header
position: relative
.user-dropdown-header
background: #E4CF8C
height: 160px
padding: 10px
text-align: center
color: black
border-bottom: #32281e 1px solid
> a:hover
background-color: transparent
img
border: #e3be7a 8px solid
height: 98px // Includes the border
&:hover
box-shadow: 0 0 20px #e3be7a
> h3
h3
font-variant: small-caps
font-family: Open Sans Condensed
margin-top: 10px
text-shadow: 2px 2px 3px white
color: #31281E
.user-level
position: absolute
top: 73px
right: 86px
right: 40px
color: gold
text-shadow: 1px 1px black, -1px -1px 0 black, 1px -1px 0 black, -1px 1px 0 black
.user-dropdown-body
li
color: black
padding: 15px
letter-spacing: 1px
font: 15px 'Helvetica Neue', Helvetica, Arial, sans-serif
+clearfix()
.user-dropdown-footer
padding: 10px
margin-left: 0px
font-size: 14px
+clearfix()
.btn-flat
border: #ddd 1px solid
border-radius: 0px
margin: 0px
font-size: 16px
#logout-button
font-weight: bold
//- Content
@ -141,6 +147,7 @@
width: 1024px
border: 5px solid rgb(110,88,41)
padding: 20px 12px
min-height: 300px
//- Footer

View file

@ -4,8 +4,9 @@
#home-view
#spacer
//height: 750px // No one could see this; let's shrink it as much as we can.
height: 606px
height: 626px
@media screen and ( max-height: 800px )
height: 510px
#play-button, #or-ipad, #apple-store-button, #slogan, .alert
text-align: center
@ -27,6 +28,9 @@
top: 308px
width: 218px
height: 219px
@media screen and ( max-height: 800px )
top: 78px
background-image: url(/images/pages/home/play_button.png)
background-position: 0 219px
@ -40,11 +44,17 @@
color: rgb(119,101,84)
font-size: 17px
max-width: 211px
@media screen and ( max-height: 800px )
top: 310px
#apple-store-button
top: 593px
height: 63px
@media screen and ( max-height: 800px )
top: 363px
#slogan
top: 681px
height: 132px
@ -53,6 +63,9 @@
font-size: 28px
line-height: 32px
color: rgb(50,40,31)
@media screen and ( max-height: 800px )
top: 451px
.alert
top: 213px

View file

@ -1,4 +1,13 @@
#play-account-modal
.account-view
color: black
.modal-dialog
min-width: 90%
.modal-header
margin-bottom: 20px
.modal-body
max-height: 500px
overflow: scroll
border-width: 2px 0
border-color: black
border-style: solid

View file

@ -46,6 +46,9 @@
color: #555555
font-size: 15px
margin-left: 5px
.panel-footer
text-align: right
.contributor-categories
list-style: none

View file

@ -0,0 +1,19 @@
extends /templates/base
block content
ol.breadcrumb
li
a(href="/")
span.glyphicon.glyphicon-home
li
a(href="/account", data-i18n="nav.account")
li.active(data-i18n="account_settings.title")
if !me.get('anonymous', true)
#save-button-container
button#save-button.btn-lg.btn.disabled(data-i18n="general.save" disabled="true") No Changes
#account-settings-view
block footer

View file

@ -1,34 +1,13 @@
extends /templates/base
if me.get('anonymous')
.alert.alert-danger(data-i18n="account_settings.not_logged_in") Log in or create an account to change your settings.
block content
h2(data-i18n="account_settings.title") Account Settings
if me.get('anonymous')
p(data-i18n="account_settings.not_logged_in") Log in or create an account to change your settings.
else
#save-button-container
button.btn#save-button.disabled(data-i18n="general.save" disabled="true") No Changes
ul.nav.nav-pills#settings-tabs
li
a(href="#general-pane", data-toggle="tab", data-i18n="account_settings.me_tab") Me
li
a(href="#picture-pane", data-toggle="tab", data-i18n="account_settings.picture_tab") Picture
li
a(href="#wizard-pane", data-toggle="tab", data-i18n="account_settings.wizard_tab") Wizard
li
a(href="#password-pane", data-toggle="tab", data-i18n="account_settings.password_tab") Password
li
a(href="#email-pane", data-toggle="tab", data-i18n="account_settings.emails_tab") Emails
if showsJobProfileTab
li
a(href="#job-profile-pane", data-toggle="tab", data-i18n="account_settings.job_profile_tab") Job Profile
.tab-content#settings-panes
#general-pane.tab-pane
p
else
.row
.col-md-6
.panel.panel-default
.panel-heading
.panel-title(data-i18n="account_settings.me_tab")
.panel-body
.form
- var name = me.get('name') || '';
- var email = me.get('email');
@ -43,19 +22,20 @@ block content
.form-group.checkbox
label(for="admin", data-i18n="account_settings.admin") Admin
input#admin(name="admin", type="checkbox", checked=admin)
#picture-pane.tab-pane
h3(data-i18n="account_settings.upload_picture") Upload a picture
#picture-treema
.gravatar-fallback
img(src=me.getPhotoURL(256), alt="Gravatar", title="Gravatar fallback image")
#wizard-pane.tab-pane
#wizard-settings-view
#password-pane.tab-pane
p
.panel.panel-default
.panel-heading
.panel-title(data-i18n="account_settings.picture_tab")
.panel-body
img.profile-photo(src=me.getPhotoURL(230), draggable="false")
input#photoURL(type="hidden", value=me.get('photoURL')||'')
button#upload-photo-button.btn.form-control.btn-primary(data-i18n="account_settings.upload_picture")
.panel.panel-default
.panel-heading
.panel-title(data-i18n="account_settings.password_tab")
.panel-body
.form
.form-group
label.control-label(for="password", data-i18n="account_settings.new_password") New Password
@ -63,11 +43,14 @@ block content
.form-group
label.control-label(for="password2", data-i18n="account_settings.new_password_verify") Verify
input#password2.form-control(name="password2", type="password")
#email-pane.tab-pane
h3(data-i18n="account_settings.email_subscriptions") Email Subscriptions
p
.col-md-6
#email-panel.panel.panel-default
.panel-heading
.panel-title(data-i18n="account_settings.emails_tab")
.panel-body
.form
.form-group.checkbox
label.control-label(for="email_generalNews", data-i18n="account_settings.email_announcements") Announcements
@ -85,19 +68,19 @@ block content
span.help-block(data-i18n="account_settings.email_any_notes_description") Disable to stop all activity notification emails.
fieldset#specific-notification-settings
.form-group.checkbox
label.control-label(for="email_recruitNotes", data-i18n="account_settings.email_recruit_notes") Job Opportunities
input#email_recruitNotes(name="email_recruitNotes", type="checkbox", checked=subs.recruitNotes)
span.help-block(data-i18n="account_settings.email_recruit_notes_description") If you play really well, we may contact you about getting you a (better) job.
hr
h4(data-i18n="account_settings.contributor_emails") Contributor Class Emails
span(data-i18n="account_settings.contribute_prefix") We're looking for people to join our party! Check out the
a(href="/contribute", data-i18n="account_settings.contribute_page") contribute page
span(data-i18n="account_settings.contribute_suffix") to find out more.
.form
.form-group.checkbox
label.control-label(for="email_archmageNews")
@ -108,7 +91,7 @@ block content
| (Coder)
input#email_archmageNews(name="email_archmageNews", type="checkbox", checked=subs.archmageNews)
span(data-i18n="contribute.archmage_subscribe_desc").help-block Get emails about general news and announcements about CodeCombat.
.form-group.checkbox
label.control-label(for="email_artisanNews")
span(data-i18n="classes.artisan_title")
@ -118,7 +101,7 @@ block content
| (Level Builder)
input#email_artisanNews(name="email_artisanNews", type="checkbox", checked=subs.artisanNews)
span(data-i18n="contribute.artisan_subscribe_desc").help-block Get emails on level editor updates and announcements.
.form-group.checkbox
label.control-label(for="email_adventurerNews")
span(data-i18n="classes.adventurer_title")
@ -128,7 +111,7 @@ block content
| (Level Playtester)
input#email_adventurerNews(name="email_adventurerNews", type="checkbox", checked=subs.adventurerNews)
span(data-i18n="contribute.adventurer_subscribe_desc").help-block Get emails when there are new levels to test.
.form-group.checkbox
label.control-label(for="email_scribeNews")
span(data-i18n="classes.scribe_title")
@ -138,7 +121,7 @@ block content
| (Article Editor)
input#email_scribeNews(name="email_scribeNews", type="checkbox", checked=subs.scribeNews)
span(data-i18n="contribute.scribe_subscribe_desc").help-block Get emails about article writing announcements.
.form-group.checkbox
label.control-label(for="email_diplomatNews")
span(data-i18n="classes.diplomat_title")
@ -148,7 +131,7 @@ block content
| (Translator)
input#email_diplomatNews(name="email_diplomatNews", type="checkbox", checked=subs.diplomatNews)
span(data-i18n="contribute.diplomat_subscribe_desc").help-block Get emails about i18n developments and, eventually, levels to translate.
.form-group.checkbox
label.control-label(for="email_ambassadorNews")
span(data-i18n="classes.ambassador_title")
@ -159,7 +142,6 @@ block content
input#email_ambassadorNews(name="email_ambassadorNews", type="checkbox", checked=subs.ambassadorNews)
span(data-i18n="contribute.ambassador_subscribe_desc").help-block Get emails on support updates and multiplayer developments.
button.btn#toggle-all-button(data-i18n="account_settings.email_toggle") Toggle All
button#toggle-all-button.btn.btn-primary.form-control(data-i18n="account_settings.email_toggle") Toggle All
#job-profile-pane.tab-pane
#job-profile-view
.clearfix

View file

@ -1,141 +0,0 @@
extends /templates/base
block content
if !me.isAnonymous()
.clearfix
.col-sm-6.clearfix
h2
span(data-i18n="account_settings.title") Account Settings
a.spl(href="/account/settings")
i.glyphicon.glyphicon-cog
hr
.row
.col-xs-6
.panel.panel-default
.panel-heading
h3.panel-title
i.glyphicon.glyphicon-picture
a(href="account/settings#picture" data-i18n="account_settings.picture_tab") Picture
.panel-body.text-center
img#picture(src="#{me.getPhotoURL(150)}" alt="Picture")
.col-xs-6
.panel.panel-default
.panel-heading
h3.panel-title
i.glyphicon.glyphicon-user
a(href="account/settings#wizard" data-i18n="account_settings.wizard_tab") Wizard
if (wizardSource)
.panel-body.text-center
img(src="#{wizardSource}")
.panel.panel-default.panel-me
.panel-heading
h3.panel-title
i.glyphicon.glyphicon-user
a(href="account/settings#me" data-i18n="account_settings.me_tab") Me
.panel-body
table
tr
th(data-i18n="general.name") Name
td=me.displayName()
tr
th(data-i18n="general.email") Email
td=me.get('email')
.panel.panel-default.panel-emails
.panel-heading
h3.panel-title
i.glyphicon.glyphicon-envelope
a(href="account/settings#emails" data-i18n="account_settings.emails_tab") Emails
.panel-body
if !hasEmailNotes && !hasEmailNews && !hasGeneralNews
p(data-i18n="account_settings.email_subscriptions_none") No email subscriptions.
if hasGeneralNews
h4(data-i18n="account_settings.email_news") News
ul
li(data-i18n="account_settings.email_announcements") Announcements
if hasEmailNotes
h4(data-i18n="account_settings.email_notifications") Notifications
ul
if subs.anyNotes
li(data-i18n="account_settings.email_any_notes") Any Notifications
if subs.recruitNotes
li(data-i18n="account_settings.email_recruit_notes") Job Opportunities
if hasEmailNews
h4(data-i18n="account_settings.contributor_emails") Contributor Emails
ul
if (subs.archmageNews)
li
span(data-i18n="classes.archmage_title")
| Archmage
span(data-i18n="classes.archmage_title_description")
| (Coder)
if (subs.artisanNews)
li
span.spr(data-i18n="classes.artisan_title")
| Artisan
span(data-i18n="classes.artisan_title_description")
| (Level Builder)
if (subs.adventurerNews)
li
span.spr(data-i18n="classes.adventurer_title")
| Adventurer
span(data-i18n="classes.adventurer_title_description")
| (Level Playtester)
if (subs.scribeNews)
li
span.spr(data-i18n="classes.scribe_title")
| Scribe
span(data-i18n="classes.scribe_title_description")
| (Article Editor)
if (subs.diplomatNews)
li
span.spr(data-i18n="classes.diplomat_title")
| Diplomat
span(data-i18n="classes.diplomat_title_description")
| (Translator)
if (subs.ambassadorNews)
li
span.spr(data-i18n="classes.ambassador_title")
| Ambassador
span(data-i18n="classes.ambassador_title_description")
| (Support)
.panel.panel-default
.panel-heading
h3.panel-title
i.glyphicon.glyphicon-wrench
a(href="account/settings#password" data-i18n="general.password") Password
//.panel.panel-default
// .panel-heading
// h3.panel-title
// i.glyphicon.glyphicon-briefcase
// a(href="account/settings#job-profile" data-i18n="account_settings.job_profile") Job Profile
.col-sm-6
h2(data-i18n="user.recently_played") Recently Played
hr
if !recentlyPlayed
div(data-i18n="common.loading") Loading...
else if recentlyPlayed.length
table.table
tr
th(data-i18n="resources.level") Level
th(data-i18n="user.last_played") Last Played
th(data-i18n="user.status") Status
each session in recentlyPlayed
if session.get('levelName')
tr
td
- var posturl = ''
- if (session.get('team')) posturl = '?team=' + session.get('team')
a(href="/play/level/#{session.get('levelID') + posturl}")= session.get('levelName') + (session.get('team') ? ' (' + session.get('team') + ')' : '')
td= moment(session.get('changed')).fromNow()
if session.get('state').complete === true
td(data-i18n="user.status_completed") Completed
else if ! session.isMultiplayer()
td(data-i18n="user.status_unfinished") Unfinished
else
td
else
.panel.panel-default
.panel-body
div(data-i18n="account.no_recent_games") No games played during the past two weeks.

View file

@ -0,0 +1,21 @@
extends /templates/base
block content
if me.get('anonymous')
p(data-i18n="account_settings.not_logged_in") Log in or create an account to change your settings.
else
ol.breadcrumb
li
a(href="/")
span.glyphicon.glyphicon-home
li.active(data-i18n="nav.account")
#account-links.panel.panel-default
.panel-heading(data-i18n="nav.account")
ul.list-group
li.list-group-item
a.btn.btn-lg.btn-primary(href="/account/settings", data-i18n="play.settings")
li.list-group-item
a.btn.btn-lg.btn-primary(href="/account/payments", data-i18n="account.payments")

View file

@ -0,0 +1,30 @@
extends /templates/base
block content
ol.breadcrumb
li
a(href="/")
span.glyphicon.glyphicon-home
li
a(href="/account", data-i18n="nav.account")
li.active(data-i18n="account.payments")
if payments.models.length
table.table.table-striped
tr
th(data-i18n="account.paid_on")
th(data-i18n="account.service")
th(data-i18n="account.price")
th(data-i18n="account.gems")
for payment in payments.models
- var service = payment.get('service')
tr
td= moment(payment.getCreationDate()).format('lll')
if service === 'ios'
td(data-i18n="account.service_apple")
td= payment.get('ios').localPrice
else
td(data-i18n="account.service_web")
td $#{(payment.get('amount')/100).toFixed(2)}
td= payment.get('gems')

View file

@ -1,7 +1,10 @@
block header
#site-nav
img#nav-logo(src="/images/pages/base/logo.png", title="CodeCombat - Learn how to code by playing a game", alt="CodeCombat")
a(href="/")
img#nav-logo(src="/images/pages/base/logo.png", title="CodeCombat - Learn how to code by playing a game", alt="CodeCombat")
div#site-nav-links
a(href="/")
img#small-nav-logo(src="/images/pages/base/logo.png", title="CodeCombat - Learn how to code by playing a game", alt="CodeCombat")
a(href="/")
span.glyphicon.glyphicon-home
a(href="/about", data-i18n="nav.about")
@ -17,7 +20,7 @@ block header
img.account-settings-image(src=me.getPhotoURL(18), alt="")
else
i.glyphicon.glyphicon-user
span.spl.spr(data-i18n="nav.account" href="/account") Account
span.spl.spr(data-i18n="nav.account" href="/account")
span.caret
ul.dropdown-menu(role="menu")
li.user-dropdown-header
@ -25,18 +28,14 @@ block header
a(href="/user/#{me.getSlugOrID()}")
img.img-circle(src="#{me.getPhotoURL()}" alt="")
h3=me.displayName()
li.user-dropdown-body
.col-xs-4.text-center
a(href="/user/#{me.getSlugOrID()}" data-i18n="nav.profile") Profile
.col-xs-4.text-center
a(href="/user/#{me.getSlugOrID()}/stats" data-i18n="nav.stats") Stats
.col-xs-4.text-center
a.disabled(data-i18n="nav.code") Code
li.user-dropdown-footer
.pull-left
a.btn.btn-default.btn-flat(href="/account" data-i18n="nav.account") Account
.pull-right
button#logout-button.btn.btn-default.btn-flat(data-i18n="login.log_out") Log Out
li
a(href="/user/#{me.getSlugOrID()}" data-i18n="nav.profile")
li
a(href="/account/settings", data-i18n="play.settings")
li
a(href="/account/payments", data-i18n="account.payments")
li
a#logout-button(data-i18n="login.log_out")
else
button.btn.btn-sm.btn-primary.header-font.signup-button(data-i18n="login.sign_up")

View file

@ -1,23 +1,26 @@
extends /templates/base
block content
.progress
.progress-bar.progress-bar-info(role="progressbar" aria-valuenow=progress aria-valuemin="0" aria-valuemax="100" style="width: "+progress+"%")= progress+"%"
if selectedLanguage
.progress
.progress-bar.progress-bar-info(role="progressbar" aria-valuenow=progress aria-valuemin="0" aria-valuemax="100" style="width: "+progress+"%")= progress+"%"
table.table.table-condensed
tr
th
select#language-select.form-control.input-sm
option(value='') Select one...
th Type
th Specifically Covered
th Generally Covered
for model in collection.models
tr
td
a(href=model.i18nURLBase+model.get('slug'))= model.get('name')
td= model.constructor.className
td(class=model.specificallyCovered ? 'success' : 'danger')= model.specificallyCovered ? 'Yes' : 'No'
td(class=model.generallyCovered ? 'success' : 'danger')= model.generallyCovered ? 'Yes' : 'No'
if selectedLanguage
for model in collection.models
tr
td
a(href=model.i18nURLBase+model.get('slug'))= model.get('name')
td= model.constructor.className
td(class=model.specificallyCovered ? 'success' : 'danger')= model.specificallyCovered ? 'Yes' : 'No'
td(class=model.generallyCovered ? 'success' : 'danger')= model.generallyCovered ? 'Yes' : 'No'

View file

@ -4,4 +4,7 @@ block modal-header-content
h3(data-i18n="play.account") Account
block modal-body-content
p TODO: show all dem account
#account-settings-view
block modal-footer-content
#save-button.btn-lg.btn.disabled(data-i18n="general.save" disabled="true") No Changes

View file

@ -1,54 +0,0 @@
extends /templates/kinds/user
block append content
.btn-group.pull-right
button#grid-layout-button.btn.btn-default(data-layout='grid', class=activeLayout==='grid' ? 'active' : '')
i.glyphicon.glyphicon-th
button#table-layout-button.btn.btn-default(data-layout='table', class=activeLayout==='table' ? 'active' : '')
i.glyphicon.glyphicon-th-list
if achievementsByCategory
if activeLayout === 'grid'
.grid-layout
each achievements, category in achievementsByCategory
.row
h2.achievement-category-title(data-i18n="achievements.category_#{category}")=category
each achievement, index in achievements
- var title = achievement.i18nName();
- var description = achievement.i18nDescription();
- var locked = ! achievement.get('unlocked');
- var style = achievement.getStyle()
- var imgURL = achievement.getImageURL();
if locked
- var imgURL = achievement.getLockedImageURL();
else
- var imgURL = achievement.getImageURL();
.col-lg-4.col-xs-12
include ../achievements/achievement-popup
else if activeLayout === 'table'
.table-layout
if earnedAchievements.length
table.table
tr
th(data-i18n="general.name") Name
th(data-i18n="general.description") Description
th(data-i18n="general.date") Date
th(data-i18n="achievements.amount_achieved") Amount
th XP
each earnedAchievement in earnedAchievements.models
- var achievement = earnedAchievement.get('achievement');
if achievement.get('category')
// No level-specific achievements in here.
tr
td= achievement.i18nName()
td= achievement.i18nDescription()
td= moment().format("MMMM Do YYYY", earnedAchievement.get('changed'))
if achievement.isRepeatable()
td= earnedAchievement.get('achievedAmount')
else
td
td= earnedAchievement.get('earnedPoints')
else
.panel#no-achievements
.panel-body(data-i18n="user.no_achievements") No achievements earned yet.
else
div How did you even do that?

View file

@ -14,17 +14,8 @@ block append content
span(data-i18n="user.favorite_prefix") Favorite language is
strong.favorite-language= favoriteLanguage
span(data-i18n="user.favorite_postfix") .
.btn-group-vertical.profile-menu
a.btn.btn-default(href="/user/#{user.getSlugOrID()}/profile")
i.glyphicon.glyphicon-briefcase
span(data-i18n="account_settings.job_profile") Job Profile
a.btn.btn-default(href="/user/#{user.getSlugOrID()}/stats")
i.glyphicon.glyphicon-certificate
span(data-i18n="user.stats") Stats
a.btn.btn-default.disabled(href="#")
i.glyphicon.glyphicon-pencil
span(data-i18n="general.code") Code
- var emails = user.get('emails')
- var emails = user.getEnabledEmails()
// TODO: fix this, use some other method for finding contributor classes other than email settings, since they're private... Maybe achievements?
if emails
ul.contributor-categories
//li.contributor-category
@ -69,9 +60,11 @@ block append content
th.col-xs-4(data-i18n="resources.level") Level
th.col-xs-4(data-i18n="user.last_played") Last Played
th.col-xs-4(data-i18n="user.status") Status
each session in singlePlayerSessions
- var count = 0
each session, index in singlePlayerSessions
if session.get('levelName')
tr
tr(class=count > 4 ? 'hide' : '')
- count++;
td
a(href="/play/level/#{session.get('levelID')}")= session.get('levelName')
td= moment(session.get('changed')).fromNow()
@ -79,6 +72,9 @@ block append content
td(data-i18n="user.status_completed") Completed
else
td(data-i18n="user.status_unfinished") Unfinished
if count > 4
.panel-footer
button.btn.btn-info.btn-sm.more-button(data-i18n="editor.more")
else
.panel-body
p(data-i18n="user.no_singleplayer") No Singleplayer games played yet.
@ -94,17 +90,20 @@ block append content
th.col-xs-4(data-i18n="resources.level") Level
th.col-xs-4(data-i18n="user.last_played") Last Played
th.col-xs-4(data-i18n="general.score") Score
each session in multiPlayerSessions
tr
each session, index in multiPlayerSessions
tr(class=index > 4 ? 'hide' : '')
td
- var posturl = ''
- if (session.get('team')) posturl = '?team=' + session.get('team')
a(href="/play/level/#{session.get('levelID') + posturl}")= session.get('levelName') + (session.get('team') ? ' (' + session.get('team') + ')' : '')
td= moment(session.get('changed')).fromNow()
if session.get('totalScore')
td= session.get('totalScore') * 100
td= parseInt(session.get('totalScore') * 100)
else
td(data-i18n="user.status_unfinished") Unfinished
if multiPlayerSessions.length > 4
.panel-footer
button.btn.btn-info.btn-sm.more-button(data-i18n="editor.more")
else
.panel-body
p(data-i18n="user.no_multiplayer") No Multiplayer games played yet.
@ -123,11 +122,15 @@ block append content
th.col-xs-4(data-i18n="achievements.achievement") Achievement
th.col-xs-4(data-i18n="achievements.last_earned") Last Earned
th.col-xs-4(data-i18n="achievements.amount_achieved") Amount
each achievement in earnedAchievements.models
tr
each achievement, index in earnedAchievements.models
tr(class=index > 4 ? 'hide' : '')
td= achievement.get('achievementName')
td= moment().format("MMMM Do YYYY", achievement.get('changed'))
if achievement.get('achievedAmount')
td= achievement.get('achievedAmount')
else
td
if earnedAchievements.length > 4
.panel-footer
button.btn.btn-info.btn-sm.more-button(data-i18n="editor.more")

View file

@ -0,0 +1,48 @@
RootView = require 'views/kinds/RootView'
template = require 'templates/account/account-settings-root-view'
AccountSettingsView = require './AccountSettingsView'
module.exports = class AccountSettingsRootView extends RootView
id: "account-settings-root-view"
template: template
events:
'click #save-button': -> @accountSettingsView.save()
shortcuts:
'enter': -> @
afterRender: ->
super()
@accountSettingsView = new AccountSettingsView()
@insertSubView(@accountSettingsView)
@listenTo @accountSettingsView, 'input-changed', @onInputChanged
@listenTo @accountSettingsView, 'save-user-began', @onUserSaveBegan
@listenTo @accountSettingsView, 'save-user-success', @onUserSaveSuccess
@listenTo @accountSettingsView, 'save-user-error', @onUserSaveError
onInputChanged: ->
@$el.find('#save-button')
.text($.i18n.t('common.save', defaultValue: 'Save'))
.addClass 'btn-info'
.removeClass 'disabled btn-danger'
.removeAttr 'disabled'
onUserSaveBegan: ->
@$el.find('#save-button')
.text($.i18n.t('common.saving', defaultValue: 'Saving...'))
.removeClass('btn-danger')
.addClass('btn-success').show()
onUserSaveSuccess: ->
@$el.find('#save-button')
.text($.i18n.t('account_settings.saved', defaultValue: 'Changes Saved'))
.removeClass('btn-success btn-info', 1000)
.attr('disabled', 'true')
onUserSaveError: ->
@$el.find('#save-button')
.text($.i18n.t('account_settings.error_saving', defaultValue: 'Error Saving'))
.removeClass('btn-success')
.addClass('btn-danger', 500)

View file

@ -1,72 +1,48 @@
RootView = require 'views/kinds/RootView'
template = require 'templates/account/settings'
CocoView = require 'views/kinds/CocoView'
template = require 'templates/account/account-settings-view'
{me} = require 'lib/auth'
forms = require 'lib/forms'
User = require 'models/User'
AuthModal = require 'views/modal/AuthModal'
WizardSettingsView = require './WizardSettingsView'
JobProfileTreemaView = require './JobProfileTreemaView'
module.exports = class AccountSettingsView extends RootView
module.exports = class AccountSettingsView extends CocoView
id: 'account-settings-view'
template: template
changedFields: [] # DOM input fields
className: 'countainer-fluid'
events:
'click #save-button': 'save'
'change #settings-panes input:checkbox': (e) -> @trigger 'checkboxToggled', e
'keyup #settings-panes input:text, #settings-panes input:password': (e) -> @trigger 'inputChanged', e
'keyup #name': 'onNameChange'
'change .panel input': 'onInputChanged'
'change #name': 'checkNameExists'
'click #toggle-all-button': 'toggleEmailSubscriptions'
'keypress #settings-panes': 'onKeyPress'
'click .profile-photo': 'onEditProfilePhoto'
'click #upload-photo-button': 'onEditProfilePhoto'
constructor: (options) ->
@save = _.debounce(@save, 200)
@onNameChange = _.debounce @checkNameExists, 500
super options
return unless me
require('lib/services/filepicker')() unless window.application.isIPadApp # Initialize if needed
@uploadFilePath = "db/user/#{me.id}"
@listenTo(me, 'invalid', (errors) -> forms.applyErrorsToForm(@$el, me.validationError))
@on 'checkboxToggled', @onToggle
@on 'checkboxToggled', @onInputChanged
@on 'inputChanged', @onInputChanged
@on 'enterPressed', @onEnter
afterInsert: ->
super()
@openModalView new AuthModal() if me.get('anonymous')
getRenderData: ->
c = super()
return c unless me
c.subs = {}
c.subs[sub] = 1 for sub in me.getEnabledEmails()
c
#- Form input callbacks
onInputChanged: (e) ->
return @enableSaveButton() unless e?.currentTarget
that = e.currentTarget
$that = $(that)
savedValue = $that.data 'saved-value'
currentValue = $that.val()
if savedValue isnt currentValue
@changedFields.push that unless that in @changedFields
@enableSaveButton()
else
_.pull @changedFields, that
@disableSaveButton() if _.isEmpty @changedFields
$(e.target).addClass 'changed'
@trigger 'input-changed'
onToggle: (e) ->
$that = $(e.currentTarget)
$that.val $that[0].checked
onEnter: ->
@save()
onKeyPress: (e) ->
@trigger 'enterPressed', e if e.which is 13
enableSaveButton: ->
$('#save-button', @$el).removeClass 'disabled'
$('#save-button', @$el).removeClass 'btn-danger'
$('#save-button', @$el).removeAttr 'disabled'
$('#save-button', @$el).text 'Save'
disableSaveButton: ->
$('#save-button', @$el).addClass 'disabled'
$('#save-button', @$el).removeClass 'btn-danger'
$('#save-button', @$el).attr 'disabled', "true"
$('#save-button', @$el).text 'No Changes'
toggleEmailSubscriptions: =>
subs = @getSubscriptions()
$('#email-panel input[type="checkbox"]', @$el).prop('checked', not _.any(_.values(subs))).addClass('changed')
checkNameExists: =>
name = $('#name', @$el).val()
@ -79,88 +55,53 @@ module.exports = class AccountSettingsView extends RootView
@suggestedName = newName
forms.setErrorToProperty @$el, 'name', "That name is taken! How about #{newName}?", true
afterRender: ->
super()
$('#settings-tabs a', @$el).click((e) =>
e.preventDefault()
$(e.target).tab('show')
# make sure errors show up in the general pane, but keep the password pane clean
$('#password-pane input').val('')
#@save() unless $(e.target).attr('href') is '#password-pane'
forms.clearFormAlerts($('#password-pane', @$el))
)
@chooseTab(location.hash.replace('#', ''))
wizardSettingsView = new WizardSettingsView()
@listenTo wizardSettingsView, 'change', @enableSaveButton
@insertSubView wizardSettingsView
@jobProfileTreemaView = new JobProfileTreemaView()
@listenTo @jobProfileTreemaView, 'change', @enableSaveButton
@insertSubView @jobProfileTreemaView
_.defer => @buildPictureTreema() # Not sure why, but the Treemas don't fully build without this if you reload the page.
afterInsert: ->
super()
$('#email-pane input[type="checkbox"]').on 'change', ->
$(@).addClass 'changed'
if me.get('anonymous')
@openModalView new AuthModal()
@updateSavedValues()
chooseTab: (category) ->
id = "##{category}-pane"
pane = $(id, @$el)
return @chooseTab('general') unless pane.length or category is 'general'
loc = "a[href=#{id}]"
$(loc, @$el).tab('show')
$('.tab-pane').removeClass('active')
pane.addClass('active')
@currentTab = category
getRenderData: ->
c = super()
return c unless me
c.subs = {}
c.subs[sub] = 1 for sub in c.me.getEnabledEmails()
c.showsJobProfileTab = me.isAdmin() or me.get('jobProfile') or location.hash.search('job-profile-') isnt -1
c
getSubscriptions: ->
inputs = ($(i) for i in $('#email-pane input[type="checkbox"].changed', @$el))
emailNames = (i.attr('name').replace('email_', '') for i in inputs)
enableds = (i.prop('checked') for i in inputs)
_.zipObject emailNames, enableds
toggleEmailSubscriptions: =>
subs = @getSubscriptions()
$('#email-pane input[type="checkbox"]', @$el).prop('checked', not _.any(_.values(subs))).addClass('changed')
@save()
buildPictureTreema: ->
data = photoURL: me.get('photoURL')
data.photoURL = null if data.photoURL?.search('gravatar') isnt -1 # Old style
schema = $.extend true, {}, me.schema()
schema.properties = _.pick me.schema().properties, 'photoURL'
schema.required = ['photoURL']
treemaOptions =
filePath: "db/user/#{me.id}"
schema: schema
data: data
callbacks: {change: @onPictureChanged}
@pictureTreema = @$el.find('#picture-treema').treema treemaOptions
@pictureTreema?.build()
@pictureTreema?.open()
@$el.find('.gravatar-fallback').toggle not me.get 'photoURL'
onPictureChanged: (e) =>
@trigger 'inputChanged', e
@$el.find('.gravatar-fallback').toggle not me.get 'photoURL'
save: (e) ->
#- Just copied from OptionsView, TODO refactor
onEditProfilePhoto: (e) ->
return if window.application.isIPadApp # TODO: have an iPad-native way of uploading a photo, since we don't want to load FilePicker on iPad (memory)
photoContainer = @$el.find('.profile-photo')
onSaving = =>
photoContainer.addClass('saving')
onSaved = (uploadingPath) =>
@$el.find('#photoURL').val(uploadingPath)
@onInputChanged() # cause for some reason editing the value doesn't trigger the jquery event
me.set('photoURL', uploadingPath)
photoContainer.removeClass('saving').attr('src', me.getPhotoURL(photoContainer.width()))
filepicker.pick {mimetypes: 'image/*'}, @onImageChosen(onSaving, onSaved)
formatImagePostData: (inkBlob) ->
url: inkBlob.url, filename: inkBlob.filename, mimetype: inkBlob.mimetype, path: @uploadFilePath, force: true
onImageChosen: (onSaving, onSaved) ->
(inkBlob) =>
onSaving()
uploadingPath = [@uploadFilePath, inkBlob.filename].join('/')
data = @formatImagePostData(inkBlob)
success = @onImageUploaded(onSaved, uploadingPath)
$.ajax '/file', type: 'POST', data: data, success: success
onImageUploaded: (onSaved, uploadingPath) ->
(e) =>
onSaved uploadingPath
#- Misc
getSubscriptions: ->
inputs = ($(i) for i in $('#email-panel input[type="checkbox"].changed', @$el))
emailNames = (i.attr('name').replace('email_', '') for i in inputs)
enableds = (i.prop('checked') for i in inputs)
_.zipObject emailNames, enableds
#- Saving changes
save: ->
$('#settings-tabs input').removeClass 'changed'
forms.clearFormAlerts(@$el)
@grabData()
@ -168,23 +109,23 @@ module.exports = class AccountSettingsView extends RootView
if res?
console.error 'Couldn\'t save because of validation errors:', res
forms.applyErrorsToForm(@$el, res)
$('.nano').nanoScroller({scrollTo: @$el.find('.has-error')})
return
return unless me.hasLocalChanges()
res = me.patch()
return unless res
save = $('#save-button', @$el).text($.i18n.t('common.saving', defaultValue: 'Saving...'))
.removeClass('btn-danger').addClass('btn-success').show()
res.error ->
res.error =>
errors = JSON.parse(res.responseText)
forms.applyErrorsToForm(@$el, errors)
save.text($.i18n.t('account_settings.error_saving', defaultValue: 'Error Saving')).removeClass('btn-success').addClass('btn-danger', 500)
$('.nano').nanoScroller({scrollTo: @$el.find('.has-error')})
@trigger 'save-user-error'
res.success (model, response, options) =>
@changedFields = []
@updateSavedValues()
save.text($.i18n.t('account_settings.saved', defaultValue: 'Changes Saved')).removeClass('btn-success', 500).attr('disabled', 'true')
@trigger 'save-user-success'
@trigger 'save-user-began'
grabData: ->
@grabPasswordData()
@ -198,6 +139,7 @@ module.exports = class AccountSettingsView extends RootView
message = $.i18n.t('account_settings.password_mismatch', defaultValue: 'Password does not match.')
err = [message: message, property: 'password2', formatted: true]
forms.applyErrorsToForm(@$el, err)
$('.nano').nanoScroller({scrollTo: @$el.find('.has-error')})
return
if bothThere
me.set('password', password1)
@ -205,36 +147,19 @@ module.exports = class AccountSettingsView extends RootView
message = $.i18n.t('account_settings.password_repeat', defaultValue: 'Please repeat your password.')
err = [message: message, property: 'password2', formatted: true]
forms.applyErrorsToForm(@$el, err)
$('.nano').nanoScroller({scrollTo: @$el.find('.has-error')})
grabOtherData: ->
$('#name', @$el).val @suggestedName if @suggestedName
me.set 'name', $('#name', @$el).val()
me.set 'email', $('#email', @$el).val()
@$el.find('#name').val @suggestedName if @suggestedName
me.set 'name', @$el.find('#name').val()
me.set 'email', @$el.find('#email').val()
for emailName, enabled of @getSubscriptions()
me.setEmailSubscription emailName, enabled
me.set 'photoURL', @pictureTreema.get('/photoURL')
me.set('photoURL', @$el.find('#photoURL').val())
adminCheckbox = @$el.find('#admin')
if adminCheckbox.length
permissions = []
permissions.push 'admin' if adminCheckbox.prop('checked')
me.set('permissions', permissions)
jobProfile = me.get('jobProfile') ? {}
updated = false
for key, val of @jobProfileTreemaView.getData()
updated = updated or not _.isEqual jobProfile[key], val
jobProfile[key] = val
if updated
jobProfile.updated = (new Date()).toISOString()
me.set 'jobProfile', jobProfile
updateSavedValues: ->
$('#settings-panes input:text').each ->
$(@).data 'saved-value', $(@).val()
$('#settings-panes input:checkbox').each ->
$(@).data 'saved-value', JSON.stringify $(@)[0].checked
destroy: ->
@pictureTreema?.destroy()
super()

View file

@ -1,38 +1,6 @@
View = require 'views/kinds/RootView'
template = require 'templates/account/account_home'
{me} = require 'lib/auth'
User = require 'models/User'
AuthModalView = require 'views/modal/AuthModal'
RecentlyPlayedCollection = require 'collections/RecentlyPlayedCollection'
ThangType = require 'models/ThangType'
RootView = require 'views/kinds/RootView'
template = require 'templates/account/main-account-view'
module.exports = class MainAccountView extends View
id: 'account-home'
template: template
constructor: (options) ->
super options
return unless me
@wizardType = ThangType.loadUniversalWizard()
@recentlyPlayed = new RecentlyPlayedCollection me.get('_id')
@supermodel.loadModel @wizardType, 'thang'
@supermodel.loadCollection @recentlyPlayed, 'recentlyPlayed'
onLoaded: ->
super()
getRenderData: ->
c = super()
c.subs = {}
enabledEmails = c.me.getEnabledEmails()
c.subs[sub] = 1 for sub in enabledEmails
c.hasEmailNotes = _.any enabledEmails, (sub) -> sub.contains 'Notes'
c.hasEmailNews = _.any enabledEmails, (sub) -> sub.contains('News') and sub isnt 'generalNews'
c.hasGeneralNews = 'generalNews' in enabledEmails
c.wizardSource = @wizardType.getPortraitSource colorConfig: me.get('wizard')?.colorConfig if @wizardType.loaded
c.recentlyPlayed = @recentlyPlayed.models
c
afterRender: ->
super()
@openModalView new AuthModalView if me.isAnonymous()
module.exports = class MainAccountView extends RootView
id: 'main-account-view'
template: template

View file

@ -0,0 +1,18 @@
RootView = require 'views/kinds/RootView'
template = require 'templates/account/payments-view'
CocoCollection = require 'collections/CocoCollection'
Payment = require 'models/Payment'
module.exports = class PaymentsView extends RootView
id: "payments-view"
template: template
constructor: (options) ->
super(options)
@payments = new CocoCollection([], { url: '/db/payment', model: Payment })
@supermodel.loadCollection(@payments, 'payments')
getRenderData: ->
c = super()
c.payments = @payments
c

View file

@ -47,6 +47,7 @@ module.exports = class I18NEditModelView extends RootView
@hush = true
$select = @$el.find('#language-select').empty()
@addLanguagesToSelect($select, @selectedLanguage)
@$el.find('option[value="en-US"]').remove()
@hush = false
editors = []
@ -123,6 +124,9 @@ module.exports = class I18NEditModelView extends RootView
onLanguageSelectChanged: (e) ->
return if @hush
@selectedLanguage = $(e.target).val()
if @selectedLanguage
me.set('preferredLanguage', @selectedLanguage)
me.patch()
@render()
onSubmitPatch: (e) ->

View file

@ -19,7 +19,7 @@ module.exports = class I18NHomeView extends RootView
constructor: (options) ->
super(options)
@selectedLanguage = me.get('preferredLanguage', true)
@selectedLanguage = me.get('preferredLanguage') or ''
#-
@aggregateModels = new Backbone.Collection()
@ -92,7 +92,12 @@ module.exports = class I18NHomeView extends RootView
afterRender: ->
super()
@addLanguagesToSelect(@$el.find('#language-select'), @selectedLanguage)
@$el.find('option[value="en-US"]').remove()
onLanguageSelectChanged: (e) ->
@selectedLanguage = $(e.target).val()
if @selectedLanguage
# simplest solution, see if this actually ends up being not what people want
me.set('preferredLanguage', @selectedLanguage)
me.patch()
@render()

View file

@ -76,7 +76,7 @@ module.exports = class RootView extends CocoView
@openModalView new ModalClass {}
showLoading: ($el) ->
$el ?= @$el.find('.main-content-area')
$el ?= @$el.find('#site-content-area')
super($el)
afterInsert: ->

View file

@ -25,6 +25,7 @@ module.exports = class AuthModal extends ModalView
'auth:logging-in-with-facebook': 'onLoggingInWithFacebook'
constructor: (options) ->
options ?= {}
@onNameChange = _.debounce @checkNameExists, 500
super options
@mode = options.mode if options.mode

View file

@ -1,15 +1,15 @@
ModalView = require 'views/kinds/ModalView'
template = require 'templates/play/modal/play-account-modal'
AccountSettingsView = require 'views/account/AccountSettingsView'
module.exports = class PlayAccountModal extends ModalView
className: 'modal fade play-modal'
template: template
modalWidthPercent: 90
plain: true
id: 'play-account-modal'
#instant: true
#events:
# 'change input.select': 'onSelectionChanged'
events:
'click #save-button': -> @accountSettingsView.save()
constructor: (options) ->
super options
@ -22,7 +22,32 @@ module.exports = class PlayAccountModal extends ModalView
super()
return unless @supermodel.finished()
Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'game-menu-open', volume: 1
@accountSettingsView = new AccountSettingsView()
@insertSubView(@accountSettingsView)
@listenTo @accountSettingsView, 'input-changed', @onInputChanged
@listenTo @accountSettingsView, 'save-user-began', @onUserSaveBegan
@listenTo @accountSettingsView, 'save-user-success', @hide
@listenTo @accountSettingsView, 'save-user-error', @onUserSaveError
onHidden: ->
super()
Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'game-menu-close', volume: 1
onInputChanged: ->
@$el.find('#save-button')
.text($.i18n.t('common.save', defaultValue: 'Save'))
.addClass 'btn-info'
.removeClass 'disabled btn-danger'
.removeAttr 'disabled'
onUserSaveBegan: ->
@$el.find('#save-button')
.text($.i18n.t('common.saving', defaultValue: 'Saving...'))
.removeClass('btn-danger')
.addClass('btn-success').show()
onUserSaveError: ->
@$el.find('#save-button')
.text($.i18n.t('account_settings.error_saving', defaultValue: 'Error Saving'))
.removeClass('btn-success')
.addClass('btn-danger', 500)

View file

@ -54,7 +54,6 @@ module.exports = class PlayItemsModal extends ModalView
constructor: (options) ->
super options
me.set('spent', 0)
@items = new Backbone.Collection()
@itemCategoryCollections = {}

View file

@ -1,55 +0,0 @@
UserView = require 'views/kinds/UserView'
template = require 'templates/user/achievements'
{me} = require 'lib/auth'
Achievement = require 'models/Achievement'
EarnedAchievement = require 'models/EarnedAchievement'
AchievementCollection = require 'collections/AchievementCollection'
EarnedAchievementCollection = require 'collections/EarnedAchievementCollection'
module.exports = class AchievementsView extends UserView
id: 'user-achievements-view'
template: template
viewName: 'Stats'
activeLayout: 'grid'
events:
'click #grid-layout-button': 'layoutChanged'
'click #table-layout-button': 'layoutChanged'
constructor: (userID, options) ->
super options, userID
onLoaded: ->
unless @achievements or @earnedAchievements
@supermodel.resetProgress()
@achievements = new AchievementCollection
@earnedAchievements = new EarnedAchievementCollection @user.getSlugOrID()
@supermodel.loadCollection @achievements, 'achievements'
@supermodel.loadCollection @earnedAchievements, 'earnedAchievements'
else
for earned in @earnedAchievements.models
return unless relatedAchievement = _.find @achievements.models, (achievement) ->
achievement.get('_id') is earned.get 'achievement'
relatedAchievement.set 'unlocked', true
earned.set 'achievement', relatedAchievement
deferredImages = (achievement.cacheLockedImage() for achievement in @achievements.models when not achievement.get 'unlocked')
whenever = $.when deferredImages...
whenever.done => @render()
super()
layoutChanged: (e) ->
@activeLayout = $(e.currentTarget).data 'layout'
@render()
getRenderData: ->
context = super()
context.activeLayout = @activeLayout
# After user is loaded
if @user and not @user.isAnonymous()
context.earnedAchievements = @earnedAchievements
context.achievementsByCategory = {}
for achievement in @achievements.models when achievement.get('category')
context.achievementsByCategory[achievement.get('category')] ?= []
context.achievementsByCategory[achievement.get('category')].push achievement
context

View file

@ -1,7 +1,7 @@
UserView = require 'views/kinds/UserView'
CocoCollection = require 'collections/CocoCollection'
LevelSession = require 'models/LevelSession'
template = require 'templates/user/user_home'
template = require 'templates/user/main-user-view'
{me} = require 'lib/auth'
EarnedAchievementCollection = require 'collections/EarnedAchievementCollection'
@ -15,6 +15,9 @@ class LevelSessionsCollection extends CocoCollection
module.exports = class MainUserView extends UserView
id: 'user-home'
template: template
events:
'click .more-button': 'onClickMoreButton'
constructor: (userID, options) ->
super options
@ -54,3 +57,8 @@ module.exports = class MainUserView extends UserView
@supermodel.loadCollection @levelSessions, 'levelSessions'
@supermodel.loadCollection @earnedAchievements, 'earnedAchievements'
super()
onClickMoreButton: (e) ->
panel = $(e.target).closest('.panel')
panel.find('tr.hide').removeClass('hide')
panel.find('.panel-footer').remove()

View file

@ -48,7 +48,13 @@ module.exports = class Handler
flattened = deltasLib.flattenDelta(delta)
_.all flattened, (delta) ->
# sometimes coverage gets moved around... allow other changes to happen to i18nCoverage
return _.isArray(delta.o) and (('i18n' in delta.dataPath and delta.o.length is 1) or 'i18nCoverage' in delta.dataPath)
return false unless _.isArray(delta.o)
return true if 'i18nCoverage' in delta.dataPath
return false unless delta.o.length is 1
index = delta.deltaPath.indexOf('i18n')
return false if index is -1
return false if delta.deltaPath[index+1] is 'en-US'
return true
formatEntity: (req, document) -> document?.toObject()
getEditableProperties: (req, document) ->

View file

@ -310,6 +310,7 @@ LevelHandler = class LevelHandler extends Handler
@sendSuccess res, data
hasAccessToDocument: (req, document, method=null) ->
method ?= req.method
return true if method is null or method is 'get'
super(req, document, method)

View file

@ -17,13 +17,13 @@ products = {
gems: 5000
id: 'gems_5'
}
'gems_10': {
amount: 999
gems: 11000
id: 'gems_10'
}
'gems_20': {
amount: 1999
gems: 25000
@ -43,7 +43,7 @@ PaymentHandler = class PaymentHandler extends Handler
payment.set 'recipient', req.user._id
payment.set 'created', new Date().toISOString()
payment
post: (req, res) ->
appleReceipt = req.body.apple?.rawReceipt
appleTransactionID = req.body.apple?.transactionID
@ -51,46 +51,46 @@ PaymentHandler = class PaymentHandler extends Handler
stripeToken = req.body.stripe?.token
stripeTimestamp = parseInt(req.body.stripe?.timestamp)
productID = req.body.productID
if not (appleReceipt or (stripeTimestamp and productID))
return @sendBadInputError(res, 'Need either apple.rawReceipt or stripe.timestamp and productID')
if stripeTimestamp and not productID
return @sendBadInputError(res, 'Need productID if paying with Stripe.')
if stripeTimestamp and (not stripeToken) and (not user.get('stripeCustomerID'))
return @sendBadInputError(res, 'Need stripe.token if new customer.')
if appleReceipt
if not appleTransactionID
return @sendBadInputError(res, 'Apple purchase? Need to specify which transaction.')
@handleApplePaymentPost(req, res, appleReceipt, appleTransactionID, appleLocalPrice)
else
@handleStripePaymentPost(req, res, stripeTimestamp, productID, stripeToken)
#- Apple payments
handleApplePaymentPost: (req, res, receipt, transactionID, localPrice) ->
formFields = { 'receipt-data': receipt }
#- verify receipt with Apple
#- verify receipt with Apple
verifyReq = request.post({url: config.apple.verifyURL, json: formFields}, (err, verifyRes, body) =>
if err or not body?.receipt?.in_app or (not body?.bundle_id is 'com.codecombat.CodeCombat')
console.warn 'apple receipt error?', err, body
@sendBadInputError(res, 'Unable to verify Apple receipt.')
return
transaction = _.find body.receipt.in_app, { transaction_id: transactionID }
return @sendBadInputError(res, 'Invalid transactionID.') unless transaction
return @sendBadInputError(res, 'Invalid transactionID.') unless transaction
#- Check existence
transactionID = transaction.transaction_id
criteria = { 'ios.transactionID': transactionID }
Payment.findOne(criteria).exec((err, payment) =>
if payment
unless payment.get('recipient').equals(req.user._id)
return @sendForbiddenError(res)
@ -119,17 +119,18 @@ PaymentHandler = class PaymentHandler extends Handler
return @sendDatabaseError(res, err) if err
@incrementGemsFor(req.user, product.gems, (err) =>
return @sendDatabaseError(res, err) if err
@sendPaymentHipChatMessage user: req.user, payment: payment
@sendCreated(res, @formatEntity(req, payment))
)
)
)
)
#- Stripe payments
handleStripePaymentPost: (req, res, timestamp, productID, token) ->
# First, make sure we save the payment info as a Customer object, if we haven't already.
if not req.user.get('stripeCustomerID')
stripe.customers.create({
@ -145,10 +146,10 @@ PaymentHandler = class PaymentHandler extends Handler
(err) =>
return @sendDatabaseError(res, err)
)
else
@beginStripePayment(req, res, timestamp, productID)
beginStripePayment: (req, res, timestamp, productID) ->
product = products[productID]
@ -168,29 +169,30 @@ PaymentHandler = class PaymentHandler extends Handler
)
)
],
((err, results) =>
return @sendDatabaseError(res, err) if err
return @sendDatabaseError(res, err) if err
[payment, charge] = results
if not (payment or charge)
# Proceed normally from the beginning
@chargeStripe(req, res, payment, product)
else if charge and not payment
# Initialized Payment. Start from charging.
@recordStripeCharge(req, res, payment, product, charge)
else
# Charged Stripe and recorded it. Recalculate gems to make sure credited the purchase.
@recalculateGemsFor(req.user, (err) =>
return @sendDatabaseError(res, err) if err
@sendPaymentHipChatMessage user: req.user, payment: payment
@sendSuccess(res, @formatEntity(req, payment))
)
)
)
chargeStripe: (req, res, payment, product) ->
stripe.charges.create({
amount: product.amount
@ -206,7 +208,7 @@ PaymentHandler = class PaymentHandler extends Handler
}).then(
# success case
((charge) => @recordStripeCharge(req, res, payment, product, charge)),
# error case
((err) =>
if err.type in ['StripeCardError', 'StripeInvalidRequestError']
@ -214,8 +216,8 @@ PaymentHandler = class PaymentHandler extends Handler
else
@sendDatabaseError(res, 'Error charging card, please retry.'))
)
recordStripeCharge: (req, res, payment, product, charge) ->
return @sendError(res, 500, 'Fake db error for testing.') if req.body.breakAfterCharging
payment = @makeNewInstance(req)
@ -241,9 +243,9 @@ PaymentHandler = class PaymentHandler extends Handler
)
)
#- Incrementing/recalculating gems
incrementGemsFor: (user, gems, done) ->
purchased = _.clone(user.get('purchased'))
if not purchased?.gems
@ -251,10 +253,10 @@ PaymentHandler = class PaymentHandler extends Handler
purchased.gems = gems
user.set('purchased', purchased)
user.save((err) -> done(err))
else
user.update({$inc: {'purchased.gems': gems}}, {}, (err) -> done(err))
recalculateGemsFor: (user, done) ->
Payment.find({recipient: user._id}).select('gems').exec((err, payments) ->
@ -264,7 +266,14 @@ PaymentHandler = class PaymentHandler extends Handler
purchased.gems = gems
user.set('purchased', purchased)
user.save((err) -> done(err))
)
sendPaymentHipChatMessage: (options) ->
try
message = "#{options.user?.get('name')} bought #{options.payment?.get('amount')} via #{options.payment?.get('service'}."
hipchat.sendHipChatMessage message
catch e
log.error "Couldn't send HipChat message on payment because of error: #{e}"
module.exports = new PaymentHandler()

View file

@ -21,6 +21,10 @@ AchievablePlugin = (schema, options) ->
# Check if an achievement has been earned
schema.post 'save', (doc) ->
# sometimes post appears to be called twice. Handle this...
# TODO: Refactor this system to make it request-specific,
# perhaps by having POST/PUT requests store the copy on the request object themselves.
return if doc.isInit('_id') and not (doc.id of before)
isNew = not doc.isInit('_id') or not (doc.id of before)
originalDocObj = before[doc.id] unless isNew

View file

@ -192,7 +192,8 @@ module.exports.loginUser = loginUser = (req, res, user, send=true, next=null) ->
module.exports.makeNewUser = makeNewUser = (req) ->
user = new User({anonymous: true})
user.set 'testGroupNumber', Math.floor(Math.random() * 256) # also in app/lib/auth
user.set 'preferredLanguage', languages.languageCodeFromAcceptedLanguages req.acceptedLanguages
lang = languages.languageCodeFromAcceptedLanguages req.acceptedLanguages
user.set 'preferredLanguage', lang if lang[...2] isnt 'en'
user.set 'lastIP', req.connection.remoteAddress
createMailOptions = (receiver, password) ->