Add mod voting feature

This commit is contained in:
Andrew Miner
2016-06-01 12:49:29 -06:00
parent cb962f6175
commit d0a2292094
17 changed files with 590 additions and 61 deletions
+1 -1
View File
@@ -10,4 +10,4 @@
.right
.tile_container
.view__suggest_mod_panel
section.view__mod_ballot
@@ -5,22 +5,38 @@
# All rights reserved.
#
ModTileController = require './mod_tile/mod_tile_controller'
PageController = require '../page_controller'
SuggestModPanelController = require './suggest_mod_panel/suggest_mod_panel_controller'
ModBallotController = require './mod_ballot/mod_ballot_controller'
ModTileController = require './mod_tile/mod_tile_controller'
PageController = require '../page_controller'
{CraftingGuideClient} = require 'crafting-guide-common'
########################################################################################################################
module.exports = class BrowsePageController extends PageController
constructor: (options={})->
if not options.client then throw new Error 'options.client is required'
if not options.modPack then throw new Error 'options.modPack is required'
options.templateName = 'browse_page'
super options
@_client = options.client
@_modPack = options.modPack
@_tileControllers = []
@_client.on c.event.change, => @tryRefresh()
# Property Methods #############################################################################
Object.defineProperties @prototype,
user:
get: -> @_user
set: (newUser)->
@_user = newUser
@tryRefresh()
# PageController Overrides #####################################################################
getMetaDescription: ->
@@ -32,12 +48,14 @@ module.exports = class BrowsePageController extends PageController
# BaseController Overrides #####################################################################
onDidRender: ->
@_suggestController = @addChild SuggestModPanelController, '.view__suggest_mod_panel'
@_modBallotController = @addChild ModBallotController, '.view__mod_ballot', client:@_client, user:@_user
@$tileContainer = @$('.tile_container')
refresh: ->
@_modBallotController.user = @user
@_refreshModTiles()
@_refreshBallot()
super
# Backbone.View Overrides ######################################################################
@@ -48,6 +66,12 @@ module.exports = class BrowsePageController extends PageController
# Private Methods ##############################################################################
_refreshBallot: ->
if @_client.status is CraftingGuideClient.Status.Down
@_modBallotController.hide()
else
@_modBallotController.show()
_refreshModTiles: ->
index = 0
mods = @_modPack.getAllMods()
@@ -0,0 +1,29 @@
//-
//- Crafting Guide - mod_ballot.jade
//-
//- Copyright © 2014-2016 by Redwood Labs
//- All rights reserved.
//-
.view__mod_ballot
.status.initial
h2 Are we missing a mod you like?<br>Vote for your favorites!
.button.vote-now: .bezel: p Vote Now!
.status.loading
.status-message
.status.loaded
h2 Are we missing a mod you like?<br>Vote for your favorites!
.instructions.
You can cast up to 3 votes. Change them any time. You get a vote back when a mod is added.
.instructions.remaining.
You have <span class="remaining-count">0</span> votes remaining.
.ballot-list
.view__suggest_mod_panel
.status.error
.status-message
.button.reload: .bezel: p Try Again
@@ -0,0 +1,79 @@
//
// Crafting Guide - mod_ballot.scss
//
// Copyright © 2014-2016 by Redwood Labs
// All rights reserved.
//
$ballot-move-duration: 0.2s;
.view__mod_ballot {
@include floating-panel;
display : flex;
padding : $size-margin-medium;
.status {
display : flex;
flex-direction : column;
h2 {
margin-bottom: $size-margin-large;
text-align: center;
}
&.initial {
padding: 0 15%;
}
&.loaded {
padding: 0 15%;
& > .instructions {
margin-bottom: $size-margin-small;
font-family: $font-family-text;
font-size: $font-size-medium;
}
.remaining {
margin-top: $size-margin-medium;
font-size: $font-size-large;
text-align: center;
}
.ballot-list {
position: relative;
margin-top: $size-margin-large;
.view__mod_ballot_line {
position: absolute;
margin-bottom: $size-margin-small;
transition: top $ballot-move-duration ease-in-out;
}
}
.view__suggest_mod_panel {
margin-top: $size-margin-xlarge;
}
}
&.error {
display: flex;
flex-direction: column;
align-items: center;
.status-message {
margin-bottom: $size-margin-large;
font-family: $font-family-header;
font-size: $font-size-medium;
text-align: center;
}
.button {
width: $size-minecraft-block * 10;
}
}
}
}
@@ -0,0 +1,270 @@
#
# Crafting Guide - mod_ballot_controller.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
BaseController = require '../../base_controller'
ModBallotLineController = require './mod_ballot_line/mod_ballot_line_controller'
SuggestModPanelController = require './suggest_mod_panel/suggest_mod_panel_controller'
########################################################################################################################
module.exports = class ModBallotController extends BaseController
@::LOAD_ERROR = 'Sorry! Something went wrong loading the mod list. Please try again.'
@::MAX_VOTES = 3
@::STATUS =
ERROR: 'error'
INITIAL: 'initial'
LOADED: 'loaded'
LOADING: 'loading'
LOGGED_OUT: 'logged-out'
@::VOTE_ERROR = 'Sorry! Something went wrong casting your vote. Please try again.'
constructor: (options={})->
if not options.client then throw new Error "options.client is required"
options.model = ballot:null, votes:null
options.templateName = 'browse_page/mod_ballot'
super options
@_client = options.client
@_lineControllers = {}
@_status = @STATUS.INITIAL
# Property Methods #############################################################################
Object.defineProperties @prototype,
isVotingAllowed:
get: ->
voteCount = @model.votes?.length or 0
return voteCount < @MAX_VOTES
user:
get: -> @_user
set: (newUser)->
@_user = newUser
# Event Methods ################################################################################
onReloadClicked: ->
tracker.trackEvent c.tracking.category.modVote, 'reload'
@_reloadData()
return false
onVoteButtonClicked: (controller)->
modId = controller.model.ballotLine.modId
if controller.model.vote?
tracker.trackEvent c.tracking.category.modVote, 'cancel-vote', @_findModName modId
@_cancelVote modId
else
tracker.trackEvent c.tracking.category.modVote, 'cast-vote', @_findModName modId
@_castVote modId
onVoteNowClicked: ->
tracker.trackEvent c.tracking.category.modVote, 'vote-now'
if not @user?
global.site.login()
else
@_reloadData()
return false
# BaseController Overrides #####################################################################
onDidRender: ->
@_suggestPanelController = @addChild SuggestModPanelController, '.view__suggest_mod_panel'
@$ballotList = @$('.ballot-list')
@$remainingCount = @$('.remaining-count')
@$remainingMessage = @$('.remaining')
@$statusMessage = @$('.status-message')
@$statusSections = @$('.status')
super
refresh: ->
@_refreshLines()
@_refreshRemaining()
@_refreshStatus()
super
# Backbone.View Methods ########################################################################
events: ->
_.extend super,
'click .button.reload': 'onReloadClicked'
'click .button.vote-now': 'onVoteNowClicked'
# Private Methods ##############################################################################
_cancelVote: (modId)->
controller = @_lineControllers[modId]
vote = @_findVote modId
controller.isWaiting = true
controller.tryRefresh()
@_client.cancelVote modVoteId:vote.id
.then =>
@model.votes = _(@model.votes).without vote
controller.model.vote = null
controller.model.ballotLine.voteCount -= 1
.catch =>
@_changeStatus @STATUS.ERROR, @VOTE_ERROR
.finally =>
controller.isWaiting = false
@tryRefresh()
_castVote: (modId)->
controller = @_lineControllers[modId]
vote = @_findVote modId
controller.isWaiting = true
controller.tryRefresh()
@_client.castVote modId:modId, userId:@user.id
.then (response)=>
vote = response.json
@model.votes.push vote
controller.model.vote = vote
controller.model.ballotLine.voteCount += 1
.catch (error)=>
logger.error -> "vote could not be cast: #{error}"
@_changeStatus @STATUS.ERROR, @VOTE_ERROR
.finally =>
controller.isWaiting = false
@tryRefresh()
_computeLines: ->
return [] unless @model.ballot?.lines?
@model.ballot.lines.sort (a, b)->
if a.voteCount != b.voteCount
return if a.voteCount > b.voteCount then -1 else +1
if a.name != b.name
return if a.name < b.name then -1 else +1
return 0
return @model.ballot.lines
_changeStatus: (status, message='')->
@_status = status
@_message = message
_findVote: (modId)->
return null unless @model.votes?
for vote in @model.votes
return vote if vote.modId is modId
return null
_findModName: (modId)->
return '' unless @model.ballot?
for line in @model.ballot.lines
return line.name if line.modId is modId
return ''
_refreshLines: ->
extraControllers = _.clone @_lineControllers
lines = @_computeLines()
for line, index in lines
controller = @_lineControllers[line.modId]
delete extraControllers[line.modId]
model = {ballotLine:line, vote:@_findVote(line.modId)}
if not controller?
controller = new ModBallotLineController
model: model
onVoteButtonClicked: (controller)=> @onVoteButtonClicked controller
controller.render()
@$ballotList.append controller.$el
@_lineControllers[line.modId] = controller
else
controller.model = model
controller.isVotingAllowed = @isVotingAllowed
for modId, controller of extraControllers
delete @_lineControllers[modId]
controller.remove()
_.delay (=> @_refreshLineLayout()), 100
_refreshLineLayout: ->
lines = @_computeLines()
offset = @$ballotList.offset()
for line in lines
controller = @_lineControllers[line.modId]
continue unless controller?
if controller.$el.height() is 0
# keep trying until the elements have been created and laid out by the browser
_.delay (=> @_refreshLineLayout()), 100
return
controller.$el.offset offset
offset.top += controller.$el.outerHeight(true)
@$ballotList.height offset.top - @$ballotList.offset().top
_refreshRemaining: ->
if @model.votes?
@$remainingCount.html @MAX_VOTES - @model.votes.length
@show @$remainingMessage
else
@hide @$remainingMessage
_refreshStatus: ->
@hide @$('.status')
@show @$(".status.#{@_status}")
@$statusMessage.html @_message
_reloadBallot: ->
return @_loadingBallot if @_loadingBallot?
@model.ballot = null
@_changeStatus @STATUS.LOADING, "Loading ballot data..."
@tryRefresh()
@_loadingBallot = @_client.getModBallot()
.then (response)=>
@model.ballot = response.json
if not @user? or @model.votes? then @_changeStatus @STATUS.LOADED
@tryRefresh()
.catch (error)=>
@_changeStatus @STATUS.ERROR, @LOAD_ERROR
.finally =>
@_loadingBallot = null
_reloadData: ->
@_reloadVotes()
@_reloadBallot()
_reloadVotes: ->
return @_loadingVotes if @_loadingVotes?
return unless @user?
@model.votes = null
@_changeStatus @STATUS.LOADING, "Loading ballot data..."
@tryRefresh()
@_loadingVotes = @_client.getModVotes()
.then (response)=>
@model.votes = response.json
if @model.ballot? then @_changeStatus @STATUS.LOADED
@tryRefresh()
.catch (error)=>
@_changeStatus @STATUS.ERROR, @LOAD_ERROR
.finally =>
@_loadingVotes = null
@@ -0,0 +1,14 @@
//-
//- Crafting Guide - mod_ballot_line.jade
//-
//- Copyright © 2014-2016 by Redwood Labs
//- All rights reserved.
//-
.view__mod_ballot_line
.vote-count
img(src='/images/unknown.png')
.name
.button.vote: .bezel: p ✓
.wait
img(src='/images/wait-small.gif')
@@ -0,0 +1,53 @@
//
// Crafting Guide - mod_ballot_line.scss
//
// Copyright © 2014-2016 by Redwood Labs
// All rights reserved.
//
.view__mod_ballot_line {
width: 100%;
display : flex;
align-items : center;
.vote-count {
flex : 0 0 $size-minecraft-block;
margin-right : $size-margin-medium;
font-family : $font-family-text;
font-size : $font-size-large;
text-align : right;
}
& > img {
margin-right : $size-margin-medium;
flex : 0 0 auto;
height : $size-minecraft-block;
width : $size-minecraft-block;
}
.name {
margin-right : $size-margin-medium;
flex : 1 1 auto;
font-family : $font-family-text;
font-size : $font-size-large;
}
.button {
flex : 0 0 auto;
width : $size-minecraft-block * 1.5;
}
.wait {
flex : 0 0 auto;
height : $size-button-height;
width : $size-minecraft-block * 1.5;
display : flex;
align-items : center;
justify-content : center;
}
}
@@ -0,0 +1,86 @@
#
# Crafting Guide - mod_ballot_line_controller.coffee
#
# Copyright © 2014-2016 by Redwood Labs
# All rights reserved.
#
BaseController = require '../../../base_controller'
########################################################################################################################
module.exports = class ModBallotLineController extends BaseController
constructor: (options={})->
options.templateName = 'browse_page/mod_ballot/mod_ballot_line'
super options
@_onVoteButtonClicked = options.onVoteButtonClicked or (controller)-> # do nothing
# Event Methods ################################################################################
onButtonClicked: ->
return if @$button.hasClass 'disabled'
@_onVoteButtonClicked this
return false
# Property Methods #############################################################################
Object.defineProperties @prototype,
isVotingAllowed:
get: -> @_isVotingAllowed
set: (isVotingAllowed)->
@_isVotingAllowed = !! isVotingAllowed
@tryRefresh()
isWaiting:
get: -> @_isWaiting
set: (isWaiting)->
@_isWaiting = !! isWaiting
@tryRefresh()
# BaseController Overrides #####################################################################
onDidRender: ->
@$button = @$('.button')
@$image = @$('img')
@$name = @$('.name')
@$voteCount = @$('.vote-count')
@$wait = @$('.wait')
super
refresh: ->
@$name.html @model.ballotLine?.name or ''
@$voteCount.html @model.ballotLine?.voteCount or ''
@_refreshVoteButton()
super
# Backbone.View Overrides ######################################################################
events: ->
return _.extend super,
'click .button': 'onButtonClicked'
# Private Methods ##############################################################################
_refreshVoteButton: ->
if @isWaiting
@$wait.removeClass 'hidden'
@$button.addClass 'hidden'
else
@$wait.addClass 'hidden'
@$button.removeClass 'hidden'
if @model.vote?
@$button.addClass 'active'
else
@$button.removeClass 'active'
if @isVotingAllowed
@$button.removeClass 'disabled'
else
@$button.addClass 'disabled'
@@ -7,7 +7,7 @@
.view__suggest_mod_panel
.initial
.instructions Are we missing a mod you like? Tell us!
.instructions Still don't see the mod you want?<br>Tell us about it!
.entry-form
input.name(placeholder="enter the mod name")
@@ -6,30 +6,24 @@
//
.view__suggest_mod_panel {
@include floating-panel;
display: flex;
flex-direction: column;
align-items: center;
padding: $size-margin-large;
.instructions {
font-family: $font-family-header;
font-size: $font-size-xlarge;
text-align: center;
}
.initial {
display: flex;
flex-direction: column;
align-items: center;
.instructions {
margin-bottom: $size-margin-large;
}
.entry-form {
width: $size-minecraft-block * 15;
display: flex;
flex-direction: column;
@@ -68,36 +62,3 @@
margin: $size-margin-xlarge 0;
}
}
@include screen-size(mobile) {
.view__suggest_mod_panel {
.instructions {
}
.initial {
.instructions {
}
.entry-form {
width: 100%;
input {
&.invalid {
}
&.valid {
}
}
.buttons {
.button {
}
}
}
}
.error, .sending, .success {
}
}
}
@@ -5,15 +5,15 @@
# All rights reserved.
#
BaseController = require '../../base_controller'
EmailClient = require '../../../models/site/email_client'
BaseController = require '../../../base_controller'
EmailClient = require '../../../../models/site/email_client'
########################################################################################################################
module.exports = class SuggestModPanelController extends BaseController
constructor: (options={})->
options.templateName = 'browse_page/suggest_mod_panel'
options.templateName = 'browse_page/mod_ballot/suggest_mod_panel'
super options
@_emailClient = new EmailClient
@@ -38,6 +38,10 @@ module.exports = class SuggestModPanelController extends BaseController
return '' unless @rendered
return @$urlField.val()
user:
get: -> @_user
set: (user)-> @_user = user
# Event Methods ################################################################################
onButtonClicked: (event)->
@@ -96,6 +100,12 @@ module.exports = class SuggestModPanelController extends BaseController
@hide $panel for $panel in [@$initialPanel, @$sendingPanel, @$successPanel, @$errorPanel]
@show $selectedPanel
data =
name: @name
url: @url
user: @user?.name
email: @user?.email
showPanel @$sendingPanel
w(@_emailClient.send 'mod-suggestion', name:@name, url:@url)
.then =>