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' # 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 .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 @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, 'clan', 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.members = @members?.models 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 lastUserCampaignLevelMap = {} maxLastUserCampaignLevel = 0 userConceptsMap = {} if @campaigns.loaded for campaign in @campaigns.models campaignID = campaign.id lastLevelIndex = 0 for levelID, level of campaign.get('levels') levelSlug = level.slug for member in context.members 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 lastLevelIndex++ context.lastUserCampaignLevelMap = lastUserCampaignLevelMap context.showExpandedProgress = maxLastUserCampaignLevel <= 30 or @showExpandedProgress context.userConceptsMap = userConceptsMap context afterRender: -> super() @updateHeroIcons() refreshData: -> me.fetch cache: false @members.fetch cache: false @memberAchievements.fetch cache: false @memberSessions.fetch cache: false 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 = [] for campaign in @campaigns.models continue if campaign.get('slug') is 'auditions' campaignLevelProgression = ID: campaign.id slug: campaign.get('slug') name: campaign.get('fullName') or campaign.get('name') levels: [] for levelID, level of campaign.get('levels') campaignLevelProgression.levels.push ID: levelID slug: level.slug name: level.name if level.concepts? for concept in level.concepts @conceptsProgression.push concept unless concept in @conceptsProgression @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() 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."