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
+2 -2
View File
@@ -188,7 +188,7 @@ module.exports = (grunt)->
tasks: ['sass']
test:
files: ['./src/**/*.coffee', './src/**/*.js', './test/**/*.coffee']
tasks: ['script:clear', 'test']
tasks: ['test']
# Compound Tasks ###################################################################################################
@@ -223,7 +223,7 @@ module.exports = (grunt)->
['prepublish', 'script:publish', 'clean', 'build']
grunt.registerTask 'start', 'build the project and start a local HTTP server',
['build', 'script:start']
['script:start']
grunt.registerTask 'test', 'run unit tests',
['mochaTest']
Binary file not shown.

After

Width:  |  Height:  |  Size: 723 B

+2
View File
@@ -1,5 +1,7 @@
#!/bin/bash
grunt watch &
pushd './build/' &>/dev/null
clear
nodemon ./server.coffee $*
+3 -3
View File
@@ -37,11 +37,11 @@ switch global.hostName
when 'prerender.crafting-guide.com'
global.env = 'prerender'
logger.level = Logger.FATAL
apiBaseUrl = 'http://local.crafting-guide.com:4347'
apiBaseUrl = 'http://prerender.crafting-guide.com:4347'
when 'local.crafting-guide.com', 'localhost'
global.env = 'local'
logger.level = Logger.DEBUG
apiBaseUrl = 'http://local.crafting-guide.com:4347'
apiBaseUrl = "http://#{global.hostName}:4347"
when 'staging.crafting-guide.com'
global.env = 'staging'
logger.level = Logger.VERBOSE
@@ -70,7 +70,7 @@ client.onStatusChanged = (client, oldStatus, newStatus)->
client.trigger 'change:status', client, oldStatus, newStatus
client.trigger 'change', client
client.checkStatus()
client.startMonitoringStatus()
########################################################################################################################
+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.
#
ModBallotController = require './mod_ballot/mod_ballot_controller'
ModTileController = require './mod_tile/mod_tile_controller'
PageController = require '../page_controller'
SuggestModPanelController = require './suggest_mod_panel/suggest_mod_panel_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 =>
+7 -7
View File
@@ -18,11 +18,11 @@
p.
All changes you make on the site get submitted directly to our GitHub repo, and then published
through the same automated deployment process as changes made by anyone else. That way, anyone
can contribute easily: whether developer or end user.
can contribute easily: whether programmer or player.
p.
To log in, we redirect you to GitHub's site, and once you're signed in, they'll send you back
here. We never see your credentials, and only get the minimum set of permissions necessary to
submit changes to the Crafting Guide repo on your behalf.
To log in, we redirect you to GitHub's site. Once you're signed in over there, they'll send you
back here. We never see your credentials, and only get the minimum set of permissions necessary
to submit changes to the Crafting Guide repo on your behalf.
p.
GitHub offers free accounts, so even if you don't already have one, you've got nothing to lose!
@@ -75,6 +75,6 @@
.left
h1 Sorry!
p.
Our interactive editing service is down right now, but you can always submit a pull request over at the
<a href="https://github.com/andrewminer/crafting-guide">Crafting Guide GitHub Repository</a>. We'll
have the editing service back up soon!
Our interactive services are down right now, but you can always submit an issue or pull request over at
the <a href="https://github.com/andrewminer/crafting-guide">Crafting Guide GitHub Repository</a>. We'll
have things back to normal soon!
+1
View File
@@ -166,6 +166,7 @@ tracking.category.craftWant = 'craft-want'
tracking.category.feedback = 'feedback'
tracking.category.markdown = 'markdown'
tracking.category.modPack = 'mod-pack'
tracking.category.modVote = 'mod-vote'
tracking.category.multiblock = 'multiblock'
tracking.category.navigate = 'navigate'
tracking.category.search = 'search'