challenges: WIP beginning challenges feature. some basic CRUD & some

basic subscribe / unsubscribe functional (but buggy)
This commit is contained in:
Tyler Renelle
2013-10-26 17:24:45 -07:00
parent e45d8307e7
commit fa25f3d300
16 changed files with 755 additions and 43 deletions

View File

@@ -49,6 +49,7 @@ module.exports = function(grunt) {
'public/js/services/groupServices.js', 'public/js/services/groupServices.js',
'public/js/services/memberServices.js', 'public/js/services/memberServices.js',
'public/js/services/guideServices.js', 'public/js/services/guideServices.js',
'public/js/services/challengeServices.js',
'public/js/filters/filters.js', 'public/js/filters/filters.js',
@@ -61,14 +62,14 @@ module.exports = function(grunt) {
'public/js/controllers/settingsCtrl.js', 'public/js/controllers/settingsCtrl.js',
'public/js/controllers/statsCtrl.js', 'public/js/controllers/statsCtrl.js',
'public/js/controllers/tasksCtrl.js', 'public/js/controllers/tasksCtrl.js',
'public/js/controllers/taskDetailsCtrl.js',
'public/js/controllers/filtersCtrl.js', 'public/js/controllers/filtersCtrl.js',
'public/js/controllers/userCtrl.js', 'public/js/controllers/userCtrl.js',
'public/js/controllers/groupsCtrl.js', 'public/js/controllers/groupsCtrl.js',
'public/js/controllers/petsCtrl.js', 'public/js/controllers/petsCtrl.js',
'public/js/controllers/inventoryCtrl.js', 'public/js/controllers/inventoryCtrl.js',
'public/js/controllers/marketCtrl.js', 'public/js/controllers/marketCtrl.js',
'public/js/controllers/footerCtrl.js' 'public/js/controllers/footerCtrl.js',
'public/js/controllers/challengesCtrl.js'
] ]
} }
}, },

View File

@@ -4,7 +4,7 @@
"version": "0.0.0-152", "version": "0.0.0-152",
"main": "./src/server.js", "main": "./src/server.js",
"dependencies": { "dependencies": {
"habitrpg-shared": "git://github.com/HabitRPG/habitrpg-shared#rewrite", "habitrpg-shared": "git://github.com/HabitRPG/habitrpg-shared#challenges",
"derby-auth": "git://github.com/lefnire/derby-auth#master", "derby-auth": "git://github.com/lefnire/derby-auth#master",
"connect-mongo": "*", "connect-mongo": "*",
"passport-facebook": "~1.0.0", "passport-facebook": "~1.0.0",

View File

@@ -0,0 +1,242 @@
"use strict";
habitrpg.controller("ChallengesCtrl", ['$scope', '$rootScope', 'User', 'Challenges', 'Notification', '$http', 'API_URL', '$compile',
function($scope, $rootScope, User, Challenges, Notification, $http, API_URL, $compile) {
$http.get(API_URL + '/api/v1/groups?minimal=true').success(function(groups){
$scope.groups = groups;
});
$scope.challenges = Challenges.Challenge.query();
//------------------------------------------------------------
// Challenge
//------------------------------------------------------------
/**
* Create
*/
$scope.create = function() {
$scope.newChallenge = new Challenges.Challenge({
name: '',
description: '',
habits: [],
dailys: [],
todos: [],
rewards: [],
leader: User.user._id,
group: null,
timestamp: +(new Date),
members: []
});
};
/**
* Save
*/
$scope.save = function(challenge) {
if (!challenge.group) return alert('Please select group');
var isNew = !challenge._id;
challenge.$save(function(){
if (isNew) {
Notification.text('Challenge Created');
$scope.discard();
Challenges.Challenge.query();
} else {
challenge._editing = false;
}
});
};
/**
* Discard
*/
$scope.discard = function() {
$scope.newChallenge = null;
};
/**
* Delete
*/
$scope["delete"] = function(challenge) {
if (confirm("Delete challenge, are you sure?") !== true) return;
challenge.delete();
};
//------------------------------------------------------------
// Tasks
//------------------------------------------------------------
$scope.addTask = function(list) {
var task = window.habitrpgShared.helpers.taskDefaults({text: list.newTask, type: list.type}, User.user.filters);
list.tasks.unshift(task);
//User.log({op: "addTask", data: task}); //TODO persist
delete list.newTask;
};
$scope.removeTask = function(list, $index) {
if (confirm("Are you sure you want to delete this task?")) return;
//TODO persist
// User.log({
// op: "delTask",
// data: task
//});
list.splice($index, 1);
};
$scope.saveTask = function(task){
task._editing = false;
// TODO persist
}
/**
* Render graphs for user scores when the "Challenges" tab is clicked
*/
//TODO
// 1. on main tab click or party
// * sort & render graphs for party
// 2. guild -> all guilds
// 3. public -> all public
// $('#profile-challenges-tab-link').on 'shown', ->
// async.each _.toArray(model.get('groups')), (g) ->
// async.each _.toArray(g.challenges), (chal) ->
// async.each _.toArray(chal.tasks), (task) ->
// async.each _.toArray(chal.members), (member) ->
// if (history = member?["#{task.type}s"]?[task.id]?.history) and !!history
// data = google.visualization.arrayToDataTable _.map(history, (h)-> [h.date,h.value])
// options =
// backgroundColor: { fill:'transparent' }
// width: 150
// height: 50
// chartArea: width: '80%', height: '80%'
// axisTitlePosition: 'none'
// legend: position: 'bottom'
// hAxis: gridlines: color: 'transparent' # since you can't seem to *remove* gridlines...
// vAxis: gridlines: color: 'transparent'
// chart = new google.visualization.LineChart $(".challenge-#{chal.id}-member-#{member.id}-history-#{task.id}")[0]
// chart.draw(data, options)
/**
* Sync user to challenge (when they score, add to statistics)
*/
// TODO this needs to be moved to the server. Either:
// 1. Calculate on load (simplest, but bad performance)
// 2. Updated from user score API
// app.model.on("change", "_page.user.priv.tasks.*.value", function(id, value, previous, passed) {
// /* Sync to challenge, but do it later*/
//
// var _this = this;
// return async.nextTick(function() {
// var chal, chalTask, chalUser, ctx, cu, model, pub, task, tobj;
// model = app.model;
// ctx = {
// model: model
// };
// task = model.at("_page.user.priv.tasks." + id);
// tobj = task.get();
// pub = model.get("_page.user.pub");
// if (((chalTask = helpers.taskInChallenge.call(ctx, tobj)) != null) && chalTask.get()) {
// chalTask.increment("value", value - previous);
// chal = model.at("groups." + tobj.group.id + ".challenges." + tobj.challenge);
// chalUser = function() {
// return helpers.indexedAt.call(ctx, chal.path(), 'members', {
// id: pub.id
// });
// };
// cu = chalUser();
// if (!(cu != null ? cu.get() : void 0)) {
// chal.push("members", {
// id: pub.id,
// name: model.get(pub.profile.name)
// });
// cu = model.at(chalUser());
// } else {
// cu.set('name', pub.profile.name);
// }
// return cu.set("" + tobj.type + "s." + tobj.id, {
// value: tobj.value,
// history: tobj.history
// });
// }
// });
// });
/*
--------------------------
Unsubscribe functions
--------------------------
*/
$scope.taskUnsubscribe = function(e, el) {
/*
since the challenge was deleted, we don't have its data to unsubscribe from - but we have the vestiges on the task
FIXME this is a really dumb way of doing this
*/
var deletedChal, i, path, tasks, tobj;
tasks = this.priv.get('tasks');
tobj = tasks[$(el).attr("data-tid")];
deletedChal = {
id: tobj.challenge,
members: [this.uid],
habits: _.where(tasks, {
type: 'habit',
challenge: tobj.challenge
}),
dailys: _.where(tasks, {
type: 'daily',
challenge: tobj.challenge
}),
todos: _.where(tasks, {
type: 'todo',
challenge: tobj.challenge
}),
rewards: _.where(tasks, {
type: 'reward',
challenge: tobj.challenge
})
};
switch ($(el).attr('data-action')) {
case 'keep':
this.priv.del("tasks." + tobj.id + ".challenge");
return this.priv.del("tasks." + tobj.id + ".group");
case 'keep-all':
return app.challenges.unsubscribe.call(this, deletedChal, true);
case 'remove':
path = "_page.lists.tasks." + this.uid + "." + tobj.type + "s";
if (~(i = _.findIndex(this.model.get(path), {
id: tobj.id
}))) {
return this.model.remove(path, i);
}
break;
case 'remove-all':
return app.challenges.unsubscribe.call(this, deletedChal, false);
}
};
$scope.unsubscribe = function(keep) {
if (keep == 'cancel') {
$scope.selectedChal = undefined;
} else {
$scope.selectedChal.$leave({keep:keep});
}
$scope.popoverEl.popover('destroy');
}
$scope.clickUnsubscribe = function(chal, $event) {
$scope.selectedChal = chal;
$scope.popoverEl = $($event.target);
var html = $compile(
'<a ng-controller="ChallengesCtrl" ng-click="unsubscribe(false)">Remove Tasks</a><br/>\n<a ng-click="unsubscribe(true)">Keep Tasks</a><br/>\n<a ng-click="unsubscribe(\'cancel\')">Cancel</a><br/>'
)($scope);
$scope.popoverEl.popover('destroy').popover({
html: true,
placement: 'top',
trigger: 'manual',
title: 'Unsubscribe From Challenge And:',
content: html
}).popover('show');
}
}]);

View File

@@ -0,0 +1,25 @@
'use strict';
/**
* Services that persists and retrieves user from localStorage.
*/
angular.module('challengeServices', ['ngResource']).
factory('Challenges', ['API_URL', '$resource', 'User', '$q', 'Members',
function(API_URL, $resource, User, $q, Members) {
var Challenge = $resource(API_URL + '/api/v1/challenges/:cid',
{cid:'@_id'},
{
//'query': {method: "GET", isArray:false}
join: {method: "POST", url: API_URL + '/api/v1/challenges/:cid/join'},
leave: {method: "POST", url: API_URL + '/api/v1/challenges/:cid/leave'}
});
//var challenges = [];
return {
Challenge: Challenge
//challenges: challenges
}
}
]);

View File

@@ -0,0 +1,158 @@
// @see ../routes for routing
var _ = require('lodash');
var nconf = require('nconf');
var async = require('async');
var algos = require('habitrpg-shared/script/algos');
var helpers = require('habitrpg-shared/script/helpers');
var items = require('habitrpg-shared/script/items');
var User = require('./../models/user').model;
var Group = require('./../models/group').model;
var Challenge = require('./../models/challenge').model;
var api = module.exports;
/*
------------------------------------------------------------------------
Challenges
------------------------------------------------------------------------
*/
// GET
api.get = function(req, res) {
// TODO only find in user's groups (+ public)
// TODO populate (group, leader, members)
Challenge.find({},function(err, challenges){
if(err) return res.json(500, {err:err});
res.json(challenges);
})
}
// CREATE
api.create = function(req, res){
// FIXME sanitize
var challenge = new Challenge(req.body);
challenge.save(function(err, saved){
// Need to create challenge with refs (group, leader)? Or is this taken care of automatically?
// @see http://mongoosejs.com/docs/populate.html
if (err) return res.json(500, {err:err});
res.json(saved);
});
}
// UPDATE
api.update = function(req, res){
//FIXME sanitize
Challenge.findByIdAndUpdate(req.params.cid, {$set:req.body}, function(err, saved){
if(err) res.json(500, {err:err});
res.json(saved);
// TODO update subscribed users' tasks, each user.__v++
})
}
// DELETE
// 1. update challenge
// 2. update sub'd users' tasks
/**
* Syncs all new tasks, deleted tasks, etc to the user object
* @param chal
* @param user
* @return nothing, user is modified directly. REMEMBER to save the user!
*/
var syncChalToUser = function(chal, user) {
if (!chal || !user) return;
// Sync tags
var tags = user.tags || [];
var i = _.findIndex(tags, {id: chal._id})
if (~i) {
if (tags[i].name !== chal.name) {
// update the name - it's been changed since
user.tags[i].name = chal.name;
}
} else {
user.tags.push({
id: chal._id,
name: chal.name,
challenge: true
});
}
tags = {};
tags[chal._id] = true;
_.each(['habits','dailys','todos','rewards'], function(type){
_.each(chal[type], function(task){
_.defaults(task, {
tags: tags,
challenge: chal._id,
group: chal.group
});
if (~(i = _.findIndex(user[type], {id:task.id}))) {
_.defaults(user[type][i], task);
} else {
user[type].push(task);
}
})
})
//FIXME account for deleted tasks (each users.tasks.broken = true)
};
api.join = function(req, res){
var user = res.locals.user;
var cid = req.params.cid;
async.waterfall([
function(cb){
Challenge.findByIdAndUpdate(cid, {$addToSet:{members:user._id}}, cb);
},
function(challenge, cb){
if (!~user.challenges.indexOf(cid))
user.challenges.unshift(cid);
// Add all challenge's tasks to user's tasks
syncChalToUser(challenge, user);
user.save(function(err){
if (err) return cb(err);
cb(null, challenge); // we want the saved challenge in the return results, due to ng-resource
});
}
], function(err, result){
if(err) return res.json(500,{err:err});
res.json(result);
});
}
api.leave = function(req, res, next){
var user = res.locals.user;
var cid = req.params.cid;
// whether or not to keep challenge's tasks. strictly default to true if "false" isn't provided
var keep = !(/^false$/i).test(req.query.keep);
async.waterfall([
function(cb){
Challenge.findByIdAndUpdate(cid, {$pull:{members:user._id}}, cb);
},
function(chal, cb){
// Remove challenge from user
//User.findByIdAndUpdate(user._id, {$pull:{challenges:cid}}, cb);
var i = user.challenges.indexOf(cid)
if (~i) user.challenges.splice(i,1);
// Remove tasks from user
_.each(chal.tasks, function(task) {
if (keep) {
delete user[task.type+'s'].id(task.id).challenge;
delete user[task.type+'s'].id(task.id).group;
} else {
user[task.type+'s'].id(task.id).remove();
}
});
user.save(function(err){
if (err) return cb(err);
cb(null, chal);
})
}
], function(err, result){
if(err) return res.json(500,{err:err});
res.json(result);
});
}

View File

@@ -41,6 +41,14 @@ api.getMember = function(req, res) {
api.getGroups = function(req, res, next) { api.getGroups = function(req, res, next) {
var user = res.locals.user; var user = res.locals.user;
// if ?minimal=true, just send down names
if (req.query.minimal) {
return Group.find({members: {'$in': [user._id]}}).select('name _id').exec(function(err, groups){
if (err) return res.json(500, {err:err});
res.json(groups);
});
}
var type = req.query.type && req.query.type.split(','); var type = req.query.type && req.query.type.split(',');
// First get all groups // First get all groups

33
src/models/challenge.js Normal file
View File

@@ -0,0 +1,33 @@
var mongoose = require("mongoose");
var Schema = mongoose.Schema;
var helpers = require('habitrpg-shared/script/helpers');
var _ = require('lodash');
var TaskSchema = require('./task').schema;
var ChallengeSchema = new Schema({
_id: {type: String, 'default': helpers.uuid},
name: String,
description: String,
habits: [TaskSchema],
dailys: [TaskSchema],
todos: [TaskSchema],
rewards: [TaskSchema],
leader: {type: String, ref: 'User'},
group: {type: String, ref: 'Group'},
// FIXME remove below, we don't need it since every time we load a challenge, we'll load it with the group ref. we don't need to look up challenges by type
//type: group.type, //type: {type: String,"enum": ['guild', 'party']},
//id: group._id
//},
timestamp: {type: Date, 'default': Date.now},
members: [{type: String, ref: 'User'}]
});
ChallengeSchema.virtual('tasks').get(function () {
var tasks = this.habits.concat(this.dailys).concat(this.todos).concat(this.rewards);
var tasks = _.object(_.pluck(tasks,'id'), tasks);
return tasks;
});
module.exports.schema = ChallengeSchema;
module.exports.model = mongoose.model("Challenge", ChallengeSchema);

View File

@@ -51,13 +51,13 @@ var GroupSchema = new Schema({
balance: Number, balance: Number,
logo: String, logo: String,
leaderMessage: String leaderMessage: String,
challenges: [{type:'String', ref:'Challenge'}]
}, { }, {
strict: 'throw', strict: 'throw',
minimize: false // So empty objects are returned minimize: false // So empty objects are returned
}); });
/** /**
* Derby duplicated stuff. This is a temporary solution, once we're completely off derby we'll run an mongo migration * Derby duplicated stuff. This is a temporary solution, once we're completely off derby we'll run an mongo migration
* to remove duplicates, then take these fucntions out * to remove duplicates, then take these fucntions out

View File

@@ -3,6 +3,7 @@ var router = new express.Router();
var user = require('../controllers/user'); var user = require('../controllers/user');
var groups = require('../controllers/groups'); var groups = require('../controllers/groups');
var auth = require('../controllers/auth'); var auth = require('../controllers/auth');
var challenges = require('../controllers/challenges');
/* /*
---------- /api/v1 API ------------ ---------- /api/v1 API ------------
@@ -78,4 +79,13 @@ router.get('/members/:uid', groups.getMember);
// Market // Market
router.post('/market/buy', auth.auth, user.marketBuy); router.post('/market/buy', auth.auth, user.marketBuy);
/* Challenges */
// Note: while challenges belong to groups, and would therefore make sense as a nested resource
// (eg /groups/:gid/challenges/:cid), they will also be referenced by users from the "challenges" tab
// without knowing which group they belong to. So to prevent unecessary lookups, we have them as a top-level resource
router.get('/challenges', auth.auth, challenges.get)
router.post('/challenges', auth.auth, challenges.create)
router.post('/challenges/:cid/join', auth.auth, challenges.join)
router.post('/challenges/:cid/leave', auth.auth, challenges.leave)
module.exports = router; module.exports = router;

View File

@@ -27,6 +27,7 @@ process.on("uncaughtException", function(error) {
mongoose = require('mongoose'); mongoose = require('mongoose');
require('./models/user'); //load up the user schema - TODO is this necessary? require('./models/user'); //load up the user schema - TODO is this necessary?
require('./models/group'); require('./models/group');
require('./models/challenge');
mongoose.connect(nconf.get('NODE_DB_URI'), function(err) { mongoose.connect(nconf.get('NODE_DB_URI'), function(err) {
if (err) throw err; if (err) throw err;
console.info('Connected with Mongoose'); console.info('Connected with Mongoose');

View File

@@ -30,14 +30,14 @@ html
script(type='text/javascript', src='/bower_components/jquery.cookie/jquery.cookie.js') script(type='text/javascript', src='/bower_components/jquery.cookie/jquery.cookie.js')
script(type='text/javascript', src='/bower_components/bootstrap-growl/jquery.bootstrap-growl.min.js') script(type='text/javascript', src='/bower_components/bootstrap-growl/jquery.bootstrap-growl.min.js')
script(type='text/javascript', src='/bower_components/bootstrap-tour/build/js/bootstrap-tour.min.js') script(type='text/javascript', src='/bower_components/bootstrap-tour/build/js/bootstrap-tour.min.js')
script(type='text/javascript', src='/bower_components/angular/angular.min.js') script(type='text/javascript', src='/bower_components/angular/angular.js')
script(type='text/javascript', src='/bower_components/angular-sanitize/angular-sanitize.min.js') script(type='text/javascript', src='/bower_components/angular-sanitize/angular-sanitize.min.js')
script(type='text/javascript', src='/bower_components/marked/lib/marked.js') script(type='text/javascript', src='/bower_components/marked/lib/marked.js')
script(type='text/javascript', src='/bower_components/angular-route/angular-route.min.js') script(type='text/javascript', src='/bower_components/angular-route/angular-route.js')
script(type='text/javascript', src='/bower_components/angular-resource/angular-resource.min.js') script(type='text/javascript', src='/bower_components/angular-resource/angular-resource.js')
script(type='text/javascript', src='/bower_components/angular-ui/build/angular-ui.min.js') script(type='text/javascript', src='/bower_components/angular-ui/build/angular-ui.js')
script(type='text/javascript', src='/bower_components/angular-ui-utils/modules/keypress/keypress.js') script(type='text/javascript', src='/bower_components/angular-ui-utils/modules/keypress/keypress.js')
// we'll remove this once angular-bootstrap is fixed // we'll remove this once angular-bootstrap is fixed
script(type='text/javascript', src='/bower_components/bootstrap/docs/assets/js/bootstrap.min.js') script(type='text/javascript', src='/bower_components/bootstrap/docs/assets/js/bootstrap.min.js')
@@ -59,6 +59,7 @@ html
script(type='text/javascript', src='/js/services/groupServices.js') script(type='text/javascript', src='/js/services/groupServices.js')
script(type='text/javascript', src='/js/services/memberServices.js') script(type='text/javascript', src='/js/services/memberServices.js')
script(type='text/javascript', src='/js/services/guideServices.js') script(type='text/javascript', src='/js/services/guideServices.js')
script(type='text/javascript', src='/js/services/challengeServices.js')
script(type='text/javascript', src='/js/filters/filters.js') script(type='text/javascript', src='/js/filters/filters.js')
@@ -78,6 +79,7 @@ html
script(type='text/javascript', src='/js/controllers/inventoryCtrl.js') script(type='text/javascript', src='/js/controllers/inventoryCtrl.js')
script(type='text/javascript', src='/js/controllers/marketCtrl.js') script(type='text/javascript', src='/js/controllers/marketCtrl.js')
script(type='text/javascript', src='/js/controllers/footerCtrl.js') script(type='text/javascript', src='/js/controllers/footerCtrl.js')
script(type='text/javascript', src='/js/controllers/challengesCtrl.js')
-} -}
//webfonts //webfonts

View File

@@ -1,10 +1,10 @@
<main:> <main>
<app:widgets:tabs> <app:widgets:tabs>
<@headers> <!--<@headers>-->
<app:widgets:tab-header group="challenges" tab="party" title="Party" default="true" /> <app:widgets:tab-header group="challenges" tab="party" title="Party" default="true" />
<app:widgets:tab-header group="challenges" tab="guild" title="Guild" /> <app:widgets:tab-header group="challenges" tab="guild" title="Guild" />
<app:widgets:tab-header group="challenges" tab="public" title="Public" /> <app:widgets:tab-header group="challenges" tab="public" title="Public" />
</@headers> <!--</@headers>-->
<app:widgets:tab-content group="challenges" tab="party" default="true"> <app:widgets:tab-content group="challenges" tab="party" default="true">
{{#unless _page.party.id}} {{#unless _page.party.id}}
@@ -34,8 +34,9 @@
</app:widgets:tab-content> </app:widgets:tab-content>
</app:widgets:tabs> </app:widgets:tabs>
</main>
<list:> <list>
<div class="{#unless _page.new.challenge}hidden{/}"> <div class="{#unless _page.new.challenge}hidden{/}">
<app:challenges:create-form /> <app:challenges:create-form />
</div> </div>
@@ -46,8 +47,9 @@
{/} {/}
<hr/> <hr/>
</div> </div>
</list>
<list-entry:> <list-entry>
<div class="accordion-group"> <div class="accordion-group">
<div class="accordion-heading"> <div class="accordion-heading">
<ul class='pull-right challenge-accordion-header-specs'> <ul class='pull-right challenge-accordion-header-specs'>
@@ -128,8 +130,9 @@
</div> </div>
</div> </div>
</div> </div>
</list-entry>
<stats:> <stats>
<h5>{@header}</h5> <h5>{@header}</h5>
<div> <div>
{#each @list as :task} {#each @list as :task}
@@ -144,11 +147,13 @@
</tr></table> </tr></table>
{/} {/}
</div> </div>
</stats>
<create-button:> <create-button>
<a x-bind='click:challenges.create' class='btn btn-success' data-type={{@type}} data-gid={{@gid}} >Create {{@text}} Challenge</a> <a x-bind='click:challenges.create' class='btn btn-success' data-type={{@type}} data-gid={{@gid}} >Create {{@text}} Challenge</a>
</create-button>
<create-form:> <create-form>
<form x-bind="submit:challenges.save"> <form x-bind="submit:challenges.save">
<div> <div>
<input type='submit' class='btn btn-success' value='Save' /> <input type='submit' class='btn btn-success' value='Save' />
@@ -167,4 +172,5 @@
todos={_page.new.challenge.todos} todos={_page.new.challenge.todos}
rewards={_page.new.challenge.rewards} rewards={_page.new.challenge.rewards}
editable=true /> editable=true />
</div> </div>
</create-form>

View File

@@ -0,0 +1,102 @@
.row-fluid(ng-controller='ChallengesCtrl')
.span2.well
h4 Filters
ul
li
input(type='checkbox', checked='checked')
.label.label-warning todo
| Party
li
input(type='checkbox', checked='checked')
.label.label-warning todo
| (list groups)
li
input(type='checkbox', checked='checked')
.label.label-warning todo
| Subscribed
li
input(type='checkbox', checked='checked')
.label.label-warning todo
| Available
.span10
// Creation form
a.btn.btn-success(ng-click='create()') Create Challenge
.create-challenge-from(ng-if='newChallenge')
form(ng-submit='save(newChallenge)')
div
input.btn.btn-success(type='submit', value='Save')
input.btn.btn-danger(type='button', ng-click='discard()', value='Discard')
select(ng-model='newChallenge.group', ng-required='required', name='Group', ng-options='g._id as g.name for g in groups')
.challenge-options
input.option-content(type='text', ng-model='newChallenge.name', placeholder='Challenge Title', required='required')
habitrpg-tasks(main=false, obj='newChallenge')
// Challenges list
.accordion-group(ng-repeat='challenge in challenges')
.accordion-heading
ul.pull-right.challenge-accordion-header-specs
li {{challenge.members.length}} Subscribers
li(ng-show='challenge.prize')
// prize
table(ng-show='challenge.prize')
tr
td {{challenge.prize}}
td
span.Pet_Currency_Gem1x
td Prize
li
// subscribe / unsubscribe
a.btn.btn-small.btn-danger(ng-show='indexOf(challenge.members, user._id)', ng-click='clickUnsubscribe(challenge, $event)')
i.icon-ban-circle
| Unsubscribe
a.btn.btn-small.btn-success(ng-hide='indexOf(challenge.members, user._id)', ng-click='challenge.$join()')
i.icon-ok
| Subscribe
a.accordion-toggle(data-toggle='collapse', data-target='#accordion-challenge-{{challenge._id}}') {{challenge.name}} (by {{challenge.leader.name}})
.accordion-body.collapse(id='accordion-challenge-{{challenge._id}}')
.accordion-inner
// Edit button
span(style='position: absolute; right: 0;')
ul.nav.nav-pills(ng-show='challenge.leader==user._id && !challenge._editing')
li
a(ng-click='challenge._editing = true')
i.icon-pencil
ul.nav.nav-pills(ng-show='challenge._editing')
li
a(ng-click='save(challenge)')
i.icon-ok
div(ng-show='challenge._editing')
.-options
input.option-content(type='text', ng-model='challenge.name')
textarea.option-content(cols='3', placeholder='Description', ng-model='challenge.description')
// <input type=number class='option-content' placeholder='Gems Prize' value={@challenge.prize} />
a.btn.btn-small.btn-danger(ng-click='delete(challenge)') Delete
div(ng-if='challenge.description') {{challenge.description}}
habitrpg-tasks(obj='challenge', main=false)
h3 Statistics
div(ng-repeat='member in challenge.members')
h4 {{member.name}}
.grid
.module
app:challenges:stats(header='Habits', challenge='{{challenge}}', member='{{member}}', list='{{challenge.habits}}')
.module
app:challenges:stats(header='Dailies', challenge='{{challenge}}', member='{{member}}', list='{{challenge.dailys}}')
.module
app:challenges:stats(header='Todos', challenge='{{challenge}}', member='{{member}}', list='{{challenge.todos}}')
//// .stats
// h5 {@header}
// div
// | {#each @list as :task}
// table
// tr
// td
// //
// FIXME commented section below isn't getting updated dynamically, temp solution is less efficient
// strong {:task.text}
// | : {challengeMemberScore(@member,:task)}
// // {round(@member[@taskType]s[:task.id].value)}
// td
// .challenge-{{@challenge.id}}-member-{{@member.id}}-history-{{:task.id}}(style='margin-left: 10px;')
// | {/}

View File

@@ -0,0 +1,112 @@
//-
NOTE: This file is a copy of /views/tasks/task.jade, make sure to keep them in sync!
We've cloned it because they differ widely enough that if(challenge)/else checks got out of
hand, and we needed separate controllers.
li(ng-repeat='task in list.tasks', class='task {{taskClasses(task)}}', data-id='{{task.id}}')
.task-meta-controls
// TODO Do we want to show participants' streaks?
//- Streak
span(ng-show='task.streak') {{task.streak}}
span(tooltip='Streak Counter')
i.icon-forward
// edit
a(ng-hide='task._editing', ng-click='toggleEdit(task)', tooltip='Edit')
i.icon-pencil(ng-hide='task._editing')
// cancel
a(ng-hide='!task._editing', ng-click='toggleEdit(task)', tooltip='Cancel')
i.icon-remove(ng-hide='!task._editing')
// delete
a(ng-click='remove(task)', tooltip='Delete')
i.icon-trash
// chart
a(ng-show='task.history', ng-click='toggleChart(task.id, task)', tooltip='Progress')
i.icon-signal
// notes
span.task-notes(ng-show='task.notes && !task._editing', popover-trigger='mouseenter', popover-placement='left', popover='{{task.notes}}', popover-title='{{task.text}}')
i.icon-comment
// left-hand side checkbox
.task-controls.task-primary
// Habits
span(ng-if='task.type=="habit"')
a.task-action-btn(ng-if='task.up') +
a.task-action-btn(ng-if='task.down') -
// Rewards
span(ng-show='task.type=="reward"')
a.money.btn-buy
span.reward-cost {{task.value}}
span.shop_gold
// Daily & Todos
span.task-checker.action-yesno(ng-if='task.type=="daily" || task.type=="todo"')
input.visuallyhidden.focusable(id='box-{{task.id}}-static', type='checkbox', ng-model='task.completed')
label(for='box-{{task.id}}-static')
// main content
p.task-text
| {{task.text}}
// edit/options dialog
.task-options(ng-show='task._editing')
form(ng-submit='saveTask(task)')
// text & notes
fieldset.option-group
label.option-title Text
input.option-content(type='text', ng-model='task.text', required)
label.option-title Extra Notes
textarea.option-content(rows='3', ng-model='task.notes')
// if Habit, plus/minus command options
fieldset.option-group(ng-if='task.type=="habit"')
legend.option-title Direction/Actions
span.task-checker.action-plusminus.select-toggle
input.visuallyhidden.focusable(id='{{task.id}}-option-plus', type='checkbox', ng-model='task.up')
label(for='{{task.id}}-option-plus')
span.task-checker.action-plusminus.select-toggle
input.visuallyhidden.focusable(id='{{task.id}}-option-minus', type='checkbox', ng-model='task.down')
label(for='{{task.id}}-option-minus')
// if Daily, calendar
fieldset(ng-if='task.type=="daily"', class="option-group")
legend.option-title Repeat
.task-controls.tile-group.repeat-days
// note, does not use data-toggle="buttons-checkbox" - it would interfere with our own click binding
button.task-action-btn.tile(ng-class='{active: task.repeat.su}', type='button', data-day='su', ng-click='task.repeat.su = !task.repeat.su') Su
button.task-action-btn.tile(ng-class='{active: task.repeat.m}', type='button', data-day='m', ng-click='task.repeat.m = !task.repeat.m') M
button.task-action-btn.tile(ng-class='{active: task.repeat.t}', type='button', data-day='t', ng-click='task.repeat.t = !task.repeat.t') T
button.task-action-btn.tile(ng-class='{active: task.repeat.w}', type='button', data-day='w', ng-click='task.repeat.w = !task.repeat.w') W
button.task-action-btn.tile(ng-class='{active: task.repeat.th}', type='button', data-day='th', ng-click='task.repeat.th = !task.repeat.th') Th
button.task-action-btn.tile(ng-class='{active: task.repeat.f}', type='button', data-day='f', ng-click='task.repeat.f = !task.repeat.f') F
button.task-action-btn.tile(ng-class='{active: task.repeat.s}', type='button', data-day='s', ng-click='task.repeat.s = !task.repeat.s') S
// if Reward, pricing
fieldset.option-group.option-short(ng-if='task.type=="reward"')
legend.option-title Price
input.option-content(type='number', size='16', min='0', step="any", ng-model='task.value')
.money.input-suffix
span.shop_gold
// if Todos, the due date
fieldset.option-group(ng-if='task.type=="todo"')
legend.option-title Due Date
input.option-content.datepicker(type='text', ng-model='task.date', data-date-format='mm/dd/yyyy')
// Advanced Options
span(ng-if='task.type!="reward"')
p.option-title.mega(ng-click='task._advanced = !task._advanced') Advanced Options
fieldset.option-group.advanced-option(ng-class="{visuallyhidden: !task._advanced}")
legend.option-title
a.priority-multiplier-help(href='https://trello.com/card/priority-multiplier/50e5d3684fe3a7266b0036d6/17', target='_blank', popover-title='How difficult is this task?', popover-trigger='mouseenter', popover="This multiplies its point value. Use sparingly, rely instead on our organic value-adjustment algorithms. But some tasks are grossly more valuable (Write Thesis vs Floss Teeth). Click for more info.")
i.icon-question-sign
| Difficulty
.task-controls.tile-group.priority-multiplier(data-id='{{task.id}}')
button.task-action-btn.tile(type='button', ng-class='{active: task.priority=="!" || !task.priority}', ng-click='task.priority="!"') Easy
button.task-action-btn.tile(type='button', ng-class='{active: task.priority=="!!"}', ng-click='task.priority="!!"') Medium
button.task-action-btn.tile(type='button', ng-class='{active: task.priority=="!!!"}', ng-click='task.priority="!!!"') Hard
button.task-action-btn.tile.spacious(type='submit') Save & Close
div(class='{{task.id}}-chart', ng-show='charts[task.id]')

View File

@@ -0,0 +1,21 @@
.grid
.module(ng-repeat='list in taskLists', ng-class='{"rewards-module": list.type==="reward"}')
.task-column(class='{{list.type}}s')
// Removed graph icon. Restore here from tasks/index.jade if needed
// Header
h2.task-column_title {{list.header}}
// Removed graph. Restore here from tasks/index.jade if needed
// Add New
form.addtask-form.form-inline.new-task-form(name='new{{list.type}}form', ng-if='challenge.leader == user._id', ng-submit='addTask(list)')
span.addtask-field
input(type='text', ng-model='list.newTask', placeholder='{{list.placeHolder}}', required)
input.addtask-btn(type='submit', value='', ng-disabled='new{{list.type}}form.$invalid')
hr
// Actual List
ul(class='{{list.type}}s', ng-show='list.tasks.length > 0', habitrpg-sortable)
include ./task

View File

@@ -77,30 +77,21 @@ a.pull-right.gem-wallet(popover-trigger='mouseenter', popover-title='Guild Bank'
input.option-content(type='text', placeholder='User Id', ng-model='invitee') input.option-content(type='text', placeholder='User Id', ng-model='invitee')
input.btn(type='submit', value='Invite') input.btn(type='submit', value='Invite')
.modal(style='position: relative;top: auto;left: auto;right: auto;margin: 0 auto 20px;z-index: 1;max-width: 100%;')
//.accordion-group .modal-header
.accordion-heading h3 Challenges
a.accordion-toggle(data-toggle='collapse', data-parent='#accordion-{{@group.id}}-parent', href='#accordion-{{@group.id}}-challenges') Challenges .modal-body
#accordion-{{@group.id}}-challenges.accordion-body.collapse a(target='_blank', href='https://trello.com/card/challenges-individual-party-guild-public/50e5d3684fe3a7266b0036d6/58') Details
.accordion-inner div(ng-show='group.challenges')
span.label table.table.table-striped
i.icon-bullhorn tr(ng-repeat='challenge in group.challenges')
| Challenges td
| coming soon! | {{challenge.name}}
a(target='_blank', href='https://trello.com/card/challenges-individual-party-guild-public/50e5d3684fe3a7266b0036d6/58') Details p.
// Visit the <span class=label><i class=icon-bullhorn></i> Challenges</span> for more information.
// {#if @group.challenges} div(ng-hid='group.challenges')
// <table class="table table-striped"> p.
// {#each @group.challenges as :challenge} No challenges yet, visit the <span class=label><i class=icon-bullhorn></i> Challenges</span> tab to create one.
// <tr><td>
// {:challenge.name}
// </td></tr>
// {/}
// </table>
// Visit the <span class=label><i class=icon-bullhorn></i> Challenges</span> for more information.
// {else}
// No challenges yet, visit the <span class=label><i class=icon-bullhorn></i> Challenges</span> tab to create one.
// {/}
a.btn.btn-danger(data-id='{{group.id}}', ng-click='leave(group)') Leave a.btn.btn-danger(data-id='{{group.id}}', ng-click='leave(group)') Leave