diff --git a/bower.json b/bower.json
index 61b7ccc74e..148ffa0a14 100644
--- a/bower.json
+++ b/bower.json
@@ -33,7 +33,8 @@
"BrowserQuest": "https://github.com/mozilla/BrowserQuest.git",
"github-buttons": "git://github.com/mdo/github-buttons.git",
"marked": "~0.2.9",
- "JavaScriptButtons": "git://github.com/paypal/JavaScriptButtons.git#master"
+ "JavaScriptButtons": "git://github.com/paypal/JavaScriptButtons.git#master",
+ "Angular-At-Directive": "git://github.com/scarletsky/Angular-At-Directive.git#master"
},
"resolutions": {
"jquery": "~2.0.3",
diff --git a/migrations/20131111_task_NaN.js b/migrations/20131111_task_NaN.js
new file mode 100644
index 0000000000..16e6788003
--- /dev/null
+++ b/migrations/20131111_task_NaN.js
@@ -0,0 +1,15 @@
+// This migration has already been run in the past. It's vital to fix these users presently, but we need to find
+// out why task values are ever getting in as NaN. My guess is API PUT /tasks/:tid routes
+db.users.find({},{habits:1,dailys:1,todos:1,rewards:1}).forEach(function(user){
+ _.each(['habits','dailys','todos','rewards'], function(type){
+ _.each(user[type], function(task){
+ task.value = +task.value;
+ if (_.isNaN(task.value)) {
+ task.value = 0;
+ print(user._id);
+ }
+ })
+ })
+
+ db.users.update({_id:user._id}, {$set:{habits: user.habits, dailys: user.dailys, todos: user.todos, rewards: user.rewards}});
+});
\ No newline at end of file
diff --git a/public/css/customizer.styl b/public/css/customizer.styl
index e61402f24b..fd9391fa9d 100644
--- a/public/css/customizer.styl
+++ b/public/css/customizer.styl
@@ -54,4 +54,4 @@ menu
.well.limited-edition
padding: 5px
- margin: 0px
\ No newline at end of file
+ margin: 25px 0 0
\ No newline at end of file
diff --git a/public/css/game-pane.styl b/public/css/game-pane.styl
index 086e922734..67f33b6d8f 100644
--- a/public/css/game-pane.styl
+++ b/public/css/game-pane.styl
@@ -57,4 +57,38 @@
margin: 5px
.option-group .option-time
- padding: 0px 5px
\ No newline at end of file
+ padding: 0px 5px
+
+// Autocomplete [TODO] make this nicer
+.list-at-user {
+ width: 100%;
+ max-width: 120px;
+ position: absolute;
+ background: white;
+ border: 1px solid #a4a4a4;
+ z-index: 10;
+ margin-left: -435px;
+ margin-top: -355px;
+}
+
+.list-at-user li {
+ line-height:14px;
+ border-bottom: 1px solid #a4a4a4;
+ list-style-type: none;
+}
+
+.list-at-user li:hover {
+ cursor: pointer;
+ background: #b9dff4;
+}
+
+.list-at-user li span {
+ margin: 2px;
+ font-size: 11.844px;
+ margin-left: 5px;
+ display: inline-block;
+}
+
+.list-cur {
+ background: #b9dff4;
+}
\ No newline at end of file
diff --git a/public/js/app.js b/public/js/app.js
index cdb890c820..261931989f 100644
--- a/public/js/app.js
+++ b/public/js/app.js
@@ -3,7 +3,7 @@
window.habitrpg = angular.module('habitrpg',
['ngResource', 'ngSanitize', 'userServices', 'groupServices', 'memberServices', 'challengeServices',
'sharedServices', 'authServices', 'notificationServices', 'guideServices',
- 'ui.bootstrap', 'ui.keypress', 'ui.router', 'chieffancypants.loadingBar'])
+ 'ui.bootstrap', 'ui.keypress', 'ui.router', 'chieffancypants.loadingBar', 'At'])
// @see https://github.com/angular-ui/ui-router/issues/110 and https://github.com/HabitRPG/habitrpg/issues/1705
// temporary hack until they have a better solution
diff --git a/public/js/controllers/challengesCtrl.js b/public/js/controllers/challengesCtrl.js
index 96f61acf77..fa719270b7 100644
--- a/public/js/controllers/challengesCtrl.js
+++ b/public/js/controllers/challengesCtrl.js
@@ -7,34 +7,13 @@ habitrpg.controller("ChallengesCtrl", ['$scope', 'User', 'Challenges', 'Notifica
Groups.Group.query({type:'party,guilds,tavern'}, function(groups){
$scope.groups = groups;
$scope.search = {
- group: _.reduce(groups, function(m,g){m[g._id]=true;return m;}, {})
+ group: _.transform(groups, function(m,g){m[g._id]=true;})
};
});
// FIXME $scope.challenges needs to be resolved first (see app.js)
$scope.challenges = Challenges.Challenge.query();
// we should fix this, that's pretty brittle
-// $scope.$watch('search', function(search){
-// if (!search) $scope.filteredChallenges = $scope.challenges;
-// $scope.filteredChallenges = $filter('filter')($scope.challenges, function(chal) {
-// return (search.group[chal.group._id] &&
-// (typeof search._isMember == 'undefined' || search._isMember == chal._isMember));
-// })
-// })
- // TODO probably better to use $watch above, to avoid this being calculated on every digest cycle
- $scope.filterChallenges = function(chal){
- return (!$scope.search) ? true :
- ($scope.search.group[chal.group._id] &&
- (typeof $scope.search._isMember == 'undefined' || $scope.search._isMember == chal._isMember));
- }
-
- $scope.$watch('newChallenge.group', function(gid){
- if (!gid) return;
- var group = _.find($scope.groups, {_id:gid});
- $scope.maxPrize = User.user.balance*4 + ((group && group.balance && group.leader==User.user._id) ? group.balance*4 : 0);
- if (gid == 'habitrpg') $scope.newChallenge.prize = 1;
- })
-
// override score() for tasks listed in challenges-editing pages, so that nothing happens
$scope.score = function(){}
@@ -216,4 +195,37 @@ habitrpg.controller("ChallengesCtrl", ['$scope', 'User', 'Challenges', 'Notifica
}).popover('show');
}
+ //------------------------------------------------------------
+ // Filtering
+ //------------------------------------------------------------
+
+// $scope.$watch('search', function(search){
+// if (!search) $scope.filteredChallenges = $scope.challenges;
+// $scope.filteredChallenges = $filter('filter')($scope.challenges, function(chal) {
+// return (search.group[chal.group._id] &&
+// (typeof search._isMember == 'undefined' || search._isMember == chal._isMember));
+// })
+// })
+ // TODO probably better to use $watch above, to avoid this being calculated on every digest cycle
+ $scope.filterChallenges = function(chal){
+ return (!$scope.search) ? true :
+ ($scope.search.group[chal.group._id] &&
+ (typeof $scope.search._isMember == 'undefined' || $scope.search._isMember == chal._isMember));
+ }
+
+ $scope.$watch('newChallenge.group', function(gid){
+ if (!gid) return;
+ var group = _.find($scope.groups, {_id:gid});
+ $scope.maxPrize = User.user.balance*4 + ((group && group.balance && group.leader==User.user._id) ? group.balance*4 : 0);
+ if (gid == 'habitrpg') $scope.newChallenge.prize = 1;
+ })
+
+ $scope.selectAll = function(){
+ $scope.search.group = _.transform($scope.groups, function(m,g){m[g._id] = true});
+ }
+
+ $scope.selectNone = function(){
+ $scope.search.group = _.transform($scope.groups, function(m,g){m[g._id] = false});
+ }
+
}]);
\ No newline at end of file
diff --git a/public/js/controllers/groupsCtrl.js b/public/js/controllers/groupsCtrl.js
index 5f6bd6e115..4c8e41685e 100644
--- a/public/js/controllers/groupsCtrl.js
+++ b/public/js/controllers/groupsCtrl.js
@@ -79,6 +79,32 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Groups', '$http', 'A
}
])
+ .controller('AutocompleteCtrl', ['$scope', 'Groups', 'User', function ($scope,Groups,User) {
+ $scope.clearUserlist = function() {
+ $scope.response = [];
+ $scope.usernames = [];
+ }
+
+ $scope.addNewUser = function(user) {
+ if($.inArray(user.user,$scope.usernames) == -1) {
+ $scope.usernames.push(user.user);
+ $scope.response.push(user);
+ }
+ }
+
+ $scope.clearUserlist();
+
+ $scope.chatChanged = function(newvalue,oldvalue){
+ if($scope.group.chat && $scope.group.chat.length > 0){
+ for(var i = 0; i < $scope.group.chat.length; i++) {
+ $scope.addNewUser($scope.group.chat[i]);
+ }
+ }
+ }
+
+ $scope.$watch('group.chat',$scope.chatChanged);
+ }])
+
.controller('ChatCtrl', ['$scope', 'Groups', 'User', function($scope, Groups, User){
$scope._chatMessage = '';
$scope._sending = false;
diff --git a/public/manifest.json b/public/manifest.json
index ee6d559b95..a522b3f140 100644
--- a/public/manifest.json
+++ b/public/manifest.json
@@ -13,6 +13,8 @@
"bower_components/angular-ui/build/angular-ui.js",
"bower_components/angular-ui-utils/modules/keypress/keypress.js",
"bower_components/angular-loading-bar/build/loading-bar.js",
+ "bower_components/Angular-At-Directive/src/at.js",
+ "bower_components/Angular-At-Directive/src/caret.js",
"bower_components/bootstrap/docs/assets/js/bootstrap.js",
"bower_components/angular-bootstrap/ui-bootstrap.js",
diff --git a/src/controllers/challenges.js b/src/controllers/challenges.js
index 233a9a543b..1f566923b4 100644
--- a/src/controllers/challenges.js
+++ b/src/controllers/challenges.js
@@ -67,12 +67,18 @@ api.get = function(req, res) {
api.getMember = function(req, res) {
var cid = req.params.cid, uid = req.params.uid;
- var elMatch = {$elemMatch:{'challenge.id':cid}};
+ // TMK we can't use $elemMatch (which would make things much cleaner) @see http://goo.gl/MxmWdQ & http://goo.gl/Iku44w
+ // Revert back to 9fbb45c to see the $elemMatch solution
User.findById(uid)
- .select({'profile.name':1, habits:elMatch, dailys:elMatch, rewards:elMatch, todos:elMatch})
+ .select('profile.name habits dailys rewards todos')
.exec(function(err, member){
if(err) return res.json(500, {err:err});
if (!member) return res.json(404, {err: 'Member '+uid+' for challenge '+cid+' not found'});
+ _.each(['habits','dailys','todos', 'rewards'], function(type){
+ member[type] = _.filter(member[type], function(task){
+ return task.challenge && task.challenge.id && task.challenge.id == cid;
+ });
+ });
res.json(member);
})
}
diff --git a/views/options/profile.jade b/views/options/profile.jade
index abcf987fc8..7ae3f23b01 100644
--- a/views/options/profile.jade
+++ b/views/options/profile.jade
@@ -57,21 +57,16 @@ script(id='partials/options.profile.avatar.html', type='text/ng-template')
button.btn.btn-small.btn-primary(ng-hide='user.purchased.skin.eb052b && user.purchased.skin.f69922 && user.purchased.skin.f5d70f && user.purchased.skin.0ff591 && user.purchased.skin.2b43f6 && user.purchased.skin.d7a9f7 && user.purchased.skin.800ed0 && user.purchased.skin.rainbow', ng-click='unlock(["skin.eb052b", "skin.f69922", "skin.f5d70f", "skin.0ff591", "skin.2b43f6", "skin.d7a9f7", "skin.800ed0", "skin.rainbow"])') Unlock Set - 5
// Special Events
- div.well.limited-edition
- .label.label-info.pull-right(popover='Available for purchase until November 10th (but permanently in your options if purchased).', popover-title='Limited Edition', popover-placement='right', popover-trigger='mouseenter')
- | Limited Edition
- i.icon.icon-question-sign
- h5.
- Spooky Skins - 2/skin
- //menu(label='Spooky Skins (2 Gems / skin)')
+ // restore to d4df481 to see purchasing + "limited edition" code
+ div(ng-if='user.purchased.skin.monster || user.purchased.skin.pumpkin || user.purchased.skin.skeleton || user.purchased.skin.zombie || user.purchased.skin.ghost || user.purchased.skin.shadow')
+ h5 Spooky Skins
menu
- button.customize-option(type='button', class='{{user.preferences.gender}}_skin_monster', ng-class='{locked: !user.purchased.skin.monster}', ng-click='unlock("skin.monster")')
- button.customize-option(type='button', class='{{user.preferences.gender}}_skin_pumpkin', ng-class='{locked: !user.purchased.skin.pumpkin}', ng-click='unlock("skin.pumpkin")')
- button.customize-option(type='button', class='{{user.preferences.gender}}_skin_skeleton', ng-class='{locked: !user.purchased.skin.skeleton}', ng-click='unlock("skin.skeleton")')
- button.customize-option(type='button', class='{{user.preferences.gender}}_skin_zombie', ng-class='{locked: !user.purchased.skin.zombie}', ng-click='unlock("skin.zombie")')
- button.customize-option(type='button', class='{{user.preferences.gender}}_skin_ghost', ng-class='{locked: !user.purchased.skin.ghost}', ng-click='unlock("skin.ghost")')
- button.customize-option(type='button', class='{{user.preferences.gender}}_skin_shadow', ng-class='{locked: !user.purchased.skin.shadow}', ng-click='unlock("skin.shadow")')
- button.btn.btn-small.btn-primary(ng-hide='user.purchased.skin.monster && user.purchased.skin.pumpkin && user.purchased.skin.skeleton && user.purchased.skin.zombie && user.purchased.skin.ghost && user.purchased.skin.shadow', ng-click='unlock(["skin.monster", "skin.pumpkin", "skin.skeleton", "skin.zombie", "skin.ghost", "skin.shadow"])') Unlock Set - 5
+ button.customize-option(type='button', class='{{user.preferences.gender}}_skin_monster', ng-if='user.purchased.skin.monster', ng-click='unlock("skin.monster")')
+ button.customize-option(type='button', class='{{user.preferences.gender}}_skin_pumpkin', ng-if='user.purchased.skin.pumpkin', ng-click='unlock("skin.pumpkin")')
+ button.customize-option(type='button', class='{{user.preferences.gender}}_skin_skeleton', ng-if='user.purchased.skin.skeleton', ng-click='unlock("skin.skeleton")')
+ button.customize-option(type='button', class='{{user.preferences.gender}}_skin_zombie', ng-if='user.purchased.skin.zombie', ng-click='unlock("skin.zombie")')
+ button.customize-option(type='button', class='{{user.preferences.gender}}_skin_ghost', ng-if='user.purchased.skin.ghost', ng-click='unlock("skin.ghost")')
+ button.customize-option(type='button', class='{{user.preferences.gender}}_skin_shadow', ng-if='user.purchased.skin.shadow', ng-click='unlock("skin.shadow")')
script(id='partials/options.profile.stats.html', type='text/ng-template')
.row-fluid
diff --git a/views/options/social/challenges.jade b/views/options/social/challenges.jade
index 5811ec2cc9..075b82cac2 100644
--- a/views/options/social/challenges.jade
+++ b/views/options/social/challenges.jade
@@ -9,12 +9,12 @@ script(type='text/ng-template', id='partials/options.social.challenges.detail.cl
script(type='text/ng-template', id='partials/options.social.challenges.detail.member.html')
.modal.wide-modal
.modal-header
- button.close(type='button', ng-click='$state.go("^")', aria-hidden='true') ×
- h3 {{obj.profile.name}}
- .modal-body
- habitrpg-tasks(main=false)
- .modal-footer
- a.btn(ng-click='$state.go("^")') Close
+ button.close(type='button', ng-click='$state.go("^")', aria-hidden='true') ×
+ h3 {{obj.profile.name}}
+ .modal-body
+ habitrpg-tasks(main=false)
+ .modal-footer
+ a.btn(ng-click='$state.go("^")') Close
script(type='text/ng-template', id='partials/options.social.challenges.detail.html')
// Edit button
@@ -61,6 +61,10 @@ script(type='text/ng-template', id='partials/options.social.challenges.html')
.span2.well#challenges-filters
h3 Filter:
h4 Groups
+ ul.unstyled
+ a.btn.btn-mini.pull-left(ng-click='selectAll()') All
+ a.btn.btn-mini(ng-click='selectNone()') None
+ br
label.checkbox(ng-repeat='group in groups')
input(type='checkbox', ng-model='search.group[group._id]')
| {{group.name}}
diff --git a/views/options/social/chat-box.jade b/views/options/social/chat-box.jade
index fde98515b5..09a6460346 100644
--- a/views/options/social/chat-box.jade
+++ b/views/options/social/chat-box.jade
@@ -1,8 +1,12 @@
form(ng-submit='postChat(group,_chatMessage)')
.-options
//FIXME ng-model makes this painfully slow! using jquery for now, which is really non-angular-like
- .control-group.option-large
- textarea.chat-textarea.option-content(style='height:6em;', ui-keypress='{13:"postChat(group,_chatMessage)"}', ng-model='_chatMessage')
+ .control-group.option-large(ng-controller='AutocompleteCtrl')
+ textarea.chat-textarea.option-content(style='height:6em;', ui-keypress='{13:"postChat(group,_chatMessage)"}', ng-model='_chatMessage', flag='@', at-user, auto-complete)
+ span.user-list(ng-show='!isAtListHidden', auto-follow='true')
+ ul.list-at-user
+ li(ng-repeat='user in users | filter:query.text | limitTo: 5', ng-click='autoComplete(user)')
+ span.username.label(class='label-contributor-{{user.contributor.level}}') {{user.user}}
table.pull-right
tr
td
diff --git a/views/shared/header/header.jade b/views/shared/header/header.jade
index 28a32bee52..626d295786 100644
--- a/views/shared/header/header.jade
+++ b/views/shared/header/header.jade
@@ -1,4 +1,4 @@
-// @TODO ui:connectionAlert
+//.header-wrap(ng-controller='HeaderCtrl', data-spy="affix", data-offset-top="148")
.header-wrap(ng-controller='HeaderCtrl')
a.label.undo-button(x-bind='click:undo', ng-show='_undo') Undo
div(ng-if='!user.preferences.hideHeader')