codecombat/app/views/clans/ClanDetailsView.coffee

359 lines
15 KiB
CoffeeScript

RootView = require 'views/core/RootView'
template = require 'templates/clans/clan-details'
app = require 'core/application'
AuthModal = require 'views/core/AuthModal'
CocoCollection = require 'collections/CocoCollection'
Campaign = require 'models/Campaign'
Clan = require 'models/Clan'
EarnedAchievement = require 'models/EarnedAchievement'
LevelSession = require 'models/LevelSession'
SubscribeModal = require 'views/core/SubscribeModal'
ThangType = require 'models/ThangType'
User = require 'models/User'
utils = require 'core/utils'
# TODO: Add message for clan not found
# TODO: Progress visual for premium levels?
# TODO: Add expanded level names toggle
# TODO: Only need campaign data if clan is private
module.exports = class ClanDetailsView extends RootView
id: 'clan-details-view'
template: template
events:
'change .expand-progress-checkbox': 'onExpandedProgressCheckbox'
'click .delete-clan-btn': 'onDeleteClan'
'click .edit-description-save-btn': 'onEditDescriptionSave'
'click .edit-name-save-btn': 'onEditNameSave'
'click .join-clan-btn': 'onJoinClan'
'click .leave-clan-btn': 'onLeaveClan'
'click .member-header': 'onClickMemberHeader'
'click .progress-header': 'onClickProgressHeader'
'click .progress-level-cell': 'onClickLevel'
'click .remove-member-btn': 'onRemoveMember'
'mouseenter .progress-level-cell': 'onMouseEnterPoint'
'mouseleave .progress-level-cell': 'onMouseLeavePoint'
constructor: (options, @clanID) ->
super options
@initData()
destroy: ->
@stopListening?()
initData: ->
@showExpandedProgress = false
@memberSort = 'nameAsc'
@stats = {}
@campaigns = new CocoCollection([], { url: "/db/campaign", model: Campaign, comparator:'_id' })
@clan = new Clan _id: @clanID
@members = new CocoCollection([], { url: "/db/clan/#{@clanID}/members", model: User, comparator: 'nameLower' })
@memberAchievements = new CocoCollection([], { url: "/db/clan/#{@clanID}/member_achievements", model: EarnedAchievement, comparator:'_id' })
# MemberSessions: only loads creatorName, levelName, codeLanguage, submittedCodeLanguage for each session
@memberSessions = new CocoCollection([], { url: "/db/clan/#{@clanID}/member_sessions", model: LevelSession, comparator:'_id' })
@listenTo me, 'sync', => @render?()
@listenTo @campaigns, 'sync', @onCampaignSync
@listenTo @clan, 'sync', @onClanSync
@listenTo @members, 'sync', @onMembersSync
@listenTo @memberAchievements, 'sync', @onMemberAchievementsSync
@listenTo @memberSessions, 'sync', @onMemberSessionsSync
@supermodel.loadModel @campaigns, 'campaigns', cache: false
@supermodel.loadModel @clan, 'clan', cache: false
@supermodel.loadCollection(@members, 'members', {cache: false})
@supermodel.loadCollection(@memberAchievements, 'member_achievements', {cache: false})
@supermodel.loadCollection(@memberSessions, 'member_sessions', {cache: false})
getRenderData: ->
context = super()
context.campaignLevelProgressions = @campaignLevelProgressions ? []
context.clan = @clan
context.conceptsProgression = @conceptsProgression ? []
if application.isProduction()
context.joinClanLink = "https://codecombat.com/clans/#{@clanID}"
else
context.joinClanLink = "http://localhost:3000/clans/#{@clanID}"
context.owner = @owner
context.memberAchievementsMap = @memberAchievementsMap
context.memberLanguageMap = @memberLanguageMap
context.memberLevelStateMap = @memberLevelMap ? {}
context.memberMaxLevelCount = @memberMaxLevelCount
context.memberSort = @memberSort
context.isOwner = @clan.get('ownerID') is me.id
context.isMember = @clanID in (me.get('clans') ? [])
context.stats = @stats
# Find last campaign level for each user
# TODO: why do we do this for every render?
highestUserLevelCountMap = {}
lastUserCampaignLevelMap = {}
maxLastUserCampaignLevel = 0
userConceptsMap = {}
if @campaigns.loaded
levelCount = 0
for campaign in @campaigns.models
campaignID = campaign.id
lastLevelIndex = 0
for levelID, level of campaign.get('levels')
levelSlug = level.slug
for member in @members?.models ? []
if context.memberLevelStateMap[member.id]?[levelSlug]
lastUserCampaignLevelMap[member.id] ?= {}
lastUserCampaignLevelMap[member.id][campaignID] ?= {}
lastUserCampaignLevelMap[member.id][campaignID] =
levelSlug: levelSlug
index: lastLevelIndex
maxLastUserCampaignLevel = lastLevelIndex if lastLevelIndex > maxLastUserCampaignLevel
if level.concepts?
userConceptsMap[member.id] ?= {}
for concept in level.concepts
continue if userConceptsMap[member.id][concept] is 'complete'
userConceptsMap[member.id][concept] = context.memberLevelStateMap[member.id][levelSlug].state
highestUserLevelCountMap[member.id] = levelCount
lastLevelIndex++
levelCount++
@sortMembers(highestUserLevelCountMap, userConceptsMap) if @clan.get('dashboardType') is 'premium'
context.members = @members?.models ? []
context.lastUserCampaignLevelMap = lastUserCampaignLevelMap
context.showExpandedProgress = maxLastUserCampaignLevel <= 30 or @showExpandedProgress
context.userConceptsMap = userConceptsMap
context.arenas = @arenas
context.i18n = utils.i18n
context
afterRender: ->
super()
@updateHeroIcons()
refreshData: ->
me.fetch cache: false
@members.fetch cache: false
@memberAchievements.fetch cache: false
@memberSessions.fetch cache: false
sortMembers: (highestUserLevelCountMap, userConceptsMap) ->
# Progress sort precedence: most completed concepts, most started concepts, most levels, name sort
return unless @members? and @memberSort?
switch @memberSort
when "nameDesc"
@members.comparator = (a, b) -> return (b.get('name') or 'Anoner').localeCompare(a.get('name') or 'Anoner')
when "progressAsc"
@members.comparator = (a, b) ->
aComplete = (concept for concept, state of userConceptsMap[a.id] when state is 'complete')
bComplete = (concept for concept, state of userConceptsMap[b.id] when state is 'complete')
aStarted = (concept for concept, state of userConceptsMap[a.id] when state is 'started')
bStarted = (concept for concept, state of userConceptsMap[b.id] when state is 'started')
if aComplete < bComplete then return -1
else if aComplete > bComplete then return 1
else if aStarted < bStarted then return -1
else if aStarted > bStarted then return 1
if highestUserLevelCountMap[a.id] < highestUserLevelCountMap[b.id] then return -1
else if highestUserLevelCountMap[a.id] > highestUserLevelCountMap[b.id] then return 1
(a.get('name') or 'Anoner').localeCompare(b.get('name') or 'Anoner')
when "progressDesc"
@members.comparator = (a, b) ->
aComplete = (concept for concept, state of userConceptsMap[a.id] when state is 'complete')
bComplete = (concept for concept, state of userConceptsMap[b.id] when state is 'complete')
aStarted = (concept for concept, state of userConceptsMap[a.id] when state is 'started')
bStarted = (concept for concept, state of userConceptsMap[b.id] when state is 'started')
if aComplete > bComplete then return -1
else if aComplete < bComplete then return 1
else if aStarted > bStarted then return -1
else if aStarted < bStarted then return 1
if highestUserLevelCountMap[a.id] > highestUserLevelCountMap[b.id] then return -1
else if highestUserLevelCountMap[a.id] < highestUserLevelCountMap[b.id] then return 1
(b.get('name') or 'Anoner').localeCompare(a.get('name') or 'Anoner')
else
@members.comparator = (a, b) -> return (a.get('name') or 'Anoner').localeCompare(b.get('name') or 'Anoner')
@members.sort()
updateHeroIcons: ->
return unless @members?.models?
for member in @members.models
continue unless hero = member.get('heroConfig')?.thangType
for slug, original of ThangType.heroes when original is hero
@$el.find(".player-hero-icon[data-memberID=#{member.id}]").removeClass('.player-hero-icon').addClass('player-hero-icon ' + slug)
onCampaignSync: ->
return unless @campaigns.loaded
@campaignLevelProgressions = []
@conceptsProgression = []
@arenas = []
for campaign in @campaigns.models
continue if campaign.get('slug') is 'auditions'
campaignLevelProgression =
ID: campaign.id
slug: campaign.get('slug')
name: utils.i18n(campaign.attributes, 'fullName') or utils.i18n(campaign.attributes, 'name')
levels: []
for levelID, level of campaign.get('levels')
continue if level.slug is 'ace-of-coders' and new Date() < new Date(1441954800000)
campaignLevelProgression.levels.push
ID: levelID
slug: level.slug
name: utils.i18n level, 'name'
if level.concepts?
for concept in level.concepts
@conceptsProgression.push concept unless concept in @conceptsProgression
if level.type is 'hero-ladder'
@arenas.push level
@campaignLevelProgressions.push campaignLevelProgression
@render?()
onClanSync: ->
unless @owner?
@owner = new User _id: @clan.get('ownerID')
@listenTo @owner, 'sync', => @render?()
@supermodel.loadModel @owner, 'owner', cache: false
@render?()
onMembersSync: ->
@stats.averageLevel = Math.round(@members.reduce(((sum, member) -> sum + member.level()), 0) / @members.length)
@render?()
onMemberAchievementsSync: ->
@memberAchievementsMap = {}
for achievement in @memberAchievements.models
user = achievement.get('user')
@memberAchievementsMap[user] ?= []
@memberAchievementsMap[user].push achievement
for user of @memberAchievementsMap
@memberAchievementsMap[user].sort (a, b) -> b.id.localeCompare(a.id)
@stats.averageAchievements = Math.round(@memberAchievements.models.length / Object.keys(@memberAchievementsMap).length)
@render?()
onMemberSessionsSync: ->
@memberLevelMap = {}
memberSessions = {}
for levelSession in @memberSessions.models
continue if levelSession.isMultiplayer()
user = levelSession.get('creator')
levelSlug = levelSession.get('levelID')
@memberLevelMap[user] ?= {}
@memberLevelMap[user][levelSlug] ?= {}
levelInfo =
level: levelSession.get('levelName')
levelID: levelSession.get('levelID')
changed: new Date(levelSession.get('changed')).toLocaleString()
playtime: levelSession.get('playtime')
sessionID: levelSession.id
@memberLevelMap[user][levelSlug].levelInfo = levelInfo
if levelSession.get('state')?.complete is true
@memberLevelMap[user][levelSlug].state = 'complete'
memberSessions[user] ?= []
memberSessions[user].push levelSession
else
@memberLevelMap[user][levelSlug].state = 'started'
@memberMaxLevelCount = 0
@memberLanguageMap = {}
for user of memberSessions
languageCounts = {}
for levelSession in memberSessions[user]
language = levelSession.get('codeLanguage') or levelSession.get('submittedCodeLanguage')
languageCounts[language] = (languageCounts[language] or 0) + 1 if language
@memberMaxLevelCount = memberSessions[user].length if @memberMaxLevelCount < memberSessions[user].length
mostUsedCount = 0
for language, count of languageCounts
if count > mostUsedCount
mostUsedCount = count
@memberLanguageMap[user] = language
@render?()
onMouseEnterPoint: (e) ->
$('.level-popup-container').hide()
container = $(e.target).find('.level-popup-container').show()
margin = 20
offset = $(e.target).offset()
scrollTop = $(e.target).offsetParent().scrollTop()
height = container.outerHeight()
container.css('left', offset.left + e.offsetX)
container.css('top', offset.top + scrollTop - height - margin)
onMouseLeavePoint: (e) ->
$(e.target).find('.level-popup-container').hide()
onClickLevel: (e) ->
levelInfo = $(e.target).data 'level-info'
return unless levelInfo?.levelID? and levelInfo?.sessionID?
url = "/play/level/#{levelInfo.levelID}?session=#{levelInfo.sessionID}&observing=true"
window.open url, '_blank'
onDeleteClan: (e) ->
return @openModalView(new AuthModal()) if me.isAnonymous()
return unless window.confirm("Delete Clan?")
options =
url: "/db/clan/#{@clanID}"
method: 'DELETE'
error: (model, response, options) =>
console.error 'Error joining clan', response
success: (model, response, options) =>
app.router.navigate "/clans"
window.location.reload()
@supermodel.addRequestResource( 'delete_clan', options).load()
onEditDescriptionSave: (e) ->
description = $('.edit-description-input').val()
@clan.set 'description', description
@clan.patch()
$('#editDescriptionModal').modal('hide')
onEditNameSave: (e) ->
if name = $('.edit-name-input').val()
@clan.set 'name', name
@clan.patch()
$('#editNameModal').modal('hide')
onExpandedProgressCheckbox: (e) ->
@showExpandedProgress = $('.expand-progress-checkbox').prop('checked')
# TODO: why does render reset the checkbox to be unchecked?
@render?()
$('.expand-progress-checkbox').attr('checked', @showExpandedProgress)
onJoinClan: (e) ->
return @openModalView(new AuthModal()) if me.isAnonymous()
return unless @clan.loaded
if @clan.get('type') is 'private' and not me.isPremium()
@openModalView new SubscribeModal()
window.tracker?.trackEvent 'Show subscription modal', category: 'Subscription', label: 'join clan'
return
options =
url: "/db/clan/#{@clanID}/join"
method: 'PUT'
error: (model, response, options) =>
console.error 'Error joining clan', response
success: (model, response, options) => @refreshData()
@supermodel.addRequestResource( 'join_clan', options).load()
onLeaveClan: (e) ->
options =
url: "/db/clan/#{@clanID}/leave"
method: 'PUT'
error: (model, response, options) =>
console.error 'Error leaving clan', response
success: (model, response, options) => @refreshData()
@supermodel.addRequestResource( 'leave_clan', options).load()
onClickMemberHeader: (e) ->
@memberSort = if @memberSort is 'nameAsc' then 'nameDesc' else 'nameAsc'
@render?()
onClickProgressHeader: (e) ->
@memberSort = if @memberSort is 'progressAsc' then 'progressDesc' else 'progressAsc'
@render?()
onRemoveMember: (e) ->
return unless window.confirm("Remove Hero?")
if memberID = $(e.target).data('id')
options =
url: "/db/clan/#{@clanID}/remove/#{memberID}"
method: 'PUT'
error: (model, response, options) =>
console.error 'Error removing clan member', response
success: (model, response, options) => @refreshData()
@supermodel.addRequestResource( 'remove_member', options).load()
else
console.error "No member ID attached to remove button."