mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-24 08:08:15 -05:00
Add course complete visual state for student CourseDetailsView
Also including a few misc tweaks to CourseDetailsView and the end-of-course HeroVictoryModal state.
This commit is contained in:
parent
cccf61e2e8
commit
c77e1c0fa2
6 changed files with 165 additions and 44 deletions
|
@ -1360,6 +1360,15 @@
|
|||
campaigns: "Campaigns"
|
||||
poll: "Poll"
|
||||
user_polls_record: "Poll Voting History"
|
||||
course: "Course"
|
||||
courses: "Courses"
|
||||
course_instance: "Course Instance"
|
||||
courses_instances: "Course Instances"
|
||||
classroom: "Classroom"
|
||||
classrooms: "Classrooms"
|
||||
clan: "Clan"
|
||||
clans: "Clans"
|
||||
members: "Members"
|
||||
|
||||
concepts:
|
||||
advanced_strings: "Advanced Strings"
|
||||
|
|
|
@ -140,3 +140,11 @@
|
|||
|
||||
.settings-name-input
|
||||
width: 50%
|
||||
|
||||
.jumbotron
|
||||
.btn
|
||||
white-space: normal
|
||||
min-height: 200px
|
||||
|
||||
h1
|
||||
font-size: 48px
|
||||
|
|
|
@ -38,7 +38,8 @@ block content
|
|||
|
||||
if !view.owner.isNew() && view.getOwnerName()
|
||||
span.spl.spr - Teacher:
|
||||
a(href="/user/#{view.owner.id}")
|
||||
//a(href="/user/#{view.owner.id}") // Don't link to profiles until we improve them
|
||||
span
|
||||
strong= view.getOwnerName()
|
||||
|
||||
h1
|
||||
|
@ -51,10 +52,60 @@ block content
|
|||
each line in courseInstance.get('description').split('\n')
|
||||
div= line
|
||||
|
||||
if view.courseComplete && !view.teacherMode
|
||||
.jumbotron
|
||||
.row
|
||||
if view.singlePlayerMode && !me.isAnonymous()
|
||||
.col-md-3
|
||||
.col-md-6
|
||||
a.btn.btn-lg.btn-success(href="/play")
|
||||
h1 Play the Campaign
|
||||
p You’re ready to take the next step! Explore hundreds of challenging levels, learn advanced programming skills, and compete in multiplayer arenas!
|
||||
.col-md-3
|
||||
else if view.singlePlayerMode && me.isAnonymous()
|
||||
.col-md-6
|
||||
a.btn.btn-lg.btn-success.signup-button
|
||||
h1 Create an Account
|
||||
p Sign up for a FREE CodeCombat account and gain access to more levels, more programming skills, and more fun!
|
||||
.col-md-6
|
||||
a.btn.btn-lg.btn-success(href="/play")
|
||||
h1 Preview Campaign
|
||||
p Take a sneak peek at all that CodeCombat has to offer before signing up for your FREE account.
|
||||
else if !view.singlePlayerMode
|
||||
.col-md-6
|
||||
if view.arenaLevel
|
||||
a.btn.btn-lg.btn-success.btn-play-level(data-level-slug=view.arenaLevel.slug, data-level-id=view.arenaLevel.original)
|
||||
h1
|
||||
span Arena
|
||||
| :
|
||||
span.spl= view.arenaLevel.name
|
||||
p= view.arenaLevel.description.replace(/!\[.*?\)/, '')
|
||||
else
|
||||
a.btn.btn-lg.btn-success.disabled
|
||||
h1 Arena Coming Soon
|
||||
p We are working on a multiplayer arena for classrooms at the end of #{course.get('name')}.
|
||||
.col-md-6
|
||||
if view.nextCourseInstance
|
||||
a.btn.btn-lg.btn-success(href="/courses/#{view.nextCourse.id}/#{view.nextCourseInstance.id}")
|
||||
h1= view.nextCourse.get('name')
|
||||
p= view.nextCourse.get('description')
|
||||
else if view.nextCourse
|
||||
a.btn.btn-lg.btn-success.disabled
|
||||
h1= view.nextCourse.get('name')
|
||||
p
|
||||
em NOT ENROLLED
|
||||
p Ask your teacher to enroll you in the next course.
|
||||
else
|
||||
a.btn.btn-lg.btn-success(disabled=!view.nextCourse ? "disabled" : "")
|
||||
h1 Next Course
|
||||
p
|
||||
em COMING SOON
|
||||
p We are hard at work making more courses for you!
|
||||
|
||||
if !me.isAnonymous()
|
||||
div.well.well-sm(role='tabpanel')
|
||||
ul.nav.nav-pills(role='tablist')
|
||||
if adminMode
|
||||
if view.teacherMode
|
||||
li.active(role='presentation')
|
||||
a(href='#progress', aria-controls='progress', role='tab', data-toggle='tab', data-i18n="courses.progress")
|
||||
li(role='presentation')
|
||||
|
@ -65,7 +116,7 @@ block content
|
|||
li(role='presentation')
|
||||
a(href='#progress', aria-controls='progress', role='tab', data-toggle='tab', data-i18n="courses.progress")
|
||||
.tab-content
|
||||
if adminMode
|
||||
if view.teacherMode
|
||||
.tab-pane.active#progress(role='tabpanel')
|
||||
+progress-tab
|
||||
.tab-pane#levels(role='tabpanel')
|
||||
|
@ -241,7 +292,7 @@ mixin progress-members-popup-completed(i, level, session)
|
|||
p
|
||||
span.spr(data-i18n="courses.completed")
|
||||
span #{moment(session.get('changed')).format('MMMM Do YYYY, h:mm:ss a')}
|
||||
if adminMode
|
||||
if view.teacherMode || me.isAdmin()
|
||||
strong(data-i18n="clans.view_solution")
|
||||
|
||||
mixin progress-members-popup-started(i, level, session)
|
||||
|
@ -253,7 +304,7 @@ mixin progress-members-popup-started(i, level, session)
|
|||
p
|
||||
span.spr(data-i18n="clans.last_played")
|
||||
span #{moment(session.get('changed')).format('MMMM Do YYYY, h:mm:ss a')}
|
||||
if adminMode
|
||||
if view.teacherMode || me.isAdmin()
|
||||
strong(data-i18n="clans.view_solution")
|
||||
|
||||
mixin levels-tab
|
||||
|
@ -271,7 +322,7 @@ mixin levels-tab
|
|||
each level, levelID in campaign.get('levels')
|
||||
tr
|
||||
td
|
||||
if lastLevelCompleted || adminMode
|
||||
if lastLevelCompleted || view.teacherMode
|
||||
- var i18n = level.type === 'course-ladder' ? 'play.compete' : 'home.play';
|
||||
button.btn.btn-success.btn-play-level(data-level-slug=level.slug, data-i18n=i18n, data-level-id=levelID)
|
||||
td
|
||||
|
|
|
@ -12,10 +12,11 @@ block modal-body-content
|
|||
#victory-text= victoryText
|
||||
|
||||
if isCourseLevel
|
||||
if currentCourseName
|
||||
p
|
||||
span.spr.level-title(data-i18n="play_level.course")
|
||||
span.level-name= currentCourseName
|
||||
.course-name-container
|
||||
if currentCourseName
|
||||
p
|
||||
span.spr.level-title(data-i18n="play_level.course")
|
||||
span.level-name= currentCourseName
|
||||
.container-fluid
|
||||
.row
|
||||
.col-md-6
|
||||
|
@ -26,6 +27,10 @@ block modal-body-content
|
|||
if nextLevelName
|
||||
.level-title(data-i18n="play_level.next_level")
|
||||
.level-name= nextLevelName.replace('Course: ', '')
|
||||
else
|
||||
.level-title(data-i18n="play_level.course")
|
||||
.level-name(data-i18n="play_level.victory_title_suffix")
|
||||
|
||||
br
|
||||
|
||||
#level-feedback
|
||||
|
|
|
@ -15,6 +15,9 @@ autoplayedOnce = false
|
|||
module.exports = class CourseDetailsView extends RootView
|
||||
id: 'course-details-view'
|
||||
template: template
|
||||
teacherMode: false
|
||||
singlePlayerMode: false
|
||||
memberSort: 'nameAsc'
|
||||
|
||||
events:
|
||||
'change .progress-expand-checkbox': 'onCheckExpandedProgress'
|
||||
|
@ -31,8 +34,6 @@ module.exports = class CourseDetailsView extends RootView
|
|||
@courseID ?= options.courseID
|
||||
@courseInstanceID ?= options.courseInstanceID
|
||||
@classroom = new Classroom()
|
||||
@adminMode = me.isAdmin()
|
||||
@memberSort = 'nameAsc'
|
||||
@course = @supermodel.getModel(Course, @courseID) or new Course _id: @courseID
|
||||
@listenTo @course, 'sync', @onCourseSync
|
||||
@prepaid = new Prepaid()
|
||||
|
@ -43,7 +44,6 @@ module.exports = class CourseDetailsView extends RootView
|
|||
|
||||
getRenderData: ->
|
||||
context = super()
|
||||
context.adminMode = @adminMode ? false
|
||||
context.campaign = @campaign
|
||||
context.conceptsCompleted = @conceptsCompleted ? {}
|
||||
context.course = @course if @course?.loaded
|
||||
|
@ -64,11 +64,19 @@ module.exports = class CourseDetailsView extends RootView
|
|||
context.document = document
|
||||
context
|
||||
|
||||
afterRender: ->
|
||||
super()
|
||||
if @supermodel.finished() and @courseComplete and me.isAnonymous() and @options.justBeatLevel
|
||||
# TODO: Make an intermediate modal that tells them they've finished HoC and has some snazzy stuff for convincing players to sign up instead of just throwing up the bare AuthModal
|
||||
AuthModal = require 'views/core/AuthModal'
|
||||
@openModalView new AuthModal showSignupRationale: true
|
||||
|
||||
onCourseSync: ->
|
||||
return if @destroyed
|
||||
# console.log 'onCourseSync'
|
||||
if me.isAnonymous() and (not me.get('hourOfCode') and not @course.get('hourOfCode'))
|
||||
@noCourseInstance = true
|
||||
@render?()
|
||||
@render()
|
||||
return
|
||||
return if @campaign?
|
||||
campaignID = @course.get('campaignID')
|
||||
|
@ -78,24 +86,36 @@ module.exports = class CourseDetailsView extends RootView
|
|||
@onCampaignSync()
|
||||
else
|
||||
@supermodel.loadModel @campaign, 'campaign'
|
||||
@render?()
|
||||
@render()
|
||||
|
||||
onCampaignSync: ->
|
||||
return if @destroyed
|
||||
# console.log 'onCampaignSync'
|
||||
if @courseInstanceID
|
||||
@loadCourseInstance(@courseInstanceID)
|
||||
else unless me.isAnonymous()
|
||||
@courseInstances = new CocoCollection([], { url: "/db/user/#{me.id}/course_instances", model: CourseInstance})
|
||||
@listenToOnce @courseInstances, 'sync', @onCourseInstancesSync
|
||||
@supermodel.loadCollection(@courseInstances, 'course_instances')
|
||||
@loadCourseInstances()
|
||||
@levelConceptMap = {}
|
||||
for levelID, level of @campaign.get('levels')
|
||||
@levelConceptMap[levelID] ?= {}
|
||||
for concept in level.concepts
|
||||
@levelConceptMap[levelID][concept] = true
|
||||
@render?()
|
||||
if level.type is 'course-ladder'
|
||||
@arenaLevel = level
|
||||
@render()
|
||||
|
||||
loadCourseInstances: ->
|
||||
@courseInstances = new CocoCollection [], {url: "/db/user/#{me.id}/course_instances", model: CourseInstance, comparator: 'courseID'}
|
||||
@listenToOnce @courseInstances, 'sync', @onCourseInstancesSync
|
||||
@supermodel.loadCollection @courseInstances, 'course_instances'
|
||||
|
||||
loadAllCourses: ->
|
||||
@allCourses = new CocoCollection [], {url: "/db/course", model: Course, comparator: '_id'}
|
||||
@listenToOnce @allCourses, 'sync', @onAllCoursesSync
|
||||
@supermodel.loadCollection @allCourses, 'courses'
|
||||
|
||||
loadCourseInstance: (courseInstanceID) ->
|
||||
return if @destroyed
|
||||
# console.log 'loadCourseInstance'
|
||||
return if @courseInstance?
|
||||
@courseInstanceID = courseInstanceID
|
||||
|
@ -107,23 +127,29 @@ module.exports = class CourseDetailsView extends RootView
|
|||
@courseInstance = @supermodel.loadModel(@courseInstance, 'course_instance').model
|
||||
|
||||
onCourseInstancesSync: ->
|
||||
return if @destroyed
|
||||
# console.log 'onCourseInstancesSync'
|
||||
if @courseInstances.models.length is 1
|
||||
@loadCourseInstance(@courseInstances.models[0].id)
|
||||
else
|
||||
if @courseInstances.models.length is 0
|
||||
@noCourseInstance = true
|
||||
@findNextCourseInstance()
|
||||
if not @courseInstance
|
||||
# We are loading these to find the one we want to display.
|
||||
if @courseInstances.models.length is 1
|
||||
@loadCourseInstance(@courseInstances.models[0].id)
|
||||
else
|
||||
@noCourseInstanceSelected = true
|
||||
@render?()
|
||||
if @courseInstances.models.length is 0
|
||||
@noCourseInstance = true
|
||||
else
|
||||
@noCourseInstanceSelected = true
|
||||
@render()
|
||||
|
||||
onCourseInstanceSync: ->
|
||||
return if @destroyed
|
||||
# console.log 'onCourseInstanceSync'
|
||||
if @courseInstance.get('classroomID')
|
||||
@classroom = new Classroom({_id: @courseInstance.get('classroomID')})
|
||||
@supermodel.loadModel @classroom, 'classroom'
|
||||
@adminMode = true if @courseInstance.get('ownerID') is me.id and @courseInstance.get('name') isnt 'Single Player'
|
||||
@levelSessions = new CocoCollection([], { url: "/db/course_instance/#{@courseInstance.id}/level_sessions", model: LevelSession, comparator:'_id' })
|
||||
@singlePlayerMode = @courseInstance.get('name') is 'Single Player'
|
||||
@teacherMode = @courseInstance.get('ownerID') is me.id and not @singlePlayerMode
|
||||
@levelSessions = new CocoCollection([], { url: "/db/course_instance/#{@courseInstance.id}/level_sessions", model: LevelSession, comparator: '_id' })
|
||||
@listenToOnce @levelSessions, 'sync', @onLevelSessionsSync
|
||||
@supermodel.loadCollection @levelSessions, 'level_sessions', cache: false
|
||||
@members = new CocoCollection([], { url: "/db/course_instance/#{@courseInstance.id}/members", model: User, comparator: 'nameLower' })
|
||||
|
@ -131,19 +157,22 @@ module.exports = class CourseDetailsView extends RootView
|
|||
@supermodel.loadCollection @members, 'members', cache: false
|
||||
@owner = new User({_id: @courseInstance.get('ownerID')})
|
||||
@supermodel.loadModel @owner, 'user'
|
||||
if @adminMode and prepaidID = @courseInstance.get('prepaidID')
|
||||
if @teacherMode and prepaidID = @courseInstance.get('prepaidID')
|
||||
@prepaid = @supermodel.getModel(Prepaid, prepaidID) or new Prepaid _id: prepaidID
|
||||
@listenTo @prepaid, 'sync', @onPrepaidSync
|
||||
if @prepaid.loaded
|
||||
@onPrepaidSync()
|
||||
else
|
||||
@supermodel.loadModel @prepaid, 'prepaid'
|
||||
@render?()
|
||||
@render()
|
||||
|
||||
onPrepaidSync: ->
|
||||
@render?()
|
||||
return if @destroyed
|
||||
# TODO: why do we rerender here? Template doesn't use prepaid.
|
||||
@render()
|
||||
|
||||
onLevelSessionsSync: ->
|
||||
return if @destroyed
|
||||
# console.log 'onLevelSessionsSync'
|
||||
@instanceStats = averageLevelsCompleted: 0, furthestLevelCompleted: '', totalLevelsCompleted: 0, totalPlayTime: 0
|
||||
@memberStats = {}
|
||||
|
@ -197,39 +226,57 @@ module.exports = class CourseDetailsView extends RootView
|
|||
@conceptsCompleted[concept] ?= 0
|
||||
@conceptsCompleted[concept]++
|
||||
|
||||
if @memberStats[me.id]?.totalLevelsCompleted >= _.size @campaign.get('levels')
|
||||
if @memberStats[me.id]?.totalLevelsCompleted >= _.size(@campaign.get('levels')) - 1 # Don't need to complete arena
|
||||
@courseComplete = true
|
||||
@loadCourseInstances() unless @courseInstances # Find the next course instance to do.
|
||||
|
||||
@render?()
|
||||
@render()
|
||||
|
||||
# If we just joined a single-player course for Hour of Code, we automatically play.
|
||||
if @instanceStats.totalLevelsCompleted is 0 and @instanceStats.totalPlayTime is 0 and @courseInstance.get('members').length is 1 and me.get('hourOfCode') and not @adminMode and not autoplayedOnce
|
||||
if @instanceStats.totalLevelsCompleted is 0 and @instanceStats.totalPlayTime is 0 and @singlePlayerMode and not autoplayedOnce
|
||||
autoplayedOnce = true
|
||||
@$el.find('button.btn-play-level').click()
|
||||
|
||||
onMembersSync: ->
|
||||
return if @destroyed
|
||||
# console.log 'onMembersSync'
|
||||
@memberUserMap = {}
|
||||
for user in @members.models
|
||||
@memberUserMap[user.id] = user
|
||||
@sortMembers()
|
||||
@render?()
|
||||
@render()
|
||||
|
||||
onAllCoursesSync: ->
|
||||
@findNextCourseInstance()
|
||||
|
||||
findNextCourseInstance: ->
|
||||
@nextCourseInstance = _.find @courseInstances.models, (ci) =>
|
||||
# Sorted by courseID
|
||||
ci.get('classroomID') is @courseInstance.get('classroomID') and ci.id isnt @courseInstance.id and ci.get('courseID') > @course.id
|
||||
if @nextCourseInstance
|
||||
nextCourseID = @nextCourseInstance.get('courseID')
|
||||
@nextCourse = @supermodel.getModel(Course, nextCourseID) or new Course _id: nextCourseID
|
||||
@nextCourse = @supermodel.loadModel(@nextCourse, 'course').model
|
||||
else if @allCourses?.loaded
|
||||
@nextCourse = _.find @allCourses.models, (course) => course.id > @course.id
|
||||
else
|
||||
@loadAllCourses()
|
||||
|
||||
onCheckExpandedProgress: (e) ->
|
||||
@showExpandedProgress = $('.progress-expand-checkbox').prop('checked')
|
||||
# TODO: why does render reset the checkbox to be unchecked?
|
||||
@render?()
|
||||
@render()
|
||||
$('.progress-expand-checkbox').attr('checked', @showExpandedProgress)
|
||||
|
||||
onClickMemberHeader: (e) ->
|
||||
@memberSort = if @memberSort is 'nameAsc' then 'nameDesc' else 'nameAsc'
|
||||
@sortMembers()
|
||||
@render?()
|
||||
@render()
|
||||
|
||||
onClickProgressHeader: (e) ->
|
||||
@memberSort = if @memberSort is 'progressAsc' then 'progressDesc' else 'progressAsc'
|
||||
@sortMembers()
|
||||
@render?()
|
||||
@render()
|
||||
|
||||
onClickPlayLevel: (e) ->
|
||||
levelSlug = $(e.target).data('level-slug')
|
||||
|
@ -237,7 +284,7 @@ module.exports = class CourseDetailsView extends RootView
|
|||
level = @campaign.get('levels')[levelID]
|
||||
if level.type is 'course-ladder'
|
||||
route = '/play/ladder/' + levelSlug
|
||||
route += '/course/' + @courseInstance.id if @courseInstance.get('members').length > 1 # No league for solo courses
|
||||
route += '/course/' + @courseInstance.id unless @singlePlayerMode # No league for solo courses
|
||||
Backbone.Mediator.publish 'router:navigate', route: route
|
||||
else
|
||||
Backbone.Mediator.publish 'router:navigate', {
|
||||
|
@ -255,7 +302,7 @@ module.exports = class CourseDetailsView extends RootView
|
|||
@loadCourseInstance(courseInstanceID)
|
||||
|
||||
onClickProgressLevelCell: (e) ->
|
||||
return unless @adminMode
|
||||
return unless @teacherMode or me.isAdmin()
|
||||
levelID = $(e.currentTarget).data('level-id')
|
||||
levelSlug = $(e.currentTarget).data('level-slug')
|
||||
userID = $(e.currentTarget).data('user-id')
|
||||
|
|
|
@ -63,9 +63,10 @@ module.exports = class HeroVictoryModal extends ModalView
|
|||
else
|
||||
@readyToContinue = true
|
||||
@playSound 'victory'
|
||||
if @level.get('type', true) is 'course' and nextLevel = @level.get('nextLevel')
|
||||
@nextLevel = new Level().setURL "/db/level/#{nextLevel.original}/version/#{nextLevel.majorVersion}"
|
||||
@nextLevel = @supermodel.loadModel(@nextLevel, 'level').model
|
||||
if @level.get('type', true) is 'course'
|
||||
if nextLevel = @level.get('nextLevel')
|
||||
@nextLevel = new Level().setURL "/db/level/#{nextLevel.original}/version/#{nextLevel.majorVersion}"
|
||||
@nextLevel = @supermodel.loadModel(@nextLevel, 'level').model
|
||||
if @courseID
|
||||
@course = new Course().setURL "/db/course/#{@courseID}"
|
||||
@course = @supermodel.loadModel(@course, 'course').model
|
||||
|
|
Loading…
Reference in a new issue