mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-04-26 14:03:28 -04:00
Merge branch 'master' into production
This commit is contained in:
commit
98d8a2cfb3
43 changed files with 587 additions and 805 deletions
app
Router.coffee
collections
lib
locale
models
styles
templates
account
account-settings-root-view.jadeaccount-settings-view.jadeaccount_home.jademain-account-view.jadepayments-view.jade
base.jadei18n
play/modal
user
views
server
commons
levels
payments
plugins
routes
|
@ -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'
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'}
|
||||
|
|
|
@ -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"
|
||||
|
|
5
app/models/Payment.coffee
Normal file
5
app/models/Payment.coffee
Normal file
|
@ -0,0 +1,5 @@
|
|||
CocoModel = require('./CocoModel')
|
||||
|
||||
module.exports = class Payment extends CocoModel
|
||||
@className: "Payment"
|
||||
urlRoot: "/db/payment"
|
|
@ -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
|
||||
|
|
49
app/styles/account/account-settings-view.sass
Normal file
49
app/styles/account/account-settings-view.sass
Normal 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
|
|
@ -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
|
||||
|
9
app/styles/account/main-account-view.sass
Normal file
9
app/styles/account/main-account-view.sass
Normal 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%
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -75,6 +75,7 @@ a
|
|||
.progress
|
||||
width: 50%
|
||||
margin: 0 25%
|
||||
margin-bottom: 20px
|
||||
|
||||
// all loading screens
|
||||
.loading-container
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -46,6 +46,9 @@
|
|||
color: #555555
|
||||
font-size: 15px
|
||||
margin-left: 5px
|
||||
|
||||
.panel-footer
|
||||
text-align: right
|
||||
|
||||
.contributor-categories
|
||||
list-style: none
|
19
app/templates/account/account-settings-root-view.jade
Normal file
19
app/templates/account/account-settings-root-view.jade
Normal 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
|
|
@ -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
|
|
@ -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.
|
21
app/templates/account/main-account-view.jade
Normal file
21
app/templates/account/main-account-view.jade
Normal 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")
|
30
app/templates/account/payments-view.jade
Normal file
30
app/templates/account/payments-view.jade
Normal 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')
|
|
@ -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")
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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?
|
|
@ -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")
|
||||
|
48
app/views/account/AccountSettingsRootView.coffee
Normal file
48
app/views/account/AccountSettingsRootView.coffee
Normal 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)
|
||||
|
|
@ -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()
|
|
@ -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
|
18
app/views/account/PaymentsView.coffee
Normal file
18
app/views/account/PaymentsView.coffee
Normal 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
|
|
@ -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) ->
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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: ->
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -54,7 +54,6 @@ module.exports = class PlayItemsModal extends ModalView
|
|||
|
||||
constructor: (options) ->
|
||||
super options
|
||||
me.set('spent', 0)
|
||||
@items = new Backbone.Collection()
|
||||
@itemCategoryCollections = {}
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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()
|
|
@ -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) ->
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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) ->
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue