mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-19 15:48:04 +01:00
challenges: WIP beginning challenges feature. some basic CRUD & some
basic subscribe / unsubscribe functional (but buggy)
This commit is contained in:
@@ -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'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
242
public/js/controllers/challengesCtrl.js
Normal file
242
public/js/controllers/challengesCtrl.js
Normal 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');
|
||||||
|
}
|
||||||
|
|
||||||
|
}]);
|
||||||
25
public/js/services/challengeServices.js
Normal file
25
public/js/services/challengeServices.js
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
158
src/controllers/challenges.js
Normal file
158
src/controllers/challenges.js
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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
33
src/models/challenge.js
Normal 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);
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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');
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
102
views/options/challenges/index.jade
Normal file
102
views/options/challenges/index.jade
Normal 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;')
|
||||||
|
// | {/}
|
||||||
112
views/options/challenges/task.jade
Normal file
112
views/options/challenges/task.jade
Normal 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]')
|
||||||
21
views/options/challenges/tasks.jade
Normal file
21
views/options/challenges/tasks.jade
Normal 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
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user