mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-04-26 14:03:28 -04:00
Merge branch 'master' into course-correction
This commit is contained in:
commit
d9d5dce2e0
21 changed files with 717 additions and 193 deletions
README.md
app
assets/images/pages/about
core
locale
schemas/models
styles
templates
views
scripts/analytics/mongodb/queries
|
@ -31,6 +31,7 @@ Whether you're novice or pro, the CodeCombat team is ready to help you implement
|
|||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
|
BIN
app/assets/images/pages/about/josh_c_small.png
Normal file
BIN
app/assets/images/pages/about/josh_c_small.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 11 KiB |
127
app/core/d3_utils.coffee
Normal file
127
app/core/d3_utils.coffee
Normal file
|
@ -0,0 +1,127 @@
|
|||
# Caller needs require 'vendor/d3'
|
||||
|
||||
module.exports.createContiguousDays = (timeframeDays) ->
|
||||
# Return list of last 'timeframeDays' contiguous days in yyyy-mm-dd format
|
||||
days = []
|
||||
currentDate = new Date()
|
||||
currentDate.setUTCDate(currentDate.getUTCDate() - timeframeDays)
|
||||
for i in [0..timeframeDays]
|
||||
currentDay = currentDate.toISOString().substr(0, 10)
|
||||
days.push(currentDay)
|
||||
currentDate.setUTCDate(currentDate.getUTCDate() + 1)
|
||||
days
|
||||
|
||||
module.exports.createLineChart = (containerSelector, chartLines) ->
|
||||
# Creates a line chart within 'containerSelector' based on chartLines
|
||||
return unless chartLines?.length > 0 and containerSelector
|
||||
|
||||
margin = 20
|
||||
keyHeight = 20
|
||||
xAxisHeight = 20
|
||||
yAxisWidth = 40
|
||||
containerWidth = $(containerSelector).width()
|
||||
containerHeight = $(containerSelector).height()
|
||||
|
||||
yScaleCount = 0
|
||||
yScaleCount++ for line in chartLines when line.showYScale
|
||||
svg = d3.select(containerSelector).append("svg")
|
||||
.attr("width", containerWidth)
|
||||
.attr("height", containerHeight)
|
||||
width = containerWidth - margin * 2 - yAxisWidth * yScaleCount
|
||||
height = containerHeight - margin * 2 - xAxisHeight - keyHeight * chartLines.length
|
||||
currentLine = 0
|
||||
currentYScale = 0
|
||||
|
||||
# Horizontal guidelines
|
||||
marks = (Math.round(i * height / 5) for i in [1..5])
|
||||
yRange = d3.scale.linear().range([height, 0]).domain([0, height])
|
||||
svg.selectAll(".line")
|
||||
.data(marks)
|
||||
.enter()
|
||||
.append("line")
|
||||
.attr("x1", margin + yAxisWidth * yScaleCount)
|
||||
.attr("y1", (d) -> margin + yRange(d))
|
||||
.attr("x2", margin + yAxisWidth * yScaleCount + width)
|
||||
.attr("y2", (d) -> margin + yRange(d))
|
||||
.attr("stroke", 'gray')
|
||||
.style("opacity", "0.3")
|
||||
|
||||
for line in chartLines
|
||||
# continue unless line.enabled
|
||||
xRange = d3.scale.linear().range([0, width]).domain([d3.min(line.points, (d) -> d.x), d3.max(line.points, (d) -> d.x)])
|
||||
yRange = d3.scale.linear().range([height, 0]).domain([line.min, line.max])
|
||||
|
||||
# x-Axis
|
||||
if currentLine is 0
|
||||
startDay = new Date(line.points[0].day)
|
||||
endDay = new Date(line.points[line.points.length - 1].day)
|
||||
xAxisRange = d3.time.scale()
|
||||
.domain([startDay, endDay])
|
||||
.range([0, width])
|
||||
xAxis = d3.svg.axis()
|
||||
.scale(xAxisRange)
|
||||
svg.append("g")
|
||||
.attr("class", "x axis")
|
||||
.call(xAxis)
|
||||
.selectAll("text")
|
||||
.attr("dy", ".35em")
|
||||
.attr("transform", "translate(" + (margin + yAxisWidth) + "," + (height + margin) + ")")
|
||||
.style("text-anchor", "start")
|
||||
|
||||
if line.showYScale
|
||||
# y-Axis
|
||||
yAxisRange = d3.scale.linear().range([height, 0]).domain([line.min, line.max])
|
||||
yAxis = d3.svg.axis()
|
||||
.scale(yRange)
|
||||
.orient("left")
|
||||
svg.append("g")
|
||||
.attr("class", "y axis")
|
||||
.attr("transform", "translate(" + (margin + yAxisWidth * currentYScale) + "," + margin + ")")
|
||||
.style("color", line.lineColor)
|
||||
.call(yAxis)
|
||||
.selectAll("text")
|
||||
.attr("y", 0)
|
||||
.attr("x", 0)
|
||||
.attr("fill", line.lineColor)
|
||||
.style("text-anchor", "start")
|
||||
currentYScale++
|
||||
|
||||
# Key
|
||||
svg.append("line")
|
||||
.attr("x1", margin)
|
||||
.attr("y1", margin + height + xAxisHeight + keyHeight * currentLine + keyHeight / 2)
|
||||
.attr("x2", margin + 40)
|
||||
.attr("y2", margin + height + xAxisHeight + keyHeight * currentLine + keyHeight / 2)
|
||||
.attr("stroke", line.lineColor)
|
||||
.attr("class", "key-line")
|
||||
svg.append("text")
|
||||
.attr("x", margin + 40 + 10)
|
||||
.attr("y", margin + height + xAxisHeight + keyHeight * currentLine + (keyHeight + 10) / 2)
|
||||
.attr("fill", line.lineColor)
|
||||
.attr("class", "key-text")
|
||||
.text(line.description)
|
||||
|
||||
# Path and points
|
||||
svg.selectAll(".circle")
|
||||
.data(line.points)
|
||||
.enter()
|
||||
.append("circle")
|
||||
.attr("transform", "translate(" + (margin + yAxisWidth * yScaleCount) + "," + margin + ")")
|
||||
.attr("cx", (d) -> xRange(d.x))
|
||||
.attr("cy", (d) -> yRange(d.y))
|
||||
.attr("r", 2)
|
||||
.attr("fill", line.lineColor)
|
||||
.attr("stroke-width", 1)
|
||||
.attr("class", "graph-point")
|
||||
.attr("data-pointid", (d) -> "#{line.lineID}#{d.x}")
|
||||
d3line = d3.svg.line()
|
||||
.x((d) -> xRange(d.x))
|
||||
.y((d) -> yRange(d.y))
|
||||
.interpolate("linear")
|
||||
svg.append("path")
|
||||
.attr("d", d3line(line.points))
|
||||
.attr("transform", "translate(" + (margin + yAxisWidth * yScaleCount) + "," + margin + ")")
|
||||
.style("stroke-width", line.strokeWidth)
|
||||
.style("stroke", line.lineColor)
|
||||
.style("fill", "none")
|
||||
currentLine++
|
|
@ -248,3 +248,39 @@ module.exports.getPrepaidCodeAmount = getPrepaidCodeAmount = (price=999, users=0
|
|||
return 0 unless users > 0 and months > 0
|
||||
total = price * users * months
|
||||
total
|
||||
|
||||
module.exports.filterMarkdownCodeLanguages = (text) ->
|
||||
currentLanguage = me.get('aceConfig')?.language or 'python'
|
||||
excludedLanguages = _.without ['javascript', 'python', 'coffeescript', 'clojure', 'lua', 'io'], currentLanguage
|
||||
exclusionRegex = new RegExp "```(#{excludedLanguages.join('|')})\n[^`]+```\n?", 'gm'
|
||||
text.replace exclusionRegex, ''
|
||||
|
||||
module.exports.aceEditModes = aceEditModes =
|
||||
'javascript': 'ace/mode/javascript'
|
||||
'coffeescript': 'ace/mode/coffee'
|
||||
'python': 'ace/mode/python'
|
||||
'clojure': 'ace/mode/clojure'
|
||||
'lua': 'ace/mode/lua'
|
||||
'io': 'ace/mode/text'
|
||||
|
||||
module.exports.initializeACE = (el, codeLanguage) ->
|
||||
contents = $(el).text().trim()
|
||||
editor = ace.edit el
|
||||
editor.setOptions maxLines: Infinity
|
||||
editor.setReadOnly true
|
||||
editor.setTheme 'ace/theme/textmate'
|
||||
editor.setShowPrintMargin false
|
||||
editor.setShowFoldWidgets false
|
||||
editor.setHighlightActiveLine false
|
||||
editor.setHighlightActiveLine false
|
||||
editor.setBehavioursEnabled false
|
||||
editor.renderer.setShowGutter false
|
||||
editor.setValue contents
|
||||
editor.clearSelection()
|
||||
session = editor.getSession()
|
||||
session.setUseWorker false
|
||||
session.setMode aceEditModes[codeLanguage]
|
||||
session.setWrapLimitRange null
|
||||
session.setUseWrapMode true
|
||||
session.setNewLineMode 'unix'
|
||||
return editor
|
||||
|
|
|
@ -603,8 +603,8 @@
|
|||
rob_blurb: "Codes things and stuff"
|
||||
josh_c_title: "Game Designer"
|
||||
josh_c_blurb: "Designs games"
|
||||
carlos_title: "Region Manager"
|
||||
carlos_blurb: "CodeCombat Brazil"
|
||||
carlos_title: "Region Manager, Brazil"
|
||||
carlos_blurb: "Celery Man"
|
||||
|
||||
teachers:
|
||||
more_info: "More Info for Teachers"
|
||||
|
|
|
@ -79,15 +79,15 @@ _.extend LevelSessionSchema.properties,
|
|||
currentScriptOffset:
|
||||
type: 'number'
|
||||
|
||||
selected:
|
||||
selected: # Not tracked any more, delete with old level types
|
||||
type: [
|
||||
'null'
|
||||
'string'
|
||||
]
|
||||
playing:
|
||||
type: 'boolean' # Not tracked any more
|
||||
type: 'boolean' # Not tracked any more, delete with old level types
|
||||
frame:
|
||||
type: 'number' # Not tracked any more
|
||||
type: 'number' # Not tracked any more, delete with old level types
|
||||
thangs: # ... what is this? Is this used?
|
||||
type: 'object'
|
||||
additionalProperties:
|
||||
|
|
|
@ -14,3 +14,15 @@
|
|||
font-size: 70pt
|
||||
.description
|
||||
font-size: 8pt
|
||||
|
||||
.line-chart-container
|
||||
height: 500px
|
||||
width: 100%
|
||||
.x.axis
|
||||
font-size: 9pt
|
||||
path
|
||||
display: none
|
||||
.y.axis
|
||||
font-size: 9pt
|
||||
path
|
||||
display: none
|
||||
|
|
|
@ -72,7 +72,13 @@ $level-resize-transition-time: 0.5s
|
|||
width: 55%
|
||||
position: relative
|
||||
overflow: hidden
|
||||
@include transition($level-resize-transition-time ease-out)
|
||||
@include transition(all $level-resize-transition-time ease-out, z-index 1.2s linear)
|
||||
z-index: 0
|
||||
|
||||
&.preview-overlay
|
||||
z-index: 20
|
||||
#goals-view
|
||||
visibility: hidden
|
||||
|
||||
canvas#webgl-surface
|
||||
background-color: #333
|
||||
|
|
|
@ -8,24 +8,48 @@
|
|||
background-position: top $backgroundPosition
|
||||
background-size: contain
|
||||
|
||||
$UNVEIL_TIME: 1.2s
|
||||
|
||||
#level-loading-view
|
||||
width: 100%
|
||||
height: 100%
|
||||
position: absolute
|
||||
z-index: 20
|
||||
$UNVEIL_TIME: 1.2s
|
||||
|
||||
&.unveiled
|
||||
pointer-events: none
|
||||
|
||||
.loading-details
|
||||
|
||||
&.preview-screen
|
||||
background-color: rgba(0, 0, 0, 0.5)
|
||||
|
||||
.left-wing, .right-wing
|
||||
width: 100%
|
||||
height: 100%
|
||||
position: absolute
|
||||
pointer-events: none
|
||||
|
||||
.left-wing
|
||||
@include wing-background('/images/level/loading_left_wing_1920.jpg', right)
|
||||
@media screen and ( max-width: 1366px )
|
||||
@include wing-background('/images/level/loading_left_wing_1366.jpg', right)
|
||||
left: -50%
|
||||
@include transition(all $UNVEIL_TIME ease)
|
||||
|
||||
.right-wing
|
||||
@include wing-background('/images/level/loading_right_wing_1920.jpg', left)
|
||||
@media screen and ( max-width: 1366px )
|
||||
@include wing-background('/images/level/loading_right_wing_1366.jpg', left)
|
||||
right: -50%
|
||||
@include transition(all $UNVEIL_TIME ease)
|
||||
|
||||
#loading-details
|
||||
position: absolute
|
||||
top: 86px
|
||||
left: 50%
|
||||
right: 50%
|
||||
$WIDTH: 450px
|
||||
width: $WIDTH
|
||||
height: 450px
|
||||
margin-left: (-$WIDTH / 2)
|
||||
margin-right: (-$WIDTH / 2)
|
||||
z-index: 100
|
||||
background: transparent url(/images/level/code_editor_background.png) no-repeat
|
||||
background-size: 100% 100%
|
||||
|
@ -34,9 +58,22 @@
|
|||
padding: 80px 80px 40px 80px
|
||||
text-align: center
|
||||
// http://matthewlein.com/ceaser/ Bounce down a bit, then snap up.
|
||||
@include transition(top $UNVEIL_TIME cubic-bezier(0.285, -0.595, 0.670, -0.600))
|
||||
@include transition($UNVEIL_TIME cubic-bezier(0.285, -0.595, 0.670, -0.600))
|
||||
font-family: 'Open Sans Condensed'
|
||||
|
||||
&.preview
|
||||
top: 0
|
||||
right: 0
|
||||
margin-right: 0
|
||||
width: 45%
|
||||
height: auto
|
||||
pointer-events: all
|
||||
@include transition($UNVEIL_TIME ease-in-out)
|
||||
|
||||
padding: 80px 70px 40px 50px
|
||||
.progress-or-start-container.intro-footer
|
||||
bottom: 30px
|
||||
|
||||
.level-loading-goals
|
||||
text-align: left
|
||||
|
||||
|
@ -49,12 +86,21 @@
|
|||
font-size: 20px
|
||||
color: black
|
||||
|
||||
.intro-doc
|
||||
text-align: left
|
||||
font-size: 16px
|
||||
overflow: scroll
|
||||
|
||||
img
|
||||
max-width: 100%
|
||||
|
||||
.progress-or-start-container
|
||||
position: absolute
|
||||
bottom: 95px
|
||||
width: 325px
|
||||
height: 80px
|
||||
left: 48px
|
||||
right: 77px
|
||||
@include transition(bottom $UNVEIL_TIME ease-out)
|
||||
|
||||
.load-progress
|
||||
width: 100%
|
||||
|
@ -131,21 +177,7 @@
|
|||
width: 401px
|
||||
color: #666
|
||||
|
||||
.left-wing, .right-wing
|
||||
width: 100%
|
||||
height: 100%
|
||||
position: absolute
|
||||
|
||||
.left-wing
|
||||
@include wing-background('/images/level/loading_left_wing_1920.jpg', right)
|
||||
@media screen and ( max-width: 1366px )
|
||||
@include wing-background('/images/level/loading_left_wing_1366.jpg', right)
|
||||
left: -50%
|
||||
@include transition(all $UNVEIL_TIME ease)
|
||||
|
||||
.right-wing
|
||||
@include wing-background('/images/level/loading_right_wing_1920.jpg', left)
|
||||
@media screen and ( max-width: 1366px )
|
||||
@include wing-background('/images/level/loading_right_wing_1366.jpg', left)
|
||||
right: -50%
|
||||
@include transition(all $UNVEIL_TIME ease)
|
||||
&.preview #tip-wrapper
|
||||
left: 48px
|
||||
right: 77px
|
||||
width: auto
|
||||
|
|
|
@ -52,3 +52,14 @@
|
|||
border-image: url(/images/level/code_toolbar_submit_button_zazz_pressed.png) 14 20 20 20 fill round
|
||||
padding: 2px 0 0 2px
|
||||
color: white
|
||||
|
||||
#guide-view
|
||||
pre.ace_editor
|
||||
padding: 2px 4px
|
||||
border-radius: 4px
|
||||
background-color: #f9f2f4
|
||||
font-size: 12px
|
||||
font-family: Monaco, Menlo, Ubuntu Mono, Consolas, "source-code-pro", monospace !important
|
||||
|
||||
.ace_cursor, .ace_bracket
|
||||
display: none
|
||||
|
|
|
@ -184,8 +184,8 @@ block content
|
|||
| Compiler Engineer
|
||||
p(data-i18n="about.rob_blurb")
|
||||
| Codes things and stuff.
|
||||
|
||||
img(src="/images/pages/about/placeholder.png").img-thumbnail
|
||||
|
||||
img(src="/images/pages/about/josh_c_small.png").img-thumbnail
|
||||
.team_bio
|
||||
h4.team_name
|
||||
| Josh Callebaut
|
||||
|
@ -201,6 +201,6 @@ block content
|
|||
h4.team_name
|
||||
| Carlos Maia
|
||||
p(data-i18n="about.carlos_title")
|
||||
| Region Manager
|
||||
| Region Manager, Brazil
|
||||
p(data-i18n="about.carlos_blurb")
|
||||
| CodeCombat Brazil
|
||||
| Celery Man
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
extends /templates/base
|
||||
|
||||
block content
|
||||
|
||||
|
||||
//- NOTE: do not localize / i18n
|
||||
|
||||
if me.isAdmin()
|
||||
.container-fluid
|
||||
.row
|
||||
|
@ -18,6 +20,21 @@ block content
|
|||
div.description 30-day Active Users
|
||||
div.count= activeUsers[0].monthlyCount
|
||||
|
||||
h3 KPI 60 days
|
||||
.kpi-recent-chart.line-chart-container
|
||||
|
||||
h3 KPI 300 days
|
||||
.kpi-chart.line-chart-container
|
||||
|
||||
h3 Active Classes 90 days
|
||||
.active-classes-chart.line-chart-container
|
||||
|
||||
h3 Recurring Revenue 90 days
|
||||
.recurring-revenue-chart.line-chart-container
|
||||
|
||||
h3 Active Users 90 days
|
||||
.active-users-chart.line-chart-container
|
||||
|
||||
h1 Active Classes
|
||||
table.table.table-striped.table-condensed
|
||||
tr
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
.right-wing
|
||||
|
||||
.loading-details.loading-container
|
||||
#loading-details.loading-container
|
||||
|
||||
.level-loading-goals.secret
|
||||
.goals-title(data-i18n="play_level.goals") Goals
|
||||
|
@ -10,6 +10,8 @@
|
|||
|
||||
.errors
|
||||
|
||||
.intro-doc
|
||||
|
||||
.progress-or-start-container
|
||||
button.start-level-button.btn.btn-lg.btn-success.btn-illustrated.header-font.needsclick(data-i18n="play_level.loading_start") Start Level
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
require 'vendor/d3'
|
||||
d3Utils = require 'core/d3_utils'
|
||||
RootView = require 'views/core/RootView'
|
||||
template = require 'templates/admin/analytics'
|
||||
utils = require 'core/utils'
|
||||
|
@ -5,86 +7,11 @@ utils = require 'core/utils'
|
|||
module.exports = class AnalyticsView extends RootView
|
||||
id: 'admin-analytics-view'
|
||||
template: template
|
||||
lineColors: ['red', 'blue', 'green', 'purple', 'goldenrod', 'brown', 'darkcyan']
|
||||
|
||||
constructor: (options) ->
|
||||
super options
|
||||
|
||||
@supermodel.addRequestResource('active_classes', {
|
||||
url: '/db/analytics_perday/-/active_classes'
|
||||
method: 'POST'
|
||||
success: (data) =>
|
||||
@activeClassGroups = {}
|
||||
dayEventsMap = {}
|
||||
for activeClass in data
|
||||
dayEventsMap[activeClass.day] ?= {}
|
||||
dayEventsMap[activeClass.day]['Total'] = 0
|
||||
for event, val of activeClass.classes
|
||||
@activeClassGroups[event] = true
|
||||
dayEventsMap[activeClass.day][event] = val
|
||||
dayEventsMap[activeClass.day]['Total'] += val
|
||||
@activeClassGroups = Object.keys(@activeClassGroups)
|
||||
@activeClassGroups.push 'Total'
|
||||
for day of dayEventsMap
|
||||
for event in @activeClassGroups
|
||||
dayEventsMap[day][event] ?= 0
|
||||
@activeClasses = []
|
||||
for day of dayEventsMap
|
||||
data = day: day, groups: []
|
||||
for group in @activeClassGroups
|
||||
data.groups.push(dayEventsMap[day][group] ? 0)
|
||||
@activeClasses.push data
|
||||
@activeClasses.sort (a, b) -> b.day.localeCompare(a.day)
|
||||
@render?()
|
||||
}, 0).load()
|
||||
|
||||
@supermodel.addRequestResource('active_users', {
|
||||
url: '/db/analytics_perday/-/active_users'
|
||||
method: 'POST'
|
||||
success: (data) =>
|
||||
@activeUsers = data
|
||||
@activeUsers.sort (a, b) -> b.day.localeCompare(a.day)
|
||||
@render?()
|
||||
}, 0).load()
|
||||
|
||||
@supermodel.addRequestResource('recurring_revenue', {
|
||||
url: '/db/analytics_perday/-/recurring_revenue'
|
||||
method: 'POST'
|
||||
success: (data) =>
|
||||
@revenueGroups = {}
|
||||
dayGroupCountMap = {}
|
||||
for dailyRevenue in data
|
||||
dayGroupCountMap[dailyRevenue.day] ?= {}
|
||||
dayGroupCountMap[dailyRevenue.day]['Daily'] = 0
|
||||
for group, val of dailyRevenue.groups
|
||||
@revenueGroups[group] = true
|
||||
dayGroupCountMap[dailyRevenue.day][group] = val
|
||||
dayGroupCountMap[dailyRevenue.day]['Daily'] += val
|
||||
@revenueGroups = Object.keys(@revenueGroups)
|
||||
@revenueGroups.push 'Daily'
|
||||
@revenueGroups.push 'Monthly'
|
||||
for day of dayGroupCountMap
|
||||
for group in @revenueGroups
|
||||
dayGroupCountMap[day][group] ?= 0
|
||||
@revenue = []
|
||||
for day of dayGroupCountMap
|
||||
data = day: day, groups: []
|
||||
for group in @revenueGroups
|
||||
data.groups.push(dayGroupCountMap[day][group] ? 0)
|
||||
@revenue.push data
|
||||
@revenue.sort (a, b) -> b.day.localeCompare(a.day)
|
||||
monthlyValues = []
|
||||
|
||||
return unless @revenue.length > 0
|
||||
|
||||
for i in [@revenue.length-1..0]
|
||||
dailyTotal = @revenue[i].groups[@revenue[i].groups.length - 2]
|
||||
monthlyValues.push(dailyTotal)
|
||||
monthlyValues.shift() if monthlyValues.length > 30
|
||||
if monthlyValues.length is 30
|
||||
monthlyIndex = @revenue[i].groups.length - 1
|
||||
@revenue[i].groups[monthlyIndex] = _.reduce(monthlyValues, (s, num) -> s + num)
|
||||
@render?()
|
||||
}, 0).load()
|
||||
@loadData()
|
||||
|
||||
getRenderData: ->
|
||||
context = super()
|
||||
|
@ -94,3 +21,303 @@ module.exports = class AnalyticsView extends RootView
|
|||
context.revenue = @revenue ? []
|
||||
context.revenueGroups = @revenueGroups ? {}
|
||||
context
|
||||
|
||||
afterRender: ->
|
||||
super()
|
||||
@createLineCharts()
|
||||
|
||||
loadData: ->
|
||||
@supermodel.addRequestResource('active_classes', {
|
||||
url: '/db/analytics_perday/-/active_classes'
|
||||
method: 'POST'
|
||||
success: (data) =>
|
||||
# Organize data by day, then group
|
||||
groupMap = {}
|
||||
dayGroupMap = {}
|
||||
for activeClass in data
|
||||
dayGroupMap[activeClass.day] ?= {}
|
||||
dayGroupMap[activeClass.day]['Total'] = 0
|
||||
for group, val of activeClass.classes
|
||||
groupMap[group] = true
|
||||
dayGroupMap[activeClass.day][group] = val
|
||||
dayGroupMap[activeClass.day]['Total'] += val
|
||||
@activeClassGroups = Object.keys(groupMap)
|
||||
@activeClassGroups.push 'Total'
|
||||
# Build list of active classes, where each entry is a day of individual group values
|
||||
@activeClasses = []
|
||||
for day of dayGroupMap
|
||||
dashedDay = "#{day.substring(0, 4)}-#{day.substring(4, 6)}-#{day.substring(6, 8)}"
|
||||
data = day: dashedDay, groups: []
|
||||
for group in @activeClassGroups
|
||||
data.groups.push(dayGroupMap[day][group] ? 0)
|
||||
@activeClasses.push data
|
||||
@activeClasses.sort (a, b) -> b.day.localeCompare(a.day)
|
||||
|
||||
@updateAllKPIChartData()
|
||||
@updateActiveClassesChartData()
|
||||
@render?()
|
||||
}, 0).load()
|
||||
|
||||
@supermodel.addRequestResource('active_users', {
|
||||
url: '/db/analytics_perday/-/active_users'
|
||||
method: 'POST'
|
||||
success: (data) =>
|
||||
@activeUsers = data.map (a) ->
|
||||
a.day = "#{a.day.substring(0, 4)}-#{a.day.substring(4, 6)}-#{a.day.substring(6, 8)}"
|
||||
a
|
||||
@activeUsers.sort (a, b) -> b.day.localeCompare(a.day)
|
||||
|
||||
@updateAllKPIChartData()
|
||||
@updateActiveUsersChartData()
|
||||
@render?()
|
||||
}, 0).load()
|
||||
|
||||
@supermodel.addRequestResource('recurring_revenue', {
|
||||
url: '/db/analytics_perday/-/recurring_revenue'
|
||||
method: 'POST'
|
||||
success: (data) =>
|
||||
# Organize data by day, then group
|
||||
groupMap = {}
|
||||
dayGroupCountMap = {}
|
||||
for dailyRevenue in data
|
||||
dayGroupCountMap[dailyRevenue.day] ?= {}
|
||||
dayGroupCountMap[dailyRevenue.day]['Daily'] = 0
|
||||
for group, val of dailyRevenue.groups
|
||||
groupMap[group] = true
|
||||
dayGroupCountMap[dailyRevenue.day][group] = val
|
||||
dayGroupCountMap[dailyRevenue.day]['Daily'] += val
|
||||
@revenueGroups = Object.keys(groupMap)
|
||||
@revenueGroups.push 'Daily'
|
||||
# Build list of recurring revenue entries, where each entry is a day of individual group values
|
||||
@revenue = []
|
||||
for day of dayGroupCountMap
|
||||
dashedDay = "#{day.substring(0, 4)}-#{day.substring(4, 6)}-#{day.substring(6, 8)}"
|
||||
data = day: dashedDay, groups: []
|
||||
for group in @revenueGroups
|
||||
data.groups.push(dayGroupCountMap[day][group] ? 0)
|
||||
@revenue.push data
|
||||
@revenue.sort (a, b) -> b.day.localeCompare(a.day)
|
||||
|
||||
return unless @revenue.length > 0
|
||||
|
||||
# Add monthly recurring revenue values
|
||||
@revenueGroups.push 'Monthly'
|
||||
monthlyValues = []
|
||||
for i in [@revenue.length-1..0]
|
||||
dailyTotal = @revenue[i].groups[@revenue[i].groups.length - 1]
|
||||
monthlyValues.push(dailyTotal)
|
||||
monthlyValues.shift() while monthlyValues.length > 30
|
||||
if monthlyValues.length is 30
|
||||
@revenue[i].groups.push(_.reduce(monthlyValues, (s, num) -> s + num))
|
||||
|
||||
@updateAllKPIChartData()
|
||||
@updateRevenueChartData()
|
||||
@render?()
|
||||
}, 0).load()
|
||||
|
||||
createLineChartPoints: (days, data) ->
|
||||
points = []
|
||||
for entry, i in data
|
||||
points.push
|
||||
x: i
|
||||
y: entry.value
|
||||
day: entry.day
|
||||
|
||||
# Ensure points for each day
|
||||
for day, i in days
|
||||
if points.length <= i or points[i].day isnt day
|
||||
prevY = if i > 0 then points[i - 1].y else 0.0
|
||||
points.splice i, 0,
|
||||
y: prevY
|
||||
day: day
|
||||
points[i].x = i
|
||||
|
||||
points.splice(0, points.length - days.length) if points.length > days.length
|
||||
points
|
||||
|
||||
createLineCharts: ->
|
||||
d3Utils.createLineChart('.kpi-recent-chart', @kpiRecentChartLines)
|
||||
d3Utils.createLineChart('.kpi-chart', @kpiChartLines)
|
||||
d3Utils.createLineChart('.active-classes-chart', @activeClassesChartLines)
|
||||
d3Utils.createLineChart('.active-users-chart', @activeUsersChartLines)
|
||||
d3Utils.createLineChart('.recurring-revenue-chart', @revenueChartLines)
|
||||
|
||||
updateAllKPIChartData: ->
|
||||
@kpiRecentChartLines = []
|
||||
@kpiChartLines = []
|
||||
@updateKPIChartData(60, @kpiRecentChartLines)
|
||||
@updateKPIChartData(300, @kpiChartLines)
|
||||
|
||||
updateKPIChartData: (timeframeDays, chartLines) ->
|
||||
days = d3Utils.createContiguousDays(timeframeDays)
|
||||
|
||||
if @activeClasses?.length > 0
|
||||
data = []
|
||||
for entry in @activeClasses
|
||||
data.push
|
||||
day: entry.day
|
||||
value: entry.groups[entry.groups.length - 1]
|
||||
data.reverse()
|
||||
points = @createLineChartPoints(days, data)
|
||||
chartLines.push
|
||||
points: points
|
||||
description: '30-day Active Classes'
|
||||
lineColor: 'blue'
|
||||
strokeWidth: 1
|
||||
min: 0
|
||||
max: _.max(points, 'y').y
|
||||
showYScale: true
|
||||
|
||||
if @revenue?.length > 0
|
||||
data = []
|
||||
for entry in @revenue
|
||||
data.push
|
||||
day: entry.day
|
||||
value: entry.groups[entry.groups.length - 1] / 100000
|
||||
data.reverse()
|
||||
points = @createLineChartPoints(days, data)
|
||||
chartLines.push
|
||||
points: points
|
||||
description: '30-day Recurring Revenue (in thousands)'
|
||||
lineColor: 'green'
|
||||
strokeWidth: 1
|
||||
min: 0
|
||||
max: _.max(points, 'y').y
|
||||
showYScale: true
|
||||
|
||||
if @activeUsers?.length > 0
|
||||
data = []
|
||||
for entry in @activeUsers
|
||||
break unless entry.monthlyCount
|
||||
data.push
|
||||
day: entry.day
|
||||
value: entry.monthlyCount / 1000
|
||||
data.reverse()
|
||||
points = @createLineChartPoints(days, data)
|
||||
chartLines.push
|
||||
points: points
|
||||
description: '30-day Active Users (in thousands)'
|
||||
lineColor: 'red'
|
||||
strokeWidth: 1
|
||||
min: 0
|
||||
max: _.max(points, 'y').y
|
||||
showYScale: true
|
||||
|
||||
updateActiveClassesChartData: ->
|
||||
@activeClassesChartLines = []
|
||||
return unless @activeClasses?.length
|
||||
days = d3Utils.createContiguousDays(90)
|
||||
|
||||
groupDayMap = {}
|
||||
for entry in @activeClasses
|
||||
for count, i in entry.groups
|
||||
groupDayMap[@activeClassGroups[i]] ?= {}
|
||||
groupDayMap[@activeClassGroups[i]][entry.day] ?= 0
|
||||
groupDayMap[@activeClassGroups[i]][entry.day] += count
|
||||
|
||||
lines = []
|
||||
colorIndex = 0
|
||||
totalMax = 0
|
||||
for group, entries of groupDayMap
|
||||
data = []
|
||||
for day, count of entries
|
||||
data.push
|
||||
day: day
|
||||
value: count
|
||||
data.reverse()
|
||||
points = @createLineChartPoints(days, data)
|
||||
@activeClassesChartLines.push
|
||||
points: points
|
||||
description: group.replace('Active classes ', '')
|
||||
lineColor: @lineColors[colorIndex++ % @lineColors.length]
|
||||
strokeWidth: 1
|
||||
min: 0
|
||||
showYScale: group is 'Total'
|
||||
totalMax = _.max(points, 'y').y if group is 'Total'
|
||||
line.max = totalMax for line in @activeClassesChartLines
|
||||
|
||||
updateActiveUsersChartData: ->
|
||||
@activeUsersChartLines = []
|
||||
return unless @activeUsers?.length
|
||||
days = d3Utils.createContiguousDays(90)
|
||||
|
||||
dailyData = []
|
||||
monthlyData = []
|
||||
dausmausData = []
|
||||
colorIndex = 0
|
||||
for entry in @activeUsers
|
||||
dailyData.push
|
||||
day: entry.day
|
||||
value: entry.dailyCount / 1000
|
||||
if entry.monthlyCount
|
||||
monthlyData.push
|
||||
day: entry.day
|
||||
value: entry.monthlyCount / 1000
|
||||
dausmausData.push
|
||||
day: entry.day
|
||||
value: Math.round(entry.dailyCount / entry.monthlyCount * 100)
|
||||
dailyData.reverse()
|
||||
monthlyData.reverse()
|
||||
dausmausData.reverse()
|
||||
dailyPoints = @createLineChartPoints(days, dailyData)
|
||||
monthlyPoints = @createLineChartPoints(days, monthlyData)
|
||||
dausmausPoints = @createLineChartPoints(days, dausmausData)
|
||||
@activeUsersChartLines.push
|
||||
points: dailyPoints
|
||||
description: 'Daily active users (in thousands)'
|
||||
lineColor: @lineColors[colorIndex++ % @lineColors.length]
|
||||
strokeWidth: 1
|
||||
min: 0
|
||||
max: _.max(dailyPoints, 'y').y
|
||||
showYScale: true
|
||||
@activeUsersChartLines.push
|
||||
points: monthlyPoints
|
||||
description: 'Monthly active users (in thousands)'
|
||||
lineColor: @lineColors[colorIndex++ % @lineColors.length]
|
||||
strokeWidth: 1
|
||||
min: 0
|
||||
max: _.max(monthlyPoints, 'y').y
|
||||
showYScale: true
|
||||
@activeUsersChartLines.push
|
||||
points: dausmausPoints
|
||||
description: 'DAUs/MAUs %'
|
||||
lineColor: @lineColors[colorIndex++ % @lineColors.length]
|
||||
strokeWidth: 1
|
||||
min: 0
|
||||
max: _.max(dausmausPoints, 'y').y
|
||||
showYScale: true
|
||||
|
||||
updateRevenueChartData: ->
|
||||
@revenueChartLines = []
|
||||
return unless @revenue?.length
|
||||
days = d3Utils.createContiguousDays(90)
|
||||
|
||||
groupDayMap = {}
|
||||
for entry in @revenue
|
||||
for count, i in entry.groups
|
||||
groupDayMap[@revenueGroups[i]] ?= {}
|
||||
groupDayMap[@revenueGroups[i]][entry.day] ?= 0
|
||||
groupDayMap[@revenueGroups[i]][entry.day] += count
|
||||
|
||||
lines = []
|
||||
colorIndex = 0
|
||||
dailyMax = 0
|
||||
for group, entries of groupDayMap
|
||||
data = []
|
||||
for day, count of entries
|
||||
data.push
|
||||
day: day
|
||||
value: count / 100
|
||||
data.reverse()
|
||||
points = @createLineChartPoints(days, data)
|
||||
@revenueChartLines.push
|
||||
points: points
|
||||
description: group.replace('DRR ', '')
|
||||
lineColor: @lineColors[colorIndex++ % @lineColors.length]
|
||||
strokeWidth: 1
|
||||
min: 0
|
||||
max: _.max(points, 'y').y
|
||||
showYScale: group in ['Daily', 'Monthly']
|
||||
dailyMax = _.max(points, 'y').y if group is 'Daily'
|
||||
for line in @revenueChartLines when line.description isnt 'Monthly'
|
||||
line.max = dailyMax
|
||||
|
|
|
@ -7,6 +7,7 @@ module.exports = class ArchmageView extends ContributeClassView
|
|||
contributorClassName: 'archmage'
|
||||
|
||||
contributors: [
|
||||
{id: '547acbb2af18b03c0563fdb3', name: 'David Liu', github: 'trotod'}
|
||||
{id: '52ccfc9bd3eb6b5a4100b60d', name: 'Glen De Cauwsemaecker', github: 'GlenDC'}
|
||||
{id: '52bfc3ecb7ec628868001297', name: 'Tom Steinbrecher', github: 'TomSteinbrecher'}
|
||||
{id: '5272806093680c5817033f73', name: 'Sébastien Moratinos', github: 'smoratinos'}
|
||||
|
@ -27,8 +28,8 @@ module.exports = class ArchmageView extends ContributeClassView
|
|||
{id: '531258b5e0789d4609614110', name: 'Ruben Vereecken', github: 'rubenvereecken'}
|
||||
{id: '5276ad5dcf83207a2801d3b4', name: 'Zach Martin', github: 'zachster01'}
|
||||
{id: '530df0cbc06854403ba67c15', name: 'Alexandru Caciulescu', github: 'Darredevil'}
|
||||
{id: '5268d9baa39d7db617000b18', name: 'Thanish Muhammed', github: 'mnmtanish'}
|
||||
{id: '53232f458e54704b074b271d', name: 'Bang Honam', github: 'walkingtospace'}
|
||||
{id: '5268d9baa39d7db617000b18', name: 'Thanish Muhammed', github: 'mnmtanish'}
|
||||
{id: '53232f458e54704b074b271d', name: 'Bang Honam', github: 'walkingtospace'}
|
||||
{id: '52d16c1dc931e2544d001daa', name: 'David Pendray', github: 'dpen2000'}
|
||||
{id: '53132ea1828a1706108ebb38', name: 'Dominik Kundel'}
|
||||
{id: '530eb29347a891b3518b3990', name: 'Ian Li'}
|
||||
|
|
|
@ -14,6 +14,7 @@ module.exports = class LevelLoadingView extends CocoView
|
|||
|
||||
subscriptions:
|
||||
'level:loaded': 'onLevelLoaded' # If Level loads after level loading view.
|
||||
'level:session-loaded': 'onSessionLoaded'
|
||||
'level:subscription-required': 'onSubscriptionRequired' # If they'd need a subscription to start playing.
|
||||
'level:course-membership-required': 'onCourseMembershipRequired' # If they'd need a subscription to start playing.
|
||||
'subscribe-modal:subscribed': 'onSubscribed'
|
||||
|
@ -44,6 +45,14 @@ module.exports = class LevelLoadingView extends CocoView
|
|||
|
||||
onLevelLoaded: (e) ->
|
||||
@level = e.level
|
||||
@prepareGoals()
|
||||
@prepareTip()
|
||||
@prepareIntro()
|
||||
|
||||
onSessionLoaded: (e) ->
|
||||
@session = e.session if e.session.get('creator') is me.id
|
||||
|
||||
prepareGoals: ->
|
||||
goalContainer = @$el.find('.level-loading-goals')
|
||||
goalList = goalContainer.find('ul')
|
||||
goalCount = 0
|
||||
|
@ -55,57 +64,121 @@ module.exports = class LevelLoadingView extends CocoView
|
|||
goalContainer.removeClass('secret')
|
||||
if goalCount is 1
|
||||
goalContainer.find('.panel-heading').text $.i18n.t 'play_level.goal' # Not plural
|
||||
|
||||
prepareTip: ->
|
||||
tip = @$el.find('.tip')
|
||||
if @level.get('loadingTip')
|
||||
loadingTip = utils.i18n @level.attributes, 'loadingTip'
|
||||
tip.text(loadingTip)
|
||||
tip.removeClass('secret')
|
||||
|
||||
prepareIntro: ->
|
||||
@docs = @level.get('documentation') ? {}
|
||||
specific = @docs.specificArticles or []
|
||||
@intro = _.find specific, name: 'Intro'
|
||||
|
||||
showReady: ->
|
||||
return if @shownReady
|
||||
@shownReady = true
|
||||
_.delay @finishShowingReady, 1500 # Let any blocking JS hog the main thread before we show that we're done.
|
||||
_.delay @finishShowingReady, 100 # Let any blocking JS hog the main thread before we show that we're done.
|
||||
|
||||
finishShowingReady: =>
|
||||
return if @destroyed
|
||||
if @options.autoUnveil
|
||||
showIntro = @getQueryVariable('intro')
|
||||
autoUnveil = not showIntro and (@options.autoUnveil or @session?.get('state').complete)
|
||||
if autoUnveil
|
||||
@startUnveiling()
|
||||
@unveil()
|
||||
@unveil true
|
||||
else
|
||||
@playSound 'level_loaded', 0.75 # old: loading_ready
|
||||
@$el.find('.progress').hide()
|
||||
@$el.find('.start-level-button').show()
|
||||
@unveil false
|
||||
|
||||
startUnveiling: (e) ->
|
||||
@playSound 'menu-button-click'
|
||||
@unveiling = true
|
||||
Backbone.Mediator.publish 'level:loading-view-unveiling', {}
|
||||
_.delay @onClickStartLevel, 1000 # If they never mouse-up for the click (or a modal shows up and interrupts the click), do it anyway.
|
||||
|
||||
onClickStartLevel: (e) =>
|
||||
return if @destroyed
|
||||
@unveil()
|
||||
@unveil true
|
||||
|
||||
onEnterPressed: (e) ->
|
||||
return unless @shownReady and not @$el.hasClass 'unveiled'
|
||||
return unless @shownReady and not @unveiled
|
||||
@startUnveiling()
|
||||
@onClickStartLevel()
|
||||
|
||||
unveil: ->
|
||||
return if @$el.hasClass 'unveiled'
|
||||
@$el.addClass 'unveiled'
|
||||
loadingDetails = @$el.find('.loading-details')
|
||||
duration = parseFloat loadingDetails.css 'transition-duration'
|
||||
loadingDetails.css 'top', -loadingDetails.outerHeight(true)
|
||||
unveil: (full) ->
|
||||
return if @destroyed or @unveiled
|
||||
@unveiled = full
|
||||
@$loadingDetails = @$el.find('#loading-details')
|
||||
duration = parseFloat(@$loadingDetails.css 'transition-duration') * 1000
|
||||
unless @$el.hasClass 'unveiled'
|
||||
@$el.addClass 'unveiled'
|
||||
@unveilWings duration
|
||||
if full
|
||||
@unveilLoadingFull()
|
||||
_.delay @onUnveilEnded, duration
|
||||
else
|
||||
@unveilLoadingPreview duration
|
||||
|
||||
unveilLoadingFull: ->
|
||||
# Get rid of the loading details screen entirely--the level is totally ready.
|
||||
unless @unveiling
|
||||
Backbone.Mediator.publish 'level:loading-view-unveiling', {}
|
||||
@unveiling = true
|
||||
if @$el.hasClass 'preview-screen'
|
||||
@$loadingDetails.css 'right', -@$loadingDetails.outerWidth(true)
|
||||
else
|
||||
@$loadingDetails.css 'top', -@$loadingDetails.outerHeight(true)
|
||||
@$el.removeClass 'preview-screen'
|
||||
$('#canvas-wrapper').removeClass 'preview-overlay'
|
||||
|
||||
unveilLoadingPreview: (duration) ->
|
||||
# Move the loading details screen over the code editor to preview the level.
|
||||
return if @$el.hasClass 'preview-screen'
|
||||
$('#canvas-wrapper').addClass 'preview-overlay'
|
||||
@$el.addClass('preview-screen')
|
||||
@$loadingDetails.addClass('preview')
|
||||
@resize()
|
||||
@onWindowResize = _.debounce @onWindowResize, 700 # Wait a bit for other views to resize before we resize
|
||||
$(window).on 'resize', @onWindowResize
|
||||
if @intro
|
||||
@$el.find('.progress-or-start-container').addClass('intro-footer')
|
||||
@$el.find('#tip-wrapper').remove()
|
||||
_.delay @unveilIntro, duration
|
||||
|
||||
resize: ->
|
||||
maxHeight = $('#page-container').outerHeight(true)
|
||||
minHeight = $('#code-area').outerHeight(true)
|
||||
@$el.css height: maxHeight
|
||||
@$loadingDetails.css minHeight: minHeight, maxHeight: maxHeight
|
||||
$intro = @$el.find('.intro-doc')
|
||||
$intro.css maxHeight: minHeight - $intro.offset().top - @$el.find('.progress-or-start-container').outerHeight() - 30 - 20
|
||||
|
||||
unveilWings: (duration) ->
|
||||
@playSound 'loading-view-unveil', 0.5
|
||||
@$el.find('.left-wing').css left: '-100%', backgroundPosition: 'right -400px top 0'
|
||||
@$el.find('.right-wing').css right: '-100%', backgroundPosition: 'left -400px top 0'
|
||||
@playSound 'loading-view-unveil', 0.5
|
||||
_.delay @onUnveilEnded, duration * 1000
|
||||
$('#level-footer-background').detach().appendTo('#page-container').slideDown(duration * 1000)
|
||||
$('#level-footer-background').detach().appendTo('#page-container').slideDown(duration)
|
||||
|
||||
unveilIntro: =>
|
||||
return if @destroyed or not @intro or @unveiled
|
||||
html = marked utils.filterMarkdownCodeLanguages(utils.i18n(@intro, 'body'))
|
||||
@$el.find('.intro-doc').html html
|
||||
@resize()
|
||||
|
||||
onUnveilEnded: =>
|
||||
return if @destroyed
|
||||
Backbone.Mediator.publish 'level:loading-view-unveiled', view: @
|
||||
|
||||
onWindowResize: (e) =>
|
||||
return if @destroyed
|
||||
@$loadingDetails.css transition: 'none'
|
||||
@resize()
|
||||
|
||||
onSubscriptionRequired: (e) ->
|
||||
@$el.find('.level-loading-goals, .tip, .load-progress').hide()
|
||||
@$el.find('.subscription-required').show()
|
||||
|
@ -120,3 +193,7 @@ module.exports = class LevelLoadingView extends CocoView
|
|||
|
||||
onSubscribed: ->
|
||||
document.location.reload()
|
||||
|
||||
destroy: ->
|
||||
$(window).off 'resize', @onWindowResize
|
||||
super()
|
||||
|
|
|
@ -155,7 +155,7 @@ module.exports = class PlayLevelView extends RootView
|
|||
afterRender: ->
|
||||
super()
|
||||
window.onPlayLevelViewLoaded? @ # still a hack
|
||||
@insertSubView @loadingView = new LevelLoadingView autoUnveil: @options.autoUnveil or @observing, level: @levelLoader?.level ? @level # May not have @level loaded yet
|
||||
@insertSubView @loadingView = new LevelLoadingView autoUnveil: @options.autoUnveil or @observing, level: @levelLoader?.level ? @level, session: @levelLoader?.session ? @session # May not have @level loaded yet
|
||||
@$el.find('#level-done-button').hide()
|
||||
$('body').addClass('is-playing')
|
||||
$('body').bind('touchmove', false) if @isIPadApp()
|
||||
|
@ -177,7 +177,6 @@ module.exports = class PlayLevelView extends RootView
|
|||
@initVolume()
|
||||
@listenTo(@session, 'change:multiplayer', @onMultiplayerChanged)
|
||||
|
||||
@originalSessionState = $.extend(true, {}, @session.get('state'))
|
||||
@register()
|
||||
@controlBar.setBus(@bus)
|
||||
@initScriptManager()
|
||||
|
@ -341,14 +340,16 @@ module.exports = class PlayLevelView extends RootView
|
|||
if window.currentModal and not window.currentModal.destroyed and window.currentModal.constructor isnt VictoryModal
|
||||
return Backbone.Mediator.subscribeOnce 'modal:closed', @onLevelStarted, @
|
||||
@surface.showLevel()
|
||||
if @isEditorPreview or @observing
|
||||
Backbone.Mediator.publish 'level:set-time', time: 0
|
||||
if (@isEditorPreview or @observing) and not @getQueryVariable('intro')
|
||||
@loadingView.startUnveiling()
|
||||
@loadingView.unveil()
|
||||
@loadingView.unveil true
|
||||
|
||||
onLoadingViewUnveiling: (e) ->
|
||||
@restoreSessionState()
|
||||
@selectHero()
|
||||
|
||||
onLoadingViewUnveiled: (e) ->
|
||||
Backbone.Mediator.publish 'level:set-playing', playing: true
|
||||
@loadingView.$el.remove()
|
||||
@removeSubView @loadingView
|
||||
@loadingView = null
|
||||
|
@ -372,21 +373,11 @@ module.exports = class PlayLevelView extends RootView
|
|||
@ambientSound = createjs.Sound.play src, loop: -1, volume: 0.1
|
||||
createjs.Tween.get(@ambientSound).to({volume: 1.0}, 10000)
|
||||
|
||||
restoreSessionState: ->
|
||||
return if @alreadyLoadedState
|
||||
@alreadyLoadedState = true
|
||||
state = @originalSessionState
|
||||
if not @level or @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder']
|
||||
Backbone.Mediator.publish 'level:suppress-selection-sounds', suppress: true
|
||||
Backbone.Mediator.publish 'tome:select-primary-sprite', {}
|
||||
Backbone.Mediator.publish 'level:suppress-selection-sounds', suppress: false
|
||||
@surface.focusOnHero()
|
||||
Backbone.Mediator.publish 'level:set-time', time: 0
|
||||
Backbone.Mediator.publish 'level:set-playing', playing: true
|
||||
else
|
||||
if state.selected
|
||||
# TODO: Should also restore selected spell here by saving spellName
|
||||
Backbone.Mediator.publish 'level:select-sprite', thangID: state.selected, spellName: null
|
||||
selectHero: ->
|
||||
Backbone.Mediator.publish 'level:suppress-selection-sounds', suppress: true
|
||||
Backbone.Mediator.publish 'tome:select-primary-sprite', {}
|
||||
Backbone.Mediator.publish 'level:suppress-selection-sounds', suppress: false
|
||||
@surface.focusOnHero()
|
||||
|
||||
# callbacks
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ template = require 'templates/play/level/tome/spell_palette_entry'
|
|||
{me} = require 'core/auth'
|
||||
filters = require 'lib/image_filter'
|
||||
DocFormatter = require './DocFormatter'
|
||||
SpellView = require 'views/play/level/tome/SpellView'
|
||||
utils = require 'core/utils'
|
||||
|
||||
module.exports = class SpellPaletteEntryView extends CocoView
|
||||
tagName: 'div' # Could also try <code> instead of <div>, but would need to adjust colors
|
||||
|
@ -59,26 +59,8 @@ module.exports = class SpellPaletteEntryView extends CocoView
|
|||
@aceEditors = []
|
||||
aceEditors = @aceEditors
|
||||
popover?.$tip?.find('.docs-ace').each ->
|
||||
contents = $(@).text()
|
||||
editor = ace.edit @
|
||||
editor.setOptions maxLines: Infinity
|
||||
editor.setReadOnly true
|
||||
editor.setTheme 'ace/theme/textmate'
|
||||
editor.setShowPrintMargin false
|
||||
editor.setShowFoldWidgets false
|
||||
editor.setHighlightActiveLine false
|
||||
editor.setHighlightActiveLine false
|
||||
editor.setBehavioursEnabled false
|
||||
editor.renderer.setShowGutter false
|
||||
editor.setValue contents
|
||||
editor.clearSelection()
|
||||
session = editor.getSession()
|
||||
session.setUseWorker false
|
||||
session.setMode SpellView.editModes[codeLanguage]
|
||||
session.setWrapLimitRange null
|
||||
session.setUseWrapMode true
|
||||
session.setNewLineMode 'unix'
|
||||
aceEditors.push editor
|
||||
aceEditor = utils.initializeACE @, codeLanguage
|
||||
aceEditors.push aceEditor
|
||||
|
||||
onMouseEnter: (e) ->
|
||||
# Make sure the doc has the updated Thang so it can regenerate its prop value
|
||||
|
|
|
@ -9,6 +9,7 @@ SpellDebugView = require './SpellDebugView'
|
|||
SpellToolbarView = require './SpellToolbarView'
|
||||
LevelComponent = require 'models/LevelComponent'
|
||||
UserCodeProblem = require 'models/UserCodeProblem'
|
||||
utils = require 'core/utils'
|
||||
|
||||
module.exports = class SpellView extends CocoView
|
||||
id: 'spell-view'
|
||||
|
@ -18,14 +19,6 @@ module.exports = class SpellView extends CocoView
|
|||
eventsSuppressed: true
|
||||
writable: true
|
||||
|
||||
@editModes:
|
||||
'javascript': 'ace/mode/javascript'
|
||||
'coffeescript': 'ace/mode/coffee'
|
||||
'python': 'ace/mode/python'
|
||||
'clojure': 'ace/mode/clojure'
|
||||
'lua': 'ace/mode/lua'
|
||||
'io': 'ace/mode/text'
|
||||
|
||||
keyBindings:
|
||||
'default': null
|
||||
'vim': 'ace/keyboard/vim'
|
||||
|
@ -93,7 +86,7 @@ module.exports = class SpellView extends CocoView
|
|||
@aceSession = @ace.getSession()
|
||||
@aceDoc = @aceSession.getDocument()
|
||||
@aceSession.setUseWorker false
|
||||
@aceSession.setMode SpellView.editModes[@spell.language]
|
||||
@aceSession.setMode utils.aceEditModes[@spell.language]
|
||||
@aceSession.setWrapLimitRange null
|
||||
@aceSession.setUseWrapMode true
|
||||
@aceSession.setNewLineMode 'unix'
|
||||
|
@ -479,7 +472,7 @@ module.exports = class SpellView extends CocoView
|
|||
|
||||
# window.zatannaInstance = @zatanna # For debugging. Make sure to not leave active when committing.
|
||||
# window.snippetEntries = snippetEntries
|
||||
lang = SpellView.editModes[e.language].substr 'ace/mode/'.length
|
||||
lang = utils.aceEditModes[e.language].substr 'ace/mode/'.length
|
||||
@zatanna.addSnippets snippetEntries, lang
|
||||
@editorLang = lang
|
||||
|
||||
|
@ -1138,8 +1131,8 @@ module.exports = class SpellView extends CocoView
|
|||
|
||||
onChangeLanguage: (e) ->
|
||||
return unless @spell.canWrite()
|
||||
@aceSession.setMode SpellView.editModes[e.language]
|
||||
@zatanna?.set 'language', SpellView.editModes[e.language].substr('ace/mode/')
|
||||
@aceSession.setMode utils.aceEditModes[e.language]
|
||||
@zatanna?.set 'language', utils.aceEditModes[e.language].substr('ace/mode/')
|
||||
wasDefault = @getSource() is @spell.originalSource
|
||||
@spell.setLanguage e.language
|
||||
@reloadCode true if wasDefault
|
||||
|
|
|
@ -4,8 +4,6 @@ Article = require 'models/Article'
|
|||
SubscribeModal = require 'views/core/SubscribeModal'
|
||||
utils = require 'core/utils'
|
||||
|
||||
# let's implement this once we have the docs database schema set up
|
||||
|
||||
module.exports = class LevelGuideView extends CocoView
|
||||
template: template
|
||||
id: 'guide-view'
|
||||
|
@ -41,10 +39,10 @@ module.exports = class LevelGuideView extends CocoView
|
|||
@docs = specific.concat(general)
|
||||
@docs = $.extend(true, [], @docs)
|
||||
@docs = [@docs[0]] if @firstOnly and @docs[0]
|
||||
doc.html = marked(@filterCodeLanguages(utils.i18n(doc, 'body'))) for doc in @docs
|
||||
doc.html = marked(utils.filterMarkdownCodeLanguages(utils.i18n(doc, 'body'))) for doc in @docs
|
||||
doc.name = (utils.i18n doc, 'name') for doc in @docs
|
||||
doc.slug = _.string.slugify(doc.name) for doc in @docs
|
||||
super()
|
||||
super options
|
||||
|
||||
destroy: ->
|
||||
if @vimeoListenerAttached
|
||||
|
@ -52,6 +50,7 @@ module.exports = class LevelGuideView extends CocoView
|
|||
window.removeEventListener('message', @onMessageReceived, false)
|
||||
else
|
||||
window.detachEvent('onmessage', @onMessageReceived, false)
|
||||
oldEditor.destroy() for oldEditor in @aceEditors ? []
|
||||
super()
|
||||
|
||||
getRenderData: ->
|
||||
|
@ -70,13 +69,17 @@ module.exports = class LevelGuideView extends CocoView
|
|||
@$el.find('.nav-tabs li:first').addClass('active')
|
||||
@$el.find('.tab-content .tab-pane:first').addClass('active')
|
||||
@$el.find('.nav-tabs a').click(@clickTab)
|
||||
@configureACEEditors()
|
||||
@playSound 'guide-open'
|
||||
|
||||
filterCodeLanguages: (text) ->
|
||||
currentLanguage = me.get('aceConfig')?.language or 'python'
|
||||
excludedLanguages = _.without ['javascript', 'python', 'coffeescript', 'clojure', 'lua', 'io'], currentLanguage
|
||||
exclusionRegex = new RegExp "```(#{excludedLanguages.join('|')})\n[^`]+```\n?", 'gm'
|
||||
text.replace exclusionRegex, ''
|
||||
configureACEEditors: ->
|
||||
oldEditor.destroy() for oldEditor in @aceEditors ? []
|
||||
@aceEditors = []
|
||||
aceEditors = @aceEditors
|
||||
codeLanguage = me.get('aceConfig')?.language or 'python'
|
||||
@$el.find('pre').each ->
|
||||
aceEditor = utils.initializeACE @, codeLanguage
|
||||
aceEditors.push aceEditor
|
||||
|
||||
clickSubscribe: (e) ->
|
||||
level = @levelSlug # Save ref to level slug
|
||||
|
|
|
@ -642,7 +642,13 @@ function getRecurringRevenueCounts(startDay) {
|
|||
var cursor = db.payments.find({_id: {$gte: startObj}});
|
||||
while (cursor.hasNext()) {
|
||||
var doc = cursor.next();
|
||||
var day = doc._id.getTimestamp().toISOString().substring(0, 10);
|
||||
var day;
|
||||
if (doc.created) {
|
||||
day = doc.created.substring(0, 10);
|
||||
}
|
||||
else {
|
||||
day = doc._id.getTimestamp().toISOString().substring(0, 10);
|
||||
}
|
||||
|
||||
if (doc.service === 'ios' || doc.service === 'bitcoin') continue;
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue