diff --git a/Gruntfile.js b/Gruntfile.js index 6c0d976cb7..d44646817f 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -49,6 +49,7 @@ module.exports = function(grunt) { 'public/js/services/groupServices.js', 'public/js/services/memberServices.js', 'public/js/services/guideServices.js', + 'public/js/services/challengeServices.js', 'public/js/filters/filters.js', @@ -61,14 +62,14 @@ module.exports = function(grunt) { 'public/js/controllers/settingsCtrl.js', 'public/js/controllers/statsCtrl.js', 'public/js/controllers/tasksCtrl.js', - 'public/js/controllers/taskDetailsCtrl.js', 'public/js/controllers/filtersCtrl.js', 'public/js/controllers/userCtrl.js', 'public/js/controllers/groupsCtrl.js', 'public/js/controllers/petsCtrl.js', 'public/js/controllers/inventoryCtrl.js', 'public/js/controllers/marketCtrl.js', - 'public/js/controllers/footerCtrl.js' + 'public/js/controllers/footerCtrl.js', + 'public/js/controllers/challengesCtrl.js' ] } }, diff --git a/package.json b/package.json index 847988171e..07a05f0925 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "0.0.0-152", "main": "./src/server.js", "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", "connect-mongo": "*", "passport-facebook": "~1.0.0", diff --git a/public/js/controllers/challengesCtrl.js b/public/js/controllers/challengesCtrl.js new file mode 100644 index 0000000000..4a882dd2f0 --- /dev/null +++ b/public/js/controllers/challengesCtrl.js @@ -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( + 'Remove Tasks
\nKeep Tasks
\nCancel
' + )($scope); + $scope.popoverEl.popover('destroy').popover({ + html: true, + placement: 'top', + trigger: 'manual', + title: 'Unsubscribe From Challenge And:', + content: html + }).popover('show'); + } + +}]); \ No newline at end of file diff --git a/public/js/services/challengeServices.js b/public/js/services/challengeServices.js new file mode 100644 index 0000000000..eaf4693845 --- /dev/null +++ b/public/js/services/challengeServices.js @@ -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 + } + } +]); diff --git a/src/controllers/challenges.js b/src/controllers/challenges.js new file mode 100644 index 0000000000..9ef575dfc4 --- /dev/null +++ b/src/controllers/challenges.js @@ -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); + }); +} \ No newline at end of file diff --git a/src/controllers/groups.js b/src/controllers/groups.js index a57268efff..cb36ffa7b0 100644 --- a/src/controllers/groups.js +++ b/src/controllers/groups.js @@ -41,6 +41,14 @@ api.getMember = function(req, res) { api.getGroups = function(req, res, next) { 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(','); // First get all groups diff --git a/src/models/challenge.js b/src/models/challenge.js new file mode 100644 index 0000000000..a74c25f39d --- /dev/null +++ b/src/models/challenge.js @@ -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); \ No newline at end of file diff --git a/src/models/group.js b/src/models/group.js index 194d164a68..b99c5870f8 100644 --- a/src/models/group.js +++ b/src/models/group.js @@ -51,13 +51,13 @@ var GroupSchema = new Schema({ balance: Number, logo: String, - leaderMessage: String + leaderMessage: String, + challenges: [{type:'String', ref:'Challenge'}] }, { - strict: 'throw', + strict: 'throw', 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 * to remove duplicates, then take these fucntions out diff --git a/src/routes/api.js b/src/routes/api.js index bd4c968ab4..7983757362 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -3,6 +3,7 @@ var router = new express.Router(); var user = require('../controllers/user'); var groups = require('../controllers/groups'); var auth = require('../controllers/auth'); +var challenges = require('../controllers/challenges'); /* ---------- /api/v1 API ------------ @@ -78,4 +79,13 @@ router.get('/members/:uid', groups.getMember); // Market 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; \ No newline at end of file diff --git a/src/server.js b/src/server.js index cb1a0c18f0..3e9c36ebc1 100644 --- a/src/server.js +++ b/src/server.js @@ -27,6 +27,7 @@ process.on("uncaughtException", function(error) { mongoose = require('mongoose'); require('./models/user'); //load up the user schema - TODO is this necessary? require('./models/group'); +require('./models/challenge'); mongoose.connect(nconf.get('NODE_DB_URI'), function(err) { if (err) throw err; console.info('Connected with Mongoose'); diff --git a/views/index.jade b/views/index.jade index 7e93871ae9..6de28f9472 100644 --- a/views/index.jade +++ b/views/index.jade @@ -30,14 +30,14 @@ html 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-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/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-resource/angular-resource.min.js') - script(type='text/javascript', src='/bower_components/angular-ui/build/angular-ui.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.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') // we'll remove this once angular-bootstrap is fixed 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/memberServices.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') @@ -78,6 +79,7 @@ html 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/footerCtrl.js') + script(type='text/javascript', src='/js/controllers/challengesCtrl.js') -} //webfonts diff --git a/views/options/challenges.html b/views/options/challenges.html index edff5ba0e0..23dd937e4a 100644 --- a/views/options/challenges.html +++ b/views/options/challenges.html @@ -1,10 +1,10 @@ - +
- <@headers> + - + {{#unless _page.party.id}} @@ -34,8 +34,9 @@ +
- +
@@ -46,8 +47,9 @@ {/}
+
- +
    @@ -128,8 +130,9 @@
+
- +
{@header}
{#each @list as :task} @@ -144,11 +147,13 @@ {/}
+
- + Create {{@text}} Challenge + - +
@@ -167,4 +172,5 @@ todos={_page.new.challenge.todos} rewards={_page.new.challenge.rewards} editable=true /> -
\ No newline at end of file + + \ No newline at end of file diff --git a/views/options/challenges/index.jade b/views/options/challenges/index.jade new file mode 100644 index 0000000000..5e63f02efd --- /dev/null +++ b/views/options/challenges/index.jade @@ -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') + // + 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;') +// | {/} diff --git a/views/options/challenges/task.jade b/views/options/challenges/task.jade new file mode 100644 index 0000000000..8f89fbcbb7 --- /dev/null +++ b/views/options/challenges/task.jade @@ -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]') diff --git a/views/options/challenges/tasks.jade b/views/options/challenges/tasks.jade new file mode 100644 index 0000000000..0b094e5645 --- /dev/null +++ b/views/options/challenges/tasks.jade @@ -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 \ No newline at end of file diff --git a/views/options/groups/group.jade b/views/options/groups/group.jade index 60e495dcf3..cc32f3b157 100644 --- a/views/options/groups/group.jade +++ b/views/options/groups/group.jade @@ -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.btn(type='submit', value='Invite') - - //.accordion-group - .accordion-heading - a.accordion-toggle(data-toggle='collapse', data-parent='#accordion-{{@group.id}}-parent', href='#accordion-{{@group.id}}-challenges') Challenges - #accordion-{{@group.id}}-challenges.accordion-body.collapse - .accordion-inner - span.label - i.icon-bullhorn - | Challenges - | coming soon! - a(target='_blank', href='https://trello.com/card/challenges-individual-party-guild-public/50e5d3684fe3a7266b0036d6/58') Details - // - // {#if @group.challenges} - // - // {#each @group.challenges as :challenge} - // - // {/} - //
- // {:challenge.name} - //
- // Visit the Challenges for more information. - // {else} - // No challenges yet, visit the Challenges tab to create one. - // {/} + .modal(style='position: relative;top: auto;left: auto;right: auto;margin: 0 auto 20px;z-index: 1;max-width: 100%;') + .modal-header + h3 Challenges + .modal-body + a(target='_blank', href='https://trello.com/card/challenges-individual-party-guild-public/50e5d3684fe3a7266b0036d6/58') Details + div(ng-show='group.challenges') + table.table.table-striped + tr(ng-repeat='challenge in group.challenges') + td + | {{challenge.name}} + p. + Visit the Challenges for more information. + div(ng-hid='group.challenges') + p. + No challenges yet, visit the Challenges tab to create one. a.btn.btn-danger(data-id='{{group.id}}', ng-click='leave(group)') Leave