diff --git a/.gitignore b/.gitignore index 002780bbdc..4ae45467dc 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,4 @@ coverage coverage.html common/dist/scripts/habitrpg-shared.js -test/spec/translations.js +test/spec/mocks/translations.js diff --git a/Gruntfile.js b/Gruntfile.js index 58ca8f524b..12612129d0 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -220,7 +220,7 @@ module.exports = function(grunt) { require('coffee-script'); var i18n = require('./website/src/i18n'), fs = require('fs'); - fs.writeFileSync('test/spec/translations.js', + fs.writeFileSync('test/spec/mocks/translations.js', "if(!window.env) window.env = {};\n" + "window.env.translations = " + JSON.stringify(i18n.translations['en']) + ';'); }); diff --git a/common/dist/sprites/spritesmith2.png b/common/dist/sprites/spritesmith2.png index c448b073f5..980f2fea09 100644 Binary files a/common/dist/sprites/spritesmith2.png and b/common/dist/sprites/spritesmith2.png differ diff --git a/common/img/sprites/spritesmith/customize/skin/skin_0ff591.png b/common/img/sprites/spritesmith/customize/skin/skin_0ff591.png index 9f046b4495..3ca94dce1e 100644 Binary files a/common/img/sprites/spritesmith/customize/skin/skin_0ff591.png and b/common/img/sprites/spritesmith/customize/skin/skin_0ff591.png differ diff --git a/common/img/sprites/spritesmith/customize/skin/skin_0ff591_sleep.png b/common/img/sprites/spritesmith/customize/skin/skin_0ff591_sleep.png index c9e8e7acf3..f674a3c93f 100644 Binary files a/common/img/sprites/spritesmith/customize/skin/skin_0ff591_sleep.png and b/common/img/sprites/spritesmith/customize/skin/skin_0ff591_sleep.png differ diff --git a/common/img/sprites/spritesmith/customize/skin/skin_2b43f6.png b/common/img/sprites/spritesmith/customize/skin/skin_2b43f6.png index 1d0460856d..6ebc9f32e2 100644 Binary files a/common/img/sprites/spritesmith/customize/skin/skin_2b43f6.png and b/common/img/sprites/spritesmith/customize/skin/skin_2b43f6.png differ diff --git a/common/img/sprites/spritesmith/customize/skin/skin_2b43f6_sleep.png b/common/img/sprites/spritesmith/customize/skin/skin_2b43f6_sleep.png index 9ad780f1ba..1cabeb8a05 100644 Binary files a/common/img/sprites/spritesmith/customize/skin/skin_2b43f6_sleep.png and b/common/img/sprites/spritesmith/customize/skin/skin_2b43f6_sleep.png differ diff --git a/common/img/sprites/spritesmith/customize/skin/skin_6bd049.png b/common/img/sprites/spritesmith/customize/skin/skin_6bd049.png index 2911881676..20e4000326 100644 Binary files a/common/img/sprites/spritesmith/customize/skin/skin_6bd049.png and b/common/img/sprites/spritesmith/customize/skin/skin_6bd049.png differ diff --git a/common/img/sprites/spritesmith/customize/skin/skin_6bd049_sleep.png b/common/img/sprites/spritesmith/customize/skin/skin_6bd049_sleep.png index 09eecdd171..722ba42cdb 100644 Binary files a/common/img/sprites/spritesmith/customize/skin/skin_6bd049_sleep.png and b/common/img/sprites/spritesmith/customize/skin/skin_6bd049_sleep.png differ diff --git a/common/img/sprites/spritesmith/customize/skin/skin_800ed0.png b/common/img/sprites/spritesmith/customize/skin/skin_800ed0.png index 46588cb9a5..5a490c6f4c 100644 Binary files a/common/img/sprites/spritesmith/customize/skin/skin_800ed0.png and b/common/img/sprites/spritesmith/customize/skin/skin_800ed0.png differ diff --git a/common/img/sprites/spritesmith/customize/skin/skin_800ed0_sleep.png b/common/img/sprites/spritesmith/customize/skin/skin_800ed0_sleep.png index ed46320d5c..ac0efda3c0 100644 Binary files a/common/img/sprites/spritesmith/customize/skin/skin_800ed0_sleep.png and b/common/img/sprites/spritesmith/customize/skin/skin_800ed0_sleep.png differ diff --git a/common/img/sprites/spritesmith/customize/skin/skin_915533.png b/common/img/sprites/spritesmith/customize/skin/skin_915533.png index 0d7f775d8a..823ca81764 100644 Binary files a/common/img/sprites/spritesmith/customize/skin/skin_915533.png and b/common/img/sprites/spritesmith/customize/skin/skin_915533.png differ diff --git a/common/img/sprites/spritesmith/customize/skin/skin_915533_sleep.png b/common/img/sprites/spritesmith/customize/skin/skin_915533_sleep.png index 371f8f7b9a..340d603e98 100644 Binary files a/common/img/sprites/spritesmith/customize/skin/skin_915533_sleep.png and b/common/img/sprites/spritesmith/customize/skin/skin_915533_sleep.png differ diff --git a/common/img/sprites/spritesmith/customize/skin/skin_98461a.png b/common/img/sprites/spritesmith/customize/skin/skin_98461a.png index 8ba33e2168..a28b1c5468 100644 Binary files a/common/img/sprites/spritesmith/customize/skin/skin_98461a.png and b/common/img/sprites/spritesmith/customize/skin/skin_98461a.png differ diff --git a/common/img/sprites/spritesmith/customize/skin/skin_98461a_sleep.png b/common/img/sprites/spritesmith/customize/skin/skin_98461a_sleep.png index ee6ffa2199..2741afbe74 100644 Binary files a/common/img/sprites/spritesmith/customize/skin/skin_98461a_sleep.png and b/common/img/sprites/spritesmith/customize/skin/skin_98461a_sleep.png differ diff --git a/common/img/sprites/spritesmith/customize/skin/skin_c06534.png b/common/img/sprites/spritesmith/customize/skin/skin_c06534.png index 5a0e350242..ef3ebe76f0 100644 Binary files a/common/img/sprites/spritesmith/customize/skin/skin_c06534.png and b/common/img/sprites/spritesmith/customize/skin/skin_c06534.png differ diff --git a/common/img/sprites/spritesmith/customize/skin/skin_c06534_sleep.png b/common/img/sprites/spritesmith/customize/skin/skin_c06534_sleep.png index 09ac494f08..ddbb7519f0 100644 Binary files a/common/img/sprites/spritesmith/customize/skin/skin_c06534_sleep.png and b/common/img/sprites/spritesmith/customize/skin/skin_c06534_sleep.png differ diff --git a/common/img/sprites/spritesmith/customize/skin/skin_c3e1dc.png b/common/img/sprites/spritesmith/customize/skin/skin_c3e1dc.png index b3a0acf388..a9d1b09425 100644 Binary files a/common/img/sprites/spritesmith/customize/skin/skin_c3e1dc.png and b/common/img/sprites/spritesmith/customize/skin/skin_c3e1dc.png differ diff --git a/common/img/sprites/spritesmith/customize/skin/skin_c3e1dc_sleep.png b/common/img/sprites/spritesmith/customize/skin/skin_c3e1dc_sleep.png index 5a8d12c052..eb505a4db8 100644 Binary files a/common/img/sprites/spritesmith/customize/skin/skin_c3e1dc_sleep.png and b/common/img/sprites/spritesmith/customize/skin/skin_c3e1dc_sleep.png differ diff --git a/common/img/sprites/spritesmith/customize/skin/skin_d7a9f7.png b/common/img/sprites/spritesmith/customize/skin/skin_d7a9f7.png index ecf379b8ad..9ece142678 100644 Binary files a/common/img/sprites/spritesmith/customize/skin/skin_d7a9f7.png and b/common/img/sprites/spritesmith/customize/skin/skin_d7a9f7.png differ diff --git a/common/img/sprites/spritesmith/customize/skin/skin_d7a9f7_sleep.png b/common/img/sprites/spritesmith/customize/skin/skin_d7a9f7_sleep.png index 40bd6dd25a..8ce9331d40 100644 Binary files a/common/img/sprites/spritesmith/customize/skin/skin_d7a9f7_sleep.png and b/common/img/sprites/spritesmith/customize/skin/skin_d7a9f7_sleep.png differ diff --git a/common/img/sprites/spritesmith/customize/skin/skin_ddc994.png b/common/img/sprites/spritesmith/customize/skin/skin_ddc994.png index c084a9e760..e55ccd274d 100644 Binary files a/common/img/sprites/spritesmith/customize/skin/skin_ddc994.png and b/common/img/sprites/spritesmith/customize/skin/skin_ddc994.png differ diff --git a/common/img/sprites/spritesmith/customize/skin/skin_ddc994_sleep.png b/common/img/sprites/spritesmith/customize/skin/skin_ddc994_sleep.png index 920727de5b..20ee65d4f9 100644 Binary files a/common/img/sprites/spritesmith/customize/skin/skin_ddc994_sleep.png and b/common/img/sprites/spritesmith/customize/skin/skin_ddc994_sleep.png differ diff --git a/common/img/sprites/spritesmith/customize/skin/skin_ea8349.png b/common/img/sprites/spritesmith/customize/skin/skin_ea8349.png index 470723ed9f..74ec2b5c15 100644 Binary files a/common/img/sprites/spritesmith/customize/skin/skin_ea8349.png and b/common/img/sprites/spritesmith/customize/skin/skin_ea8349.png differ diff --git a/common/img/sprites/spritesmith/customize/skin/skin_ea8349_sleep.png b/common/img/sprites/spritesmith/customize/skin/skin_ea8349_sleep.png index 1484d376f8..8e8c089749 100644 Binary files a/common/img/sprites/spritesmith/customize/skin/skin_ea8349_sleep.png and b/common/img/sprites/spritesmith/customize/skin/skin_ea8349_sleep.png differ diff --git a/common/img/sprites/spritesmith/customize/skin/skin_eb052b.png b/common/img/sprites/spritesmith/customize/skin/skin_eb052b.png index ede562fb75..ffa04f3e26 100644 Binary files a/common/img/sprites/spritesmith/customize/skin/skin_eb052b.png and b/common/img/sprites/spritesmith/customize/skin/skin_eb052b.png differ diff --git a/common/img/sprites/spritesmith/customize/skin/skin_eb052b_sleep.png b/common/img/sprites/spritesmith/customize/skin/skin_eb052b_sleep.png index 21e2dc8ae2..ff384460b3 100644 Binary files a/common/img/sprites/spritesmith/customize/skin/skin_eb052b_sleep.png and b/common/img/sprites/spritesmith/customize/skin/skin_eb052b_sleep.png differ diff --git a/common/img/sprites/spritesmith/customize/skin/skin_f5a76e.png b/common/img/sprites/spritesmith/customize/skin/skin_f5a76e.png index 35d2f7f08e..0e7b3bbfb9 100644 Binary files a/common/img/sprites/spritesmith/customize/skin/skin_f5a76e.png and b/common/img/sprites/spritesmith/customize/skin/skin_f5a76e.png differ diff --git a/common/img/sprites/spritesmith/customize/skin/skin_f5a76e_sleep.png b/common/img/sprites/spritesmith/customize/skin/skin_f5a76e_sleep.png index 64a078a008..5621d1001a 100644 Binary files a/common/img/sprites/spritesmith/customize/skin/skin_f5a76e_sleep.png and b/common/img/sprites/spritesmith/customize/skin/skin_f5a76e_sleep.png differ diff --git a/common/img/sprites/spritesmith/customize/skin/skin_f5d70f.png b/common/img/sprites/spritesmith/customize/skin/skin_f5d70f.png index eb9067a981..4ba5d3193e 100644 Binary files a/common/img/sprites/spritesmith/customize/skin/skin_f5d70f.png and b/common/img/sprites/spritesmith/customize/skin/skin_f5d70f.png differ diff --git a/common/img/sprites/spritesmith/customize/skin/skin_f5d70f_sleep.png b/common/img/sprites/spritesmith/customize/skin/skin_f5d70f_sleep.png index b5fb256d22..5cb072899e 100644 Binary files a/common/img/sprites/spritesmith/customize/skin/skin_f5d70f_sleep.png and b/common/img/sprites/spritesmith/customize/skin/skin_f5d70f_sleep.png differ diff --git a/common/img/sprites/spritesmith/customize/skin/skin_f69922.png b/common/img/sprites/spritesmith/customize/skin/skin_f69922.png index 2ac2ef62a8..071299b891 100644 Binary files a/common/img/sprites/spritesmith/customize/skin/skin_f69922.png and b/common/img/sprites/spritesmith/customize/skin/skin_f69922.png differ diff --git a/common/img/sprites/spritesmith/customize/skin/skin_f69922_sleep.png b/common/img/sprites/spritesmith/customize/skin/skin_f69922_sleep.png index 786e8fbd3c..0d12e4afb4 100644 Binary files a/common/img/sprites/spritesmith/customize/skin/skin_f69922_sleep.png and b/common/img/sprites/spritesmith/customize/skin/skin_f69922_sleep.png differ diff --git a/common/locales/en/challenge.json b/common/locales/en/challenge.json index d8e6dbe914..a22e4bf57d 100644 --- a/common/locales/en/challenge.json +++ b/common/locales/en/challenge.json @@ -48,5 +48,9 @@ "removeTasks": "Remove Tasks", "keepTasks": "Keep Tasks", "closeCha": "Close challenge and...", - "leaveCha": "Leave challenge and..." + "leaveCha": "Leave challenge and...", + "challengedOwnedFilterHeader": "Ownership", + "challengedOwnedFilter": "Owned", + "challengedNotOwnedFilter": "Not Owned", + "challengedEitherOwnedFilter": "Either" } diff --git a/common/locales/en/tasks.json b/common/locales/en/tasks.json index e0bad70ab4..e80aeee367 100644 --- a/common/locales/en/tasks.json +++ b/common/locales/en/tasks.json @@ -36,6 +36,11 @@ "newDailyBulk": "New Dailies (one per line)", "streakCounter": "Streak Counter", "repeat": "Repeat", + "repeatEvery": "Repeat Every", + "repeatDays": "Every X Days", + "repeatWeek": "On Certain Days of the Week", + "day": "Day", + "days": "Days", "restoreStreak": "Restore Streak", "todos": "To-Dos", "newTodo": "New To-Do", @@ -60,6 +65,9 @@ "clearTags": "Clear", "hideTags": "Hide", "showTags": "Show", + "startDate": "Start Date", + "startDateHelpTitle": "When should this task start?", + "startDateHelp": "Set the date for which this task takes effect. Will not be due on earlier days.", "streakName": "Streak Achievements", "streakText": "Has performed <%= streaks %> 21-day streaks on Dailies", "streakSingular": "Streaker", @@ -95,5 +103,6 @@ "rewardHelp1": "The Equipment you buy for your avatar is stored in <%= linkStart %>Inventory > Equipment<%= linkEnd %>.", "rewardHelp2": "Equipment affects your stats (<%= linkStart %>Avatar > Stats<%= linkEnd %>).", "rewardHelp3": "Special equipment will appear here during World Events.", - "rewardHelp4": "Don't be afraid to set custom Rewards! Check out some samples here." + "rewardHelp4": "Don't be afraid to set custom Rewards! Check out some samples here.", + "clickForHelp": "Click for help" } diff --git a/common/locales/en_GB/tasks.json b/common/locales/en_GB/tasks.json index 852edcfc3f..515992d5d7 100644 --- a/common/locales/en_GB/tasks.json +++ b/common/locales/en_GB/tasks.json @@ -54,6 +54,7 @@ "newReward": "New Reward", "newRewardBulk": "New Rewards (one per line)", "price": "Price", + "search": "Search", "tags": "Tags", "editTags": "Edit", "newTag": "New Tag", @@ -79,4 +80,4 @@ "pushTaskToBottom": "Push task to bottom", "emptyTask": "Enter the task's title first.", "dailiesRestingInInn": "You're Resting in the Inn! Your Dailies will NOT hurt you tonight, but they WILL still refresh every day. If you're in a quest, you won't deal damage/collect items until you check out of the Inn, but you can still be injured by a Boss if your Party mates skip their own Dailies." -} \ No newline at end of file +} diff --git a/common/script/index.coffee b/common/script/index.coffee index 5ab1c09699..e18df3dd3f 100644 --- a/common/script/index.coffee +++ b/common/script/index.coffee @@ -72,13 +72,37 @@ api.daysSince = (yesterday, options = {}) -> Math.abs api.startOfDay(_.defaults {now:yesterday}, o).diff(api.startOfDay(_.defaults {now:o.now}, o), 'days') ### - Should the user do this taks on this date, given the task's repeat options and user.preferences.dayStart? + Should the user do this task on this date, given the task's repeat options and user.preferences.dayStart? ### -api.shouldDo = (day, repeat, options={}) -> - return false unless repeat +api.shouldDo = (day, dailyTask, options = {}) -> + return false unless dailyTask.type == 'daily' && dailyTask.repeat + if !dailyTask.startDate + dailyTask.startDate = moment().toDate() + if dailyTask.startDate instanceof String + dailyTask.startDate = moment(dailyTask.startDate).toDate() o = sanitizeOptions options - selected = repeat[api.dayMapping[api.startOfDay(_.defaults {now:day}, o).day()]] - return selected + day = api.startOfDay(_.defaults {now:day}, o) + dayOfWeekNum = day.day() # e.g. 1 for Monday if week starts on Mon + + # check if event is today or in the future + hasStartedCheck = day >= api.startOfDay(_.defaults {now:dailyTask.startDate}, o) + + if dailyTask.frequency == 'daily' + daysSinceTaskStart = api.numDaysApart(day.startOf('day'), dailyTask.startDate, o) + everyXCheck = (daysSinceTaskStart % dailyTask.everyX == 0) + return everyXCheck && hasStartedCheck + else if dailyTask.frequency == 'weekly' + dayOfWeekCheck = dailyTask.repeat[api.dayMapping[dayOfWeekNum]] + return dayOfWeekCheck && hasStartedCheck + else + # unexpected frequency string + return false + +api.numDaysApart = (day1, day2, o) -> + startOfDay1 = api.startOfDay(_.defaults {now:day1}, o) + startOfDay2 = api.startOfDay(_.defaults {now:day2}, o) + numDays = Math.abs(startOfDay1.diff(startOfDay2, 'days')) + return numDays ### ------------------------------------------------------ @@ -211,7 +235,7 @@ api.taskDefaults = (task={}) -> _.defaults(task, {up:true,down:true}) if task.type is 'habit' _.defaults(task, {history: []}) if task.type in ['habit', 'daily'] _.defaults(task, {completed:false}) if task.type in ['daily', 'todo'] - _.defaults(task, {streak:0, repeat: {su:1,m:1,t:1,w:1,th:1,f:1,s:1}}) if task.type is 'daily' + _.defaults(task, {streak:0, repeat: {su:1,m:1,t:1,w:1,th:1,f:1,s:1}}, startDate: new Date(), everyX: 1, frequency: 'weekly') if task.type is 'daily' task._id = task.id # may need this for TaskSchema if we go back to using it, see http://goo.gl/a5irq4 task.value ?= if task.type is 'reward' then 10 else 0 task.priority = 1 unless _.isNumber(task.priority) # hotfix for apiv1. once we're off apiv1, we can remove this @@ -281,7 +305,7 @@ api.taskClasses = (task, filters=[], dayStart=0, lastCron=+new Date, showComplet # show as completed if completed (naturally) or not required for today if type in ['todo', 'daily'] - if completed or (type is 'daily' and !api.shouldDo(+new Date, task.repeat, {dayStart})) + if completed or (type is 'daily' and !api.shouldDo(+new Date, task, {dayStart})) classes += " completed" else classes += " uncompleted" @@ -534,7 +558,7 @@ api.wrap = (user, main=true) -> user.preferences.costume = false # Remove unlocked features flags = user.flags - if not (user.achievements.ultimateGear or user.achievements.beastMaster) + if not user.achievements.beastMaster flags.rebirthEnabled = false flags.itemsEnabled = false flags.dropsEnabled = false @@ -1489,7 +1513,7 @@ api.wrap = (user, main=true) -> user._tmp.drop = _.defaults content.quests[k], type: 'Quest' dialog: i18n.t('messageFoundQuest', {questText: content.quests[k].text(req.language)}, req.language) - if !user.flags.rebirthEnabled and (user.stats.lvl >= 50 or user.achievements.ultimateGear or user.achievements.beastMaster) + if !user.flags.rebirthEnabled and (user.stats.lvl >= 50 or user.achievements.beastMaster) user.flags.rebirthEnabled = true if user.stats.lvl >= api.maxLevel and !user.flags.freeRebirth user.flags.freeRebirth = true @@ -1565,7 +1589,7 @@ api.wrap = (user, main=true) -> {completed, repeat} = daily thatDay = moment(now).subtract({days: 1}) - if api.shouldDo(thatDay, repeat, user.preferences) || completed + if api.shouldDo(thatDay.toDate(), daily, user.preferences) || completed _.each daily.checklist, ((box)->box.completed=false;true) daily.completed = false return @@ -1592,7 +1616,7 @@ api.wrap = (user, main=true) -> scheduleMisses = 0 _.times daysMissed, (n) -> thatDay = moment(now).subtract({days: n + 1}) - if api.shouldDo(thatDay, repeat, user.preferences) + if api.shouldDo(thatDay.toDate(), task, user.preferences) scheduleMisses++ if user.stats.buffs.stealth user.stats.buffs.stealth-- diff --git a/karma.conf.js b/karma.conf.js index 0b79578199..a4ee7a1296 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -35,7 +35,7 @@ module.exports = function(config) { 'website/public/bower_components/js-emoji/emoji.js', 'common/dist/scripts/habitrpg-shared.js', - "test/spec/translations.js", + "test/spec/mocks/translations.js", "website/public/js/env.js", @@ -51,7 +51,9 @@ module.exports = function(config) { "website/public/js/services/challengeServices.js", "website/public/js/services/paymentServices.js", - "website/public/js/filters/filters.js", + "website/public/js/filters/money.js", + "website/public/js/filters/roundLargeNumbers.js", + "website/public/js/filters/taskOrdering.js", "website/public/js/directives/focus-me.directive.js", "website/public/js/directives/from-now.directive.js", @@ -77,7 +79,7 @@ module.exports = function(config) { "website/public/js/controllers/footerCtrl.js", "website/public/js/controllers/challengesCtrl.js", "website/public/js/controllers/hallCtrl.js", - 'test/spec/mock/**/*.js', + 'test/spec/mocks/**/*.js', 'test/spec/specHelper.js', 'test/spec/**/*.js' ], diff --git a/test/common/algos.mocha.coffee b/test/common/algos.mocha.coffee index 1f02d94628..44ff5d0f72 100644 --- a/test/common/algos.mocha.coffee +++ b/test/common/algos.mocha.coffee @@ -159,7 +159,7 @@ describe 'User', -> it 'handles perfect days', -> user = newUser() user.dailys = [] - _.times 3, ->user.dailys.push shared.taskDefaults({type:'daily'}) + _.times 3, ->user.dailys.push shared.taskDefaults({type:'daily', startDate: moment().subtract(7, 'days')}) cron = -> user.lastCron = moment().subtract(1,'days');user.fns.cron() cron() @@ -193,7 +193,7 @@ describe 'User', -> user.preferences.sleep = true cron = -> user.lastCron = moment().subtract(1, 'days');user.fns.cron() user.dailys = [] - _.times 2, -> user.dailys.push shared.taskDefaults({type:'daily'}) + _.times 2, -> user.dailys.push shared.taskDefaults({type:'daily', startDate: moment().subtract(7, 'days')}) it 'remains in the inn on cron', -> cron() @@ -883,8 +883,9 @@ describe 'Cron', -> before.dailys[0].repeat = after.dailys[0].repeat = options.repeat if options.repeat before.dailys[0].streak = after.dailys[0].streak = 10 before.dailys[0].completed = after.dailys[0].completed = true if options.checked + before.dailys[0].startDate = after.dailys[0].startDate = moment().subtract(30, 'days') if options.shouldDo - expect(shared.shouldDo(now, options.repeat, {timezoneOffset, dayStart:options.dayStart, now})).to.be.ok() + expect(shared.shouldDo(now.toDate(), after.dailys[0], {timezoneOffset, dayStart:options.dayStart, now})).to.be.ok() after.fns.cron {now} before.stats.mp=after.stats.mp #FIXME switch options.expect diff --git a/test/common/dailies.coffee b/test/common/dailies.coffee new file mode 100644 index 0000000000..d26c8d012e --- /dev/null +++ b/test/common/dailies.coffee @@ -0,0 +1,330 @@ +_ = require 'lodash' +expect = require 'expect.js' +sinon = require 'sinon' +moment = require 'moment' +shared = require '../../common/script/index.coffee' +shared.i18n.translations = require('../../website/src/i18n.js').translations + +repeatWithoutLastWeekday = ()-> + repeat = {su:1,m:1,t:1,w:1,th:1,f:1,s:1} + if shared.startOfWeek(moment().zone(0)).isoWeekday() == 1 # Monday + repeat.su = false + else + repeat.s = false + {repeat: repeat} + +### Helper Functions #### +# @TODO: Refactor into helper file +newUser = (addTasks=true)-> + buffs = {per:0, int:0, con:0, str:0, stealth: 0, streaks: false} + user = + auth: + timestamps: {} + stats: {str:1, con:1, per:1, int:1, mp: 32, class: 'warrior', buffs: buffs} + items: + lastDrop: + count: 0 + hatchingPotions: {} + eggs: {} + food: {} + gear: + equipped: {} + costume: {} + party: + quest: + progress: + down: 0 + preferences: {} + dailys: [] + todos: [] + rewards: [] + flags: {} + achievements: {} + contributor: + level: 2 + shared.wrap(user) + user.ops.reset(null, ->) + if addTasks + _.each ['habit', 'todo', 'daily'], (task)-> + user.ops.addTask {body: {type: task, id: shared.uuid()}} + user + +cron = (usr) -> + usr.lastCron = moment().subtract(1,'days') + usr.fns.cron() + +describe 'daily/weekly that repeats everyday (default)', -> + user = null + daily = null + weekly = null + + describe 'when startDate is in the future', -> + + beforeEach -> + user = newUser() + user.dailys = [ + shared.taskDefaults({type:'daily', startDate: moment().add(7, 'days'), frequency: 'daily'}) + shared.taskDefaults({type:'daily', startDate: moment().add(7, 'days'), frequency: 'weekly', repeat: {su:1,m:1,t:1,w:1,th:1,f:1,s:1}}) + ] + daily = user.dailys[0] + weekly = user.dailys[1] + + it 'does not damage user for not completing it', -> + cron(user) + expect(user.stats.hp).to.be 50 + + it 'does not change value on cron if daily is incomplete', -> + cron(user) + expect(daily.value).to.be 0 + expect(weekly.value).to.be 0 + + it 'does not reset checklists if daily is not marked as complete', -> + checklist = [ + { + 'text' : '1', + 'id' : 'checklist-one', + 'completed' : true + }, + { + 'text' : '2', + 'id' : 'checklist-two', + 'completed' : true + }, + { + 'text' : '3', + 'id' : 'checklist-three', + 'completed' : false + } + ] + daily.checklist = checklist + weekly.checklist = checklist + cron(user) + + expect(daily.checklist[0].completed).to.be true + expect(daily.checklist[1].completed).to.be true + expect(daily.checklist[2].completed).to.be false + + expect(weekly.checklist[0].completed).to.be true + expect(weekly.checklist[1].completed).to.be true + expect(weekly.checklist[2].completed).to.be false + + it 'resets checklists if daily is marked as complete', -> + checklist = [ + { + 'text' : '1', + 'id' : 'checklist-one', + 'completed' : true + }, + { + 'text' : '2', + 'id' : 'checklist-two', + 'completed' : true + }, + { + 'text' : '3', + 'id' : 'checklist-three', + 'completed' : false + } + ] + daily.checklist = checklist + weekly.checklist = checklist + daily.completed = true + weekly.completed = true + cron(user) + + _.each daily.checklist, (box)-> + expect(box.completed).to.be false + + _.each weekly.checklist, (box)-> + expect(box.completed).to.be false + + it 'is due on startDate', -> + daily_due_today = shared.shouldDo moment(), daily + daily_due_on_start_date = shared.shouldDo moment().add(7, 'days'), daily + + expect(daily_due_today).to.be false + expect(daily_due_on_start_date).to.be true + + weekly_due_today = shared.shouldDo moment(), weekly + weekly_due_on_start_date = shared.shouldDo moment().add(7, 'days'), weekly + + expect(weekly_due_today).to.be false + expect(weekly_due_on_start_date).to.be true + + describe 'when startDate is in the past', -> + completeDaily = null + + beforeEach -> + user = newUser() + user.dailys = [ + shared.taskDefaults({type:'daily', startDate: moment().subtract(7, 'days'), frequency: 'daily'}) + shared.taskDefaults({type:'daily', startDate: moment().subtract(7, 'days'), frequency: 'weekly'}) + ] + daily = user.dailys[0] + weekly = user.dailys[1] + + it 'does damage user for not completing it', -> + cron(user) + expect(user.stats.hp).to.be.lessThan 50 + + it 'decreases value on cron if daily is incomplete', -> + cron(user) + expect(daily.value).to.be.lessThan 0 + expect(weekly.value).to.be.lessThan 0 + + it 'resets checklists if daily is not marked as complete', -> + checklist = [ + { + 'text' : '1', + 'id' : 'checklist-one', + 'completed' : true + }, + { + 'text' : '2', + 'id' : 'checklist-two', + 'completed' : true + }, + { + 'text' : '3', + 'id' : 'checklist-three', + 'completed' : false + } + ] + daily.checklist = checklist + weekly.checklist = checklist + cron(user) + + _.each daily.checklist, (box)-> + expect(box.completed).to.be false + + _.each weekly.checklist, (box)-> + expect(box.completed).to.be false + + it 'resets checklists if daily is marked as complete', -> + checklist = [ + { + 'text' : '1', + 'id' : 'checklist-one', + 'completed' : true + }, + { + 'text' : '2', + 'id' : 'checklist-two', + 'completed' : true + }, + { + 'text' : '3', + 'id' : 'checklist-three', + 'completed' : false + } + ] + daily.checklist = checklist + daily.completed = true + weekly.checklist = checklist + weekly.completed = true + cron(user) + + _.each daily.checklist, (box)-> + expect(box.completed).to.be false + + _.each weekly.checklist, (box)-> + expect(box.completed).to.be false + + describe 'when startDate is today', -> + completeDaily = null + + beforeEach -> + user = newUser() + user.dailys = [ + # Must set start date to yesterday, because cron mock sets last cron to yesterday + shared.taskDefaults({type:'daily', startDate: moment().subtract(1, 'days'), frequency: 'daily'}) + shared.taskDefaults({type:'daily', startDate: moment().subtract(1, 'days'), frequency: 'weekly'}) + ] + daily = user.dailys[0] + weekly = user.dailys[1] + + it 'does damage user for not completing it', -> + cron(user) + expect(user.stats.hp).to.be.lessThan 50 + + it 'decreases value on cron if daily is incomplete', -> + cron(user) + expect(daily.value).to.be.lessThan 0 + expect(weekly.value).to.be.lessThan 0 + + it 'resets checklists if daily is not marked as complete', -> + checklist = [ + { + 'text' : '1', + 'id' : 'checklist-one', + 'completed' : true + }, + { + 'text' : '2', + 'id' : 'checklist-two', + 'completed' : true + }, + { + 'text' : '3', + 'id' : 'checklist-three', + 'completed' : false + } + ] + daily.checklist = checklist + weekly.checklist = checklist + cron(user) + + _.each daily.checklist, (box)-> + expect(box.completed).to.be false + + _.each weekly.checklist, (box)-> + expect(box.completed).to.be false + + it 'resets checklists if daily is marked as complete', -> + checklist = [ + { + 'text' : '1', + 'id' : 'checklist-one', + 'completed' : true + }, + { + 'text' : '2', + 'id' : 'checklist-two', + 'completed' : true + }, + { + 'text' : '3', + 'id' : 'checklist-three', + 'completed' : false + } + ] + daily.checklist = checklist + daily.completed = true + weekly.checklist = checklist + weekly.completed = true + cron(user) + + _.each daily.checklist, (box)-> + expect(box.completed).to.be false + + _.each weekly.checklist, (box)-> + expect(box.completed).to.be false + +describe 'daily that repeats every x days', -> + user = null + daily = null + + beforeEach -> + user = newUser() + user.dailys = [ shared.taskDefaults({type:'daily', startDate: moment(), frequency: 'daily'}) ] + daily = user.dailys[0] + + _.times 11, (due) -> + + it 'where x equals ' + due, -> + daily.everyX = due + + _.times 30, (day) -> + isDue = shared.shouldDo moment().add(day, 'days'), daily + expect(isDue).to.be true if day % due == 0 + expect(isDue).to.be false if day % due != 0 diff --git a/test/spec/authCtrlSpec.js b/test/spec/controllers/authCtrlSpec.js similarity index 100% rename from test/spec/authCtrlSpec.js rename to test/spec/controllers/authCtrlSpec.js diff --git a/test/spec/controllers/challengesCtrlSpec.js b/test/spec/controllers/challengesCtrlSpec.js new file mode 100644 index 0000000000..417095c152 --- /dev/null +++ b/test/spec/controllers/challengesCtrlSpec.js @@ -0,0 +1,177 @@ +'use strict'; + +describe('Challenges Controller', function() { + var $rootScope, scope, user, ctrl, challenges, groups; + + beforeEach(function() { + module(function($provide) { + $provide.value('User', {}); + }); + + inject(function($rootScope, $controller, Challenges, Groups){ + user = specHelper.newUser(); + user._id = "unique-user-id"; + + scope = $rootScope.$new(); + + // Load RootCtrl to ensure shared behaviors are loaded + $controller('RootCtrl', {$scope: scope, User: {user: user}}); + + ctrl = $controller('ChallengesCtrl', {$scope: scope, User: {user: user}}); + + challenges = Challenges; + groups = Groups; + }); + }); + + describe('filterChallenges', function() { + var ownMem, ownNotMem, notOwnMem, notOwnNotMem; + + beforeEach(function() { + ownMem = new challenges.Challenge({ + name: 'test', + description: 'You are the owner and member', + habits: [], + dailys: [], + todos: [], + rewards: [], + leader: user._id, + group: "test", + timestamp: +(new Date), + members: [user], + official: false, + _isMember: true + }); + + ownNotMem = new challenges.Challenge({ + name: 'test', + description: 'You are the owner, but not a member', + habits: [], + dailys: [], + todos: [], + rewards: [], + leader: user._id, + group: "test", + timestamp: +(new Date), + members: [], + official: false, + _isMember: false + }); + + notOwnMem = new challenges.Challenge({ + name: 'test', + description: 'Not owner but a member', + habits: [], + dailys: [], + todos: [], + rewards: [], + leader: {_id:"test"}, + group: "test", + timestamp: +(new Date), + members: [user], + official: false, + _isMember: true + }); + + notOwnNotMem = new challenges.Challenge({ + name: 'test', + description: 'Not owner or member', + habits: [], + dailys: [], + todos: [], + rewards: [], + leader: {_id:"test"}, + group: "test", + timestamp: +(new Date), + members: [], + official: false, + _isMember: false + }); + + scope.search = { + group: _.transform(groups, function(m,g){m[g._id]=true;}) + }; + }); + + it('displays challenges that match membership: either and owner: either', function() { + scope.search._isMember = 'either'; + scope.search._isOwner = 'either'; + expect(scope.filterChallenges(ownMem)).to.eql(true); + expect(scope.filterChallenges(ownNotMem)).to.eql(true); + expect(scope.filterChallenges(notOwnMem)).to.eql(true); + expect(scope.filterChallenges(notOwnNotMem)).to.eql(true); + }); + + it('displays challenges that match membership: either and owner: true', function() { + scope.search._isMember = 'either'; + scope.search._isOwner = true; + expect(scope.filterChallenges(ownMem)).to.eql(true); + expect(scope.filterChallenges(ownNotMem)).to.eql(true); + expect(scope.filterChallenges(notOwnMem)).to.eql(false); + expect(scope.filterChallenges(notOwnNotMem)).to.eql(false); + }); + + it('displays challenges that match membership: either and owner: false', function() { + scope.search._isMember = 'either'; + scope.search._isOwner = false; + expect(scope.filterChallenges(ownMem)).to.eql(false); + expect(scope.filterChallenges(ownNotMem)).to.eql(false); + expect(scope.filterChallenges(notOwnMem)).to.eql(true); + expect(scope.filterChallenges(notOwnNotMem)).to.eql(true); + }); + + it('displays challenges that match membership: true and owner: either', function() { + scope.search._isMember = true; + scope.search._isOwner = 'either'; + expect(scope.filterChallenges(ownMem)).to.eql(true); + expect(scope.filterChallenges(ownNotMem)).to.eql(false); + expect(scope.filterChallenges(notOwnMem)).to.eql(true); + expect(scope.filterChallenges(notOwnNotMem)).to.eql(false); + }); + + it('displays challenges that match membership: true and owner: true', function() { + scope.search._isMember = true; + scope.search._isOwner = true; + expect(scope.filterChallenges(ownMem)).to.eql(true); + expect(scope.filterChallenges(ownNotMem)).to.eql(false); + expect(scope.filterChallenges(notOwnMem)).to.eql(false); + expect(scope.filterChallenges(notOwnNotMem)).to.eql(false); + }); + + it('displays challenges that match membership: true and owner: false', function() { + scope.search._isMember = true; + scope.search._isOwner = false; + expect(scope.filterChallenges(ownMem)).to.eql(false); + expect(scope.filterChallenges(ownNotMem)).to.eql(false); + expect(scope.filterChallenges(notOwnMem)).to.eql(true); + expect(scope.filterChallenges(notOwnNotMem)).to.eql(false); + }); + + it('displays challenges that match membership: false and owner: either', function() { + scope.search._isMember = false; + scope.search._isOwner = 'either'; + expect(scope.filterChallenges(ownMem)).to.eql(false); + expect(scope.filterChallenges(ownNotMem)).to.eql(true); + expect(scope.filterChallenges(notOwnMem)).to.eql(false); + expect(scope.filterChallenges(notOwnNotMem)).to.eql(true); + }); + + it('displays challenges that match membership: false and owner: true', function() { + scope.search._isMember = false; + scope.search._isOwner = true; + expect(scope.filterChallenges(ownMem)).to.eql(false); + expect(scope.filterChallenges(ownNotMem)).to.eql(true); + expect(scope.filterChallenges(notOwnMem)).to.eql(false); + expect(scope.filterChallenges(notOwnNotMem)).to.eql(false); + }); + + it('displays challenges that match membership: false and owner: false', function() { + scope.search._isMember = false; + scope.search._isOwner = false; + expect(scope.filterChallenges(ownMem)).to.eql(false); + expect(scope.filterChallenges(ownNotMem)).to.eql(false); + expect(scope.filterChallenges(notOwnMem)).to.eql(false); + expect(scope.filterChallenges(notOwnNotMem)).to.eql(true); + }); + }); +}); diff --git a/test/spec/controllers/filtersCtrlSpec.js b/test/spec/controllers/filtersCtrlSpec.js new file mode 100644 index 0000000000..bbebce3cfd --- /dev/null +++ b/test/spec/controllers/filtersCtrlSpec.js @@ -0,0 +1,39 @@ +'use strict'; + +describe('Filters Controller', function() { + var scope, user; + + beforeEach(inject(function($rootScope, $controller, Shared) { + user = specHelper.newUser(); + Shared.wrap(user); + scope = $rootScope.$new(); + $controller('FiltersCtrl', {$scope: scope, User: {user: user}}); + })); + + describe('tags', function(){ + it('creates a tag', function(){ + scope._newTag = {name:'tagName'} + scope.createTag(); + expect(user.tags).to.have.length(1); + expect(user.tags[0].name).to.eql('tagName'); + expect(user.tags[0]).to.have.property('id'); + }); + + it('toggles tag filtering', inject(function(Shared){ + var tag = {id: Shared.uuid(), name: 'myTag'}; + scope.toggleFilter(tag); + expect(user.filters[tag.id]).to.eql(true); + scope.toggleFilter(tag); + expect(user.filters[tag.id]).to.eql(false); + })); + }); + + describe('updateTaskFilter', function(){ + it('updatest user\'s filter query with the value of filterQuery', function () { + scope.filterQuery = 'task'; + scope.updateTaskFilter(); + + expect(user.filterQuery).to.eql(scope.filterQuery); + }); + }); +}); diff --git a/test/spec/groupCtrlSpec.js b/test/spec/controllers/groupCtrlSpec.js similarity index 100% rename from test/spec/groupCtrlSpec.js rename to test/spec/controllers/groupCtrlSpec.js diff --git a/test/spec/hallCtrlSpec.js b/test/spec/controllers/hallCtrlSpec.js similarity index 100% rename from test/spec/hallCtrlSpec.js rename to test/spec/controllers/hallCtrlSpec.js diff --git a/test/spec/headerCtrlSpec.js b/test/spec/controllers/headerCtrlSpec.js similarity index 100% rename from test/spec/headerCtrlSpec.js rename to test/spec/controllers/headerCtrlSpec.js diff --git a/test/spec/inventoryCtrlSpec.js b/test/spec/controllers/inventoryCtrlSpec.js similarity index 100% rename from test/spec/inventoryCtrlSpec.js rename to test/spec/controllers/inventoryCtrlSpec.js diff --git a/test/spec/rootCtrlSpec.js b/test/spec/controllers/rootCtrlSpec.js similarity index 100% rename from test/spec/rootCtrlSpec.js rename to test/spec/controllers/rootCtrlSpec.js diff --git a/test/spec/filters/largeRoundNumbersSpec.js b/test/spec/filters/largeRoundNumbersSpec.js new file mode 100644 index 0000000000..bb563b0a87 --- /dev/null +++ b/test/spec/filters/largeRoundNumbersSpec.js @@ -0,0 +1,33 @@ +describe('roundLargeNumbers', function() { + + beforeEach(module('habitrpg')); + + it('returns same number if less than 1000', inject(function(roundLargeNumbersFilter) { + for(var num = 0; num < 1000; num++) { + expect(roundLargeNumbersFilter(num)).to.eql(num); + }; + })); + + it('truncates number and appends "k" if number is 1000-999999', inject(function(roundLargeNumbersFilter) { + expect(roundLargeNumbersFilter(999.01)).to.eql("1.0k"); + expect(roundLargeNumbersFilter(1000)).to.eql("1.0k"); + expect(roundLargeNumbersFilter(3284.12)).to.eql("3.3k"); + expect(roundLargeNumbersFilter(52983.99)).to.eql("53.0k"); + expect(roundLargeNumbersFilter(452983.99)).to.eql("453.0k"); + expect(roundLargeNumbersFilter(999999)).to.eql("1000.0k"); + })); + + it('truncates number and appends "m" if number is 1000000-999999999', inject(function(roundLargeNumbersFilter) { + expect(roundLargeNumbersFilter(999999.01)).to.eql("1.0m"); + expect(roundLargeNumbersFilter(1000000)).to.eql("1.0m"); + expect(roundLargeNumbersFilter(3284124.12)).to.eql("3.3m"); + expect(roundLargeNumbersFilter(52983105.99)).to.eql("53.0m"); + expect(roundLargeNumbersFilter(452983410.99)).to.eql("453.0m"); + expect(roundLargeNumbersFilter(999999999)).to.eql("1000.0m"); + })); + + it('truncates number and appends b" if number is greater than 999999999', inject(function(roundLargeNumbersFilter) { + expect(roundLargeNumbersFilter(999999999.01)).to.eql("1.0b"); + expect(roundLargeNumbersFilter(1423985738.54)).to.eql("1.4b"); + })); +}); diff --git a/test/spec/filters/moneySpec.js b/test/spec/filters/moneySpec.js new file mode 100644 index 0000000000..6f557cc433 --- /dev/null +++ b/test/spec/filters/moneySpec.js @@ -0,0 +1,35 @@ +describe('filter', function() { + + beforeEach(module('habitrpg')); + + describe('gold', function() { + it('rounds down decimal values', inject(function(goldFilter) { + expect(goldFilter(10)).to.eql(10); + expect(goldFilter(10.0)).to.eql(10); + expect(goldFilter(10.1)).to.eql(10); + expect(goldFilter(10.2)).to.eql(10); + expect(goldFilter(10.3)).to.eql(10); + expect(goldFilter(10.4)).to.eql(10); + expect(goldFilter(10.5)).to.eql(10); + expect(goldFilter(10.6)).to.eql(10); + expect(goldFilter(10.7)).to.eql(10); + expect(goldFilter(10.8)).to.eql(10); + expect(goldFilter(10.9)).to.eql(10); + expect(goldFilter(11)).to.eql(11); + })); + }); + + describe('silver', function() { + it('converts decimal value of gold to silver', inject(function(silverFilter) { + expect(silverFilter(10)).to.be.closeTo(0, 1); + expect(silverFilter(10.01)).to.be.closeTo(1, 1); + expect(silverFilter(10.05)).to.be.closeTo(5, 1); + expect(silverFilter(10.17)).to.be.closeTo(17, 1); + expect(silverFilter(10.23)).to.be.closeTo(23, 1); + expect(silverFilter(10.25)).to.be.closeTo(25, 1); + expect(silverFilter(10.53)).to.be.closeTo(53, 1); + expect(silverFilter(10.75)).to.be.closeTo(75, 1); + expect(silverFilter(10.99)).to.be.closeTo(99, 1); + })); + }); +}); diff --git a/test/spec/filters/taskOrderingSpec.js b/test/spec/filters/taskOrderingSpec.js new file mode 100644 index 0000000000..989a56c7e8 --- /dev/null +++ b/test/spec/filters/taskOrderingSpec.js @@ -0,0 +1,54 @@ +'use strict'; + +describe('Task Ordering Filters', function() { + var filter + , orderBySpy = sinon.spy(); + + beforeEach(function() { + module(function($provide) { + $provide.value('orderByFilter', orderBySpy); + }); + inject(function($rootScope, $filter) { + filter = $filter; + }); + }); + + describe('conditionalOrderBy', function() { + describe('when the predicate is true', function() { + it('delegates the arguments to the orderBy filter', function() { + filter('conditionalOrderBy')('array', true, 'sortPredicate', 'reverseOrder'); + expect(orderBySpy).to.have.been.calledWith('array','sortPredicate','reverseOrder'); + }); + }); + + describe('when the predicate is false', function() { + it('returns the initial array', function() { + expect(filter('conditionalOrderBy')([1,2,3], false)).to.eql([1,2,3]); + }); + }); + }); + + describe('filterByTextAndNotes', function () { + it('returns undefined when no input given', function () { + expect(filter('filterByTextAndNotes')()).to.eql(undefined); + }); + + it('returns input if term is not a string', function () { + var input = [1, 2, 3]; + expect(filter('filterByTextAndNotes')(input, '')).to.eql(input); + expect(filter('filterByTextAndNotes')(input, undefined)).to.eql(input); + expect(filter('filterByTextAndNotes')(input, [])).to.eql(input); + expect(filter('filterByTextAndNotes')(input, new Date())).to.eql(input); + }); + + it('filters items by notes and text', function () { + var tasks = [ + { text: 'foo' }, + { text: 'foo', notes: 'bar' } + ]; + + expect(filter('filterByTextAndNotes')(tasks, 'bar')).to.eql([tasks[1]]); + expect(filter('filterByTextAndNotes')(tasks, 'foo')).to.eql([tasks[0], tasks[1]]); + }); + }); +}); diff --git a/test/spec/filtersCtrlSpec.js b/test/spec/filtersCtrlSpec.js deleted file mode 100644 index edeaf723e3..0000000000 --- a/test/spec/filtersCtrlSpec.js +++ /dev/null @@ -1,28 +0,0 @@ -'use strict'; - -describe('Filters Controller', function() { - var scope, user; - - beforeEach(inject(function($rootScope, $controller, Shared) { - user = specHelper.newUser(); - Shared.wrap(user); - scope = $rootScope.$new(); - $controller('FiltersCtrl', {$scope: scope, User: {user: user}}); - })); - - it('creates a tag', function(){ - scope._newTag = {name:'tagName'} - scope.createTag(); - expect(user.tags).to.have.length(1); - expect(user.tags[0].name).to.eql('tagName'); - expect(user.tags[0]).to.have.property('id'); - }); - - it('toggles tag filtering', inject(function(Shared){ - var tag = {id: Shared.uuid(), name: 'myTag'}; - scope.toggleFilter(tag); - expect(user.filters[tag.id]).to.eql(true); - scope.toggleFilter(tag); - expect(user.filters[tag.id]).to.eql(false); - })) -}); diff --git a/test/spec/filtersSpec.js b/test/spec/filtersSpec.js deleted file mode 100644 index b6d426ccc1..0000000000 --- a/test/spec/filtersSpec.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict'; - -describe('Custom Filters', function() { - var filter - , orderBySpy = sinon.spy(); - - beforeEach(function() { - module(function($provide) { - $provide.value('orderByFilter', orderBySpy); - }); - inject(function($rootScope, $filter) { - filter = $filter; - }); - }); - - describe('conditionalOrderBy', function() { - describe('when the predicate is true', function() { - it('delegates the arguments to the orderBy filter', function() { - filter('conditionalOrderBy')('array', true, 'sortPredicate', 'reverseOrder'); - expect(orderBySpy).to.have.been.calledWith('array','sortPredicate','reverseOrder'); - }); - }); - - describe('when the predicate is false', function() { - it('returns the initial array', function() { - expect(filter('conditionalOrderBy')([1,2,3], false)).to.eql([1,2,3]); - }); - }); - }); -}); diff --git a/test/spec/mock/mixpanelMock.js b/test/spec/mocks/mixpanelMock.js similarity index 100% rename from test/spec/mock/mixpanelMock.js rename to test/spec/mocks/mixpanelMock.js diff --git a/test/spec/groupServicesSpec.js b/test/spec/services/groupServicesSpec.js similarity index 100% rename from test/spec/groupServicesSpec.js rename to test/spec/services/groupServicesSpec.js diff --git a/test/spec/memberServicesSpec.js b/test/spec/services/memberServicesSpec.js similarity index 100% rename from test/spec/memberServicesSpec.js rename to test/spec/services/memberServicesSpec.js diff --git a/test/spec/notificationServicesSpec.js b/test/spec/services/notificationServicesSpec.js similarity index 100% rename from test/spec/notificationServicesSpec.js rename to test/spec/services/notificationServicesSpec.js diff --git a/test/spec/userServicesSpec.js b/test/spec/services/userServicesSpec.js similarity index 100% rename from test/spec/userServicesSpec.js rename to test/spec/services/userServicesSpec.js diff --git a/website/public/css/filters.styl b/website/public/css/filters.styl index ca0245c97a..5ba86cd853 100644 --- a/website/public/css/filters.styl +++ b/website/public/css/filters.styl @@ -35,3 +35,6 @@ margin-right: 0.618em @extend $hrpg-button-with-input hrpg-button-color-mixin($color-options-submenu) + .filters-search + margin-bottom: 0.618em + max-width: 180px diff --git a/website/public/css/global-colors.styl b/website/public/css/global-colors.styl index 60dfa28db7..bddce574b7 100644 --- a/website/public/css/global-colors.styl +++ b/website/public/css/global-colors.styl @@ -20,7 +20,7 @@ $color-toolbar = lighten($color-herobox, 70%) $color-options-menu = lighten($color-herobox, 85%) $color-options-submenu = lighten($color-herobox, 75%) // Button colors -$color-button-style-one = $best +$color-button-highlight = $best // Task background $color-tasks = lighten($color-herobox, 65%) // Task filter colors @@ -37,4 +37,4 @@ $color-contributor-seven = #00aaff $color-contributor-mod = #130ead $color-contributor-staff = #88108f $color-contributor-npc = #000 -$color-contributor-npc-font = #00FF00 \ No newline at end of file +$color-contributor-npc-font = #00FF00 diff --git a/website/public/css/global-modules.styl b/website/public/css/global-modules.styl index 8e8908aac6..9aae889d4c 100644 --- a/website/public/css/global-modules.styl +++ b/website/public/css/global-modules.styl @@ -13,14 +13,21 @@ hrpg-text-shadow-mixin($hrpg-text-shadow-base-color) 1px 1px 1px darken($hrpg-text-shadow-base-color,70%); // Buttons // The !important declarations override Bootstrap -hrpg-button-color-mixin($hrpg-button-color) +// buttons with the .highlight class pass highlight=true to this mixin +hrpg-button-color-mixin($hrpg-button-color, highlight=false) + // this case covers button elements with the highlight class + // like the subscribe button, for which the following selectors do not apply + if highlight==true + border-color: darken($color-button-highlight, 16.18%) !important + background-color: $color-button-highlight !important + & + color: darken($hrpg-button-color, 70%) !important + hrpg-anchor-button-color-mixin($color-button-highlight) > a, > button - background-color: $hrpg-button-color !important - &:active - background-color: darken($hrpg-button-color, 61.18%) !important - @media screen and (min-width:768px) - &:hover - background-color: darken($hrpg-button-color, 2.36%) !important + if highlight==true + hrpg-anchor-button-color-mixin($color-button-highlight) + else + hrpg-anchor-button-color-mixin($hrpg-button-color) > a, > button, > input, textarea color: darken($hrpg-button-color, 70%) !important border-color: darken($hrpg-button-color, 16.18%) !important @@ -38,6 +45,9 @@ hrpg-button-color-mixin($hrpg-button-color) &:active background-color: darken($hrpg-button-color, 16.18%) !important; > a:nth-of-type(2) + if highlight==true + border-left: 1px solid darken($color-button-highlight, 3.82%) !important + else border-left: 1px solid darken($hrpg-button-color, 3.82%) !important > div @media screen and (min-width:768px) @@ -69,6 +79,13 @@ hrpg-button-color-mixin($hrpg-button-color) color: #fff !important; span color: #fff !important; +hrpg-anchor-button-color-mixin($hrpg-button-color) + background-color: $hrpg-button-color !important + &:active + background-color: darken($hrpg-button-color, 61.18%) !important + @media screen and (min-width:768px) + &:hover + background-color: darken($hrpg-button-color, 2.36%) !important $hrpg-button-master list-style: none > a, > button, > input, label::after @@ -89,9 +106,8 @@ $hrpg-button > a, > button, > input, label::after border: 1px solid #ccc !important border-radius: 0.382em !important -$hrpg-button-call-to-action - @extend $hrpg-button - hrpg-button-color-mixin($color-button-style-one) + .highlight + hrpg-button-color-mixin($color-toolbar, true) $hrpg-button-toggle @extend $hrpg-button-master border: 1px solid #ccc !important @@ -100,6 +116,8 @@ $hrpg-button-toggle border-radius: 0.382em 0em 0em 0.382em !important > a:last-of-type border-radius: 0em 0.382em 0.382em 0em !important + &.highlight + hrpg-button-color-mixin($color-toolbar, true) // Input + Button $hrpg-button-with-input @extend $hrpg-button-master diff --git a/website/public/css/menu.styl b/website/public/css/menu.styl index 39c12c6b95..e9700a2a42 100644 --- a/website/public/css/menu.styl +++ b/website/public/css/menu.styl @@ -85,6 +85,9 @@ @extend $hrpg-button-toggle @extend $hrpg-modal-dropdown-right hrpg-button-color-mixin($color-toolbar) + &.highlight + > a span.glyphicon + margin-right: 0.382em !important .toolbar-button @extend $hrpg-button hrpg-button-color-mixin($color-toolbar) @@ -163,7 +166,7 @@ @extend $hrpg-button hrpg-button-color-mixin(lighten($color-toolbar,32.8%)) .toolbar-subscribe-button, .toolbar-controls .toolbar-subscribe-button - @extend $hrpg-button-call-to-action + @extend $hrpg-button @media screen and (max-width:768px) .toolbar-toggle display: none diff --git a/website/public/css/tasks.styl b/website/public/css/tasks.styl index 5c8dc6d319..de185d6315 100644 --- a/website/public/css/tasks.styl +++ b/website/public/css/tasks.styl @@ -12,7 +12,7 @@ for $stage in $stages .color-{$stage[0]}:not(.completed) background-color: $stage[1] border: 1px solid shade($stage[1],10%) - .priority-multiplier, .task-attributes, .repeat-days + .priority-multiplier, .task-attributes, .repeat-days, .repeat-frequency li hrpg-button-color-mixin($stage[1]) button @@ -63,7 +63,7 @@ for $stage in $stages color: darken($completed,30%) background-color: $completed border: 1px solid shade($completed,10%) - .priority-multiplier, .task-attributes, .repeat-days + .priority-multiplier, .task-attributes, .repeat-days, .repeat-frequency li hrpg-button-color-mixin($completed) button @@ -215,7 +215,6 @@ for $stage in $stages border: 1px solid #aaa border-radius: 0.382em padding-left: 0.618em - background-color: #fff !important -webkit-appearance: none -moz-appearance: none appearance: none @@ -410,6 +409,10 @@ form padding: 0 0 1em margin-bottom: 1em + button.advanced-options-toggle + display: block; + width: 100%; + background: none; .option-title font-size: 1em margin: 0.5em 0 0.5em @@ -507,7 +510,7 @@ form form padding-bottom: 1em - .priority-multiplier, .task-attributes, .repeat-days + .priority-multiplier, .task-attributes, .repeat-days, .repeat-frequency text-align: center li @extend $hrpg-button @@ -517,6 +520,7 @@ form &:last-of-type margin-right: 0 .repeat-days + padding-bottom: 1em li button min-width: 2.5em @@ -524,6 +528,11 @@ form text-align: center @extend $hrpg-button +// Dailies +.dailies + .repeat-weekly + padding-bottom: 1em + // Habits – task button styles (+ -) .habits .task-actions @@ -580,7 +589,7 @@ form margin-top: 0.5em span margin-right: 0.5em - > form + > .checklist-form li @extend $hrpg-button-with-input @extend $clearfix diff --git a/website/public/emails/images/PROMO-Enchanted-Armoire-v1.png b/website/public/emails/images/PROMO-Enchanted-Armoire-v1.png new file mode 100644 index 0000000000..9a42b70e42 Binary files /dev/null and b/website/public/emails/images/PROMO-Enchanted-Armoire-v1.png differ diff --git a/website/public/front/home.html b/website/public/front/home.html deleted file mode 100644 index fd4099c1ef..0000000000 --- a/website/public/front/home.html +++ /dev/null @@ -1,810 +0,0 @@ - - - - - - HabitRPG | Gamify Your Life - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-

Motivate yourself and your team!

- -
-

Join 200,000 players making it fun to achieve goals! - -

-
-
Featured in
- - - - - - - - -
-
- -
-
-
-

HabitRPG is a free habit building and productivity app that treats your real life like a game. With in-game rewards and punishments to motivate you and a strong social network to inspire you, HabitRPG can help you achieve your goals to become healthy, hard-working, and happy.

-
- - - - -
-

Players use HabitRPG to manage...

-
- - -
-
- -
-

Join us on a mini-quest!

- - -
- -
-

Complete a task to earn gold!

-
-

-
- - -
- - - -

Spend gold on virtual and real-life rewards.

Instant rewards keep you motivated!

- -
- - - -
-
- - - -
-
- -
-

As you stay productive, you unlock new content!

-
- - - - - - -
- -
- - - - -
-
- -
- - - -

Achieve your goals and level up.

Unlock new motivational tools, such as pet collecting, random rewards, spell-casting, and more!

- -
- - - -
-
- -
-

Miss a daily goal?

- - - -
-
- -
-
-
 
-
-
-

Lose health!

-

Break bad habits and procrastination cycles with immediate consequences.

- -
- - - -
-
- -
-

Battle monsters with your friends!

- - -
-
-
-
-
-
 
-
-
-
- -
-
-
-
-
-
-
 
-
-
-
-
- -

If you slack off, they all get hurt!

-

Playing with your friends keeps you accountable for your tasks.
Issue each other Challenges to complete a goal together!

- -
- - - -
-
- - -
-

We also feature...

- -
-
- -
- - -
-
-
-

Pets and Mounts

-

Eggs and items drop when you complete your tasks. Be as productive as possible to collect pets and mounts!

-
-
-
-
- - -
- -
- - -
-
-
-

Achievement Badges

-

Do something totally awesome? Get a badge and show it off!

-
-
-
- -
-
- -
- - - - -
-
-
-

Equipment and extras

-

Buy limited edition equipment, potions, and other virtual goodies in our Market with your task rewards!

-
-
-
-
- -
-
- -
- -
-
-
-

Social play

-

Join common-interest groups with like-minded people.

Create Challenges to compete against other users.

-
-
-
-
-
- - - -
-
-
-
-

Join 200,000 players making it fun to achieve goals!

-
-
-
-
- -
-
-
- -
- - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/website/public/front/more-info.html b/website/public/front/more-info.html deleted file mode 100644 index 5189db93c1..0000000000 --- a/website/public/front/more-info.html +++ /dev/null @@ -1,28 +0,0 @@ -HabitRPG | Gamify Your Life - - - - - - - - - - - - - -
HabitRPG logo

A free habit building app that treats your life like a game.



The problem with most productivity apps on the market is that they provide no incentive to continue using them. HabitRPG fixes this by making habit building fun! By rewarding you for your successes and penalizing you for slip-ups, HabitRPG provides external motivation for completing your day-to-day activities.

Instant Gratification

Whenever you reinforce a positive habit, complete a daily task, or take care of an old to-do, HabitRPG immediately rewards you with experience points and gold. As you gain experience, you can level up, increasing your stats and unlocking more features, like classes and pets. Gold can be spent on in-game items that change your experience or personalized rewards you've created for motivation. When even the smallest successes provide you with an immediate reward, you're less likely to procrastinate. 

Consequences

Whenever you indulge in a bad habit or fail to complete one of your daily tasks, you lose health. If your health drops too low, you die and lose some of the progress you've made. By providing immediate consequences, HabitRPG can help break bad habits and procrastination cycles before they cause real-world problems. 

Accountability

With an active community, HabitRPG provides the accountability you need to stay on task. With the party system, you can bring in a group of your closest friends to cheer you on. The guild system allows you to find people with similar interests or obstacles, so you can share your goals and swap tips on how to tackle your problems. On HabitRPG, the community means that you have both the support and the accountability you need to succeed.

\ No newline at end of file diff --git a/website/public/front/statichome.html b/website/public/front/statichome.html deleted file mode 100644 index 3bf04f348f..0000000000 --- a/website/public/front/statichome.html +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/website/public/js/controllers/challengesCtrl.js b/website/public/js/controllers/challengesCtrl.js index 24985e85ac..770d736803 100644 --- a/website/public/js/controllers/challengesCtrl.js +++ b/website/public/js/controllers/challengesCtrl.js @@ -19,7 +19,9 @@ habitrpg.controller("ChallengesCtrl", ['$rootScope','$scope', 'Shared', 'User', $scope.challenges = challenges; $scope.groupsFilter = _.uniq(_.pluck(challenges, 'group'), function(g){return g._id}); $scope.search = { - group: _.transform($scope.groups, function(m,g){m[g._id]=true;}) + group: _.transform($scope.groups, function(m,g){m[g._id]=true;}), + _isMember: "either", + _isOwner: "either" }; }); } @@ -254,18 +256,10 @@ habitrpg.controller("ChallengesCtrl", ['$rootScope','$scope', 'Shared', 'User', // 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)); + if (!$scope.search) return true; + + return _shouldShowChallenge(chal); } $scope.$watch('newChallenge.group', function(gid){ @@ -286,4 +280,16 @@ habitrpg.controller("ChallengesCtrl", ['$rootScope','$scope', 'Shared', 'User', $scope.shouldShow = function(task, list, prefs){ return true; }; + + function _shouldShowChallenge(chal) { + // Have to check that the leader object exists first in the + // case where a challenge's leader deletes their account + var userIsOwner = (chal.leader && chal.leader._id) == User.user.id; + + var groupSelected = $scope.search.group[chal.group._id]; + var checkOwner = $scope.search._isOwner === 'either' || (userIsOwner === $scope.search._isOwner); + var checkMember = $scope.search._isMember === 'either' || (chal._isMember === $scope.search._isMember); + + return groupSelected && checkOwner && checkMember; + } }]); diff --git a/website/public/js/controllers/filtersCtrl.js b/website/public/js/controllers/filtersCtrl.js index b1204b86d3..cfdc45658d 100644 --- a/website/public/js/controllers/filtersCtrl.js +++ b/website/public/js/controllers/filtersCtrl.js @@ -5,6 +5,7 @@ habitrpg.controller("FiltersCtrl", ['$scope', '$rootScope', 'User', 'Shared', var user = User.user; $scope._editing = false; $scope._newTag = {name:''}; + $scope.filterQuery = ''; var tagsSnap; // used to compare which tags need updating @@ -30,6 +31,11 @@ habitrpg.controller("FiltersCtrl", ['$scope', '$rootScope', 'User', 'Shared', // User.save(); }; + $scope.updateTaskFilter = function(){ + user.filterQuery = $scope.filterQuery; + }; + $scope.updateTaskFilter(); + $scope.createTag = function() { User.user.ops.addTag({body:{name:$scope._newTag.name, id:Shared.uuid()}}); $scope._newTag.name = ''; diff --git a/website/public/js/controllers/tasksCtrl.js b/website/public/js/controllers/tasksCtrl.js index 3bb0df8c31..634847ad1a 100644 --- a/website/public/js/controllers/tasksCtrl.js +++ b/website/public/js/controllers/tasksCtrl.js @@ -4,6 +4,7 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','N function($scope, $rootScope, $location, User, Notification, $http, ApiUrl, $timeout, Shared, Guide) { $scope.obj = User.user; // used for task-lists $scope.user = User.user; + $scope.armoireCount = function(gear) { return Shared.countArmoire(gear); }; @@ -131,6 +132,19 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','N */ $scope._today = moment().add({days: 1}); + /* + ------------------------ + Dailies + ------------------------ + */ + + $scope.openDatePicker = function($event, task) { + $event.preventDefault(); + $event.stopPropagation(); + + task._isDatePickerOpen = !task._isDatePickerOpen; + } + /* ------------------------ Checklists @@ -216,7 +230,7 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','N $scope.shouldShow = function(task, list, prefs){ if (task._editing) // never hide a task while being edited return true; - var shouldDo = task.type == 'daily' ? habitrpgShared.shouldDo(new Date, task.repeat, prefs) : true; + var shouldDo = task.type == 'daily' ? habitrpgShared.shouldDo(new Date, task, prefs) : true; switch (list.view) { case "yellowred": // Habits return task.value < 1; diff --git a/website/public/js/filters/filters.js b/website/public/js/filters/filters.js deleted file mode 100644 index 4578c1a5db..0000000000 --- a/website/public/js/filters/filters.js +++ /dev/null @@ -1,31 +0,0 @@ -angular.module('habitrpg') - .filter('gold', function () { - return function (gp) { - return Math.floor(gp); - } - }) - .filter('silver', function () { - return function (gp) { - return Math.floor((gp - Math.floor(gp))*100); - } - }) - .filter('htmlDecode',function(){ - return function(html){ - return $('
').html(html).text(); - } - }) - .filter('goldRoundThousandsToK', function(){ - return function (gp) { - return (gp > 999999999) ? (gp / Math.pow(10, 9)).toFixed(1) + "b" : - (gp > 999999) ? (gp / Math.pow(10, 6)).toFixed(1) + "m" : - (gp > 999) ? (gp / Math.pow(10, 3)).toFixed(1) + "k" : gp; - } - }) - .filter('conditionalOrderBy', ['$filter', function($filter) { - return function (array, predicate, sortPredicate, reverseOrder) { - if (predicate) { - return $filter('orderBy')(array, sortPredicate, reverseOrder); - } - return array; - }; - }]); diff --git a/website/public/js/filters/money.js b/website/public/js/filters/money.js new file mode 100644 index 0000000000..ae22604d49 --- /dev/null +++ b/website/public/js/filters/money.js @@ -0,0 +1,11 @@ +angular.module('habitrpg') + .filter('gold', function () { + return function (gp) { + return Math.floor(gp); + } + }) + .filter('silver', function () { + return function (gp) { + return Math.floor((gp - Math.floor(gp))*100); + } + }); diff --git a/website/public/js/filters/roundLargeNumbers.js b/website/public/js/filters/roundLargeNumbers.js new file mode 100644 index 0000000000..9eebd6c546 --- /dev/null +++ b/website/public/js/filters/roundLargeNumbers.js @@ -0,0 +1,30 @@ +angular.module('habitrpg') + .filter('roundLargeNumbers', function(){ + return function (num) { + return _calculateRoundedNumber(num); + } + }); + +function _calculateRoundedNumber(num) { + if (num > 999999999) { + return _convertToBillion(num); + } else if (num > 999999) { + return _convertToMillion(num); + } else if (num > 999) { + return _convertToThousand(num); + } else { + return num; + } +} + +function _convertToThousand(num) { + return (num / Math.pow(10, 3)).toFixed(1) + "k"; +} + +function _convertToMillion(num) { + return (num / Math.pow(10, 6)).toFixed(1) + "m"; +} + +function _convertToBillion(num) { + return (num / Math.pow(10, 9)).toFixed(1) + "b"; +} diff --git a/website/public/js/filters/taskOrdering.js b/website/public/js/filters/taskOrdering.js new file mode 100644 index 0000000000..42abd471c0 --- /dev/null +++ b/website/public/js/filters/taskOrdering.js @@ -0,0 +1,30 @@ +angular.module('habitrpg') + .filter('conditionalOrderBy', ['$filter', function($filter) { + return function (array, predicate, sortPredicate, reverseOrder) { + if (predicate) { + return $filter('orderBy')(array, sortPredicate, reverseOrder); + } + return array; + }; + }]) + .filter('filterByTextAndNotes', ['$filter', function($filter) { + return function (input, term) { + if (!input) return; + + if (!angular.isString(term) || term.legth === 0) { + return input; + } + + term = new RegExp(term, 'i'); + + var result = []; + + for (var i = 0; i < input.length; i++) { + if (term.test(input[i].text) || term.test(input[i].notes)) { + result.push(input[i]); + } + } + + return result; + }; + }]); diff --git a/website/public/manifest.json b/website/public/manifest.json index 7436040c57..d7afa0e961 100644 --- a/website/public/manifest.json +++ b/website/public/manifest.json @@ -38,6 +38,7 @@ "js/app.js", "common/script/public/config.js", + "js/services/sharedServices.js", "js/services/notificationServices.js", "common/script/public/userServices.js", @@ -48,7 +49,9 @@ "js/services/challengeServices.js", "js/services/paymentServices.js", - "js/filters/filters.js", + "js/filters/money.js", + "js/filters/roundLargeNumbers.js", + "js/filters/taskOrdering.js", "js/directives/focus-me.directive.js", "js/directives/from-now.directive.js", diff --git a/website/src/models/task.js b/website/src/models/task.js index 2646263e80..8ae24e353a 100644 --- a/website/src/models/task.js +++ b/website/src/models/task.js @@ -8,6 +8,7 @@ var mongoose = require("mongoose"); var Schema = mongoose.Schema; var shared = require('../../../common'); var _ = require('lodash'); +var moment = require('moment'); // Task Schema // ----------- @@ -50,10 +51,13 @@ var checklist = [{ var DailySchema = new Schema( _.defaults({ - type: {type:String, 'default': 'daily'}, + type: {type: String, 'default': 'daily'}, + frequency: {type: String, 'default': 'weekly', enum: ['daily', 'weekly']}, + everyX: {type: Number, 'default': 1}, // e.g. once every X weeks + startDate: {type: Date, 'default': moment().startOf('day').toDate()}, history: Array, completed: {type: Boolean, 'default': false}, - repeat: { + repeat: { // used only for 'weekly' frequency, m: {type: Boolean, 'default': true}, t: {type: Boolean, 'default': true}, w: {type: Boolean, 'default': true}, diff --git a/website/views/index.jade b/website/views/index.jade index cf70ebccdf..52d464c1b0 100644 --- a/website/views/index.jade +++ b/website/views/index.jade @@ -38,7 +38,7 @@ html(ng-app="habitrpg", ng-controller="RootCtrl", ng-class='{"applying-action":a include ./shared/header/menu include ./shared/modals/index include ./shared/header/header - include ./shared/tasks/lists + include ./shared/tasks/index include ./main/index include ./options/index diff --git a/website/views/main/filters.jade b/website/views/main/filters.jade index 20e244cce2..b72d424364 100644 --- a/website/views/main/filters.jade +++ b/website/views/main/filters.jade @@ -1,6 +1,10 @@ .container-fluid .row .filters(ng-controller='FiltersCtrl') + .input-group.input-group-sm.filters-search.pull-right + input.form-control(type='text', placeholder=env.t('search'), ng-model='filterQuery', ng-change='updateTaskFilter()', ng-model-options='{ debounce: 250 }') + .input-group-addon + .glyphicon.glyphicon-search ul.filters-controls li=env.t('tags') //- Edit button diff --git a/website/views/options/social/boss.jade b/website/views/options/social/boss.jade index 1574f648e8..6411462bbe 100644 --- a/website/views/options/social/boss.jade +++ b/website/views/options/social/boss.jade @@ -52,13 +52,13 @@ mixin boss(tavern, mobile) .meter.health .bar(style='width: {{Shared.percent(progress.hp, boss.hp)}}%;') span.meter-text.value - | {{Math.ceil(progress.hp) | goldRoundThousandsToK}} / {{boss.hp | goldRoundThousandsToK}} + | {{Math.ceil(progress.hp) | roundLargeNumbers}} / {{boss.hp | roundLargeNumbers}} .meter-label(tooltip='Rage', ng-if='boss.rage') span.glyphicon.glyphicon-fire .meter.mana(ng-if='boss.rage',popover="{{::boss.rage.description()}}",popover-title="{{::boss.rage.title()}}",popover-trigger='mouseenter',popover-placement='right') .bar(style='width: {{Shared.percent(progress.rage, boss.rage.value)}}%;') span.meter-text.value - | {{Math.ceil(progress.rage) | goldRoundThousandsToK}} / {{boss.rage.value | goldRoundThousandsToK}} + | {{Math.ceil(progress.rage) | roundLargeNumbers}} / {{boss.rage.value | roundLargeNumbers}} div(ng-if='::Content.quests[group.quest.key].collect') div(class="quest_{{::group.quest.key}}") h4=env.t('collected') + ':' diff --git a/website/views/options/social/challenges.jade b/website/views/options/social/challenges.jade index b2e0d2c1a4..4bfa55c791 100644 --- a/website/views/options/social/challenges.jade +++ b/website/views/options/social/challenges.jade @@ -81,16 +81,29 @@ script(type='text/ng-template', id='partials/options.social.challenges.html') h4=env.t('membership') .radio label - input(type='radio', name='search-participation-radio', ng-click='search._isMember = true') + input(type='radio', name='search-participation-radio', ng-model='search._isMember', ng-value='true', ng-change='filterChallenges') =env.t('participating') .radio label - input(type='radio', name='search-participation-radio', ng-click='search._isMember = false') + input(type='radio', name='search-participation-radio', ng-model='search._isMember', ng-value='false', ng-change='filterChallenges') =env.t('notParticipating') .radio label - input(type='radio', name='search-participation-radio', ng-click='search._isMember = undefined', checked='checked') + input(type='radio', name='search-participation-radio', ng-model='search._isMember', value='either', ng-change='filterChallenges') =env.t('either') + h4=env.t('challengedOwnedFilterHeader') + .radio + label + input(type='radio', name='search-owner-radio', ng-model='search._isOwner', ng-value='true', ng-change='filterChallenges') + =env.t('challengedOwnedFilter') + .radio + label + input(type='radio', name='search-owner-radio', ng-model='search._isOwner', ng-value='false', ng-change='filterChallenges') + =env.t('challengedNotOwnedFilter') + .radio + label + input(type='radio', name='search-owner-radio', ng-model='search._isOwner', value='either', ng-change='filterChallenges', checked='checked') + =env.t('challengedEitherOwnedFilter') .col-md-10 a.btn.btn-info#back-to-challenges(ng-show="cid", ui-sref='options.social.challenges', ui-sref-opts='{reload: true}') | Back to all challenges diff --git a/website/views/shared/header/menu.jade b/website/views/shared/header/menu.jade index ebfdd6a6ff..f0dd838f81 100644 --- a/website/views/shared/header/menu.jade +++ b/website/views/shared/header/menu.jade @@ -146,8 +146,9 @@ nav.toolbar(ng-controller='AuthCtrl', ng-class='{active: isToolbarHidden}') a(target="_blank" ng-href='http://data.habitrpg.com?uuid={{user._id}}')=env.t('dataTool') li a(ui-sref='options.settings.export')=env.t('exportData') - li.toolbar-button-dropdown + li.toolbar-button-dropdown.highlight a(target="_blank" href='http://habitrpg.wikia.com/wiki/') + span.glyphicon.glyphicon-question-sign span=env.t('help') a(ng-click='expandMenu("help")', ng-class='{active: _expandedMenu == "help"}') span ☰ @@ -169,7 +170,7 @@ nav.toolbar(ng-controller='AuthCtrl', ng-class='{active: isToolbarHidden}') a(ng-click='showTour()', popover-placement='right', popover-trigger='mouseenter', popover=env.t('restartTour'))= env.t('showTour') ul.toolbar-subscribe(ng-if='!user.purchased.plan.customerId') li.toolbar-subscribe-button - button(ui-sref='options.settings.subscription',popover-trigger='mouseenter',popover-placement='bottom',popover-title=env.t('subscriptions'),popover=env.t('subDescription'),popover-append-to-body='true')=env.t('subscribe') + button.highlight(ui-sref='options.settings.subscription',popover-trigger='mouseenter',popover-placement='bottom',popover-title=env.t('subscriptions'),popover=env.t('subDescription'),popover-append-to-body='true')=env.t('subscribe') ul.toolbar-options li.toolbar-notifs a(ng-click='expandMenu("notifs")') @@ -264,7 +265,7 @@ nav.toolbar(ng-controller='AuthCtrl', ng-class='{active: isToolbarHidden}') span.gem-text {{user.balance * 4 | number:0}} li.toolbar-currency.gold(popover=env.t('gold') + ' ({{Shared.gold(user.stats.gp)}})', popover-placement='bottom',popover-trigger='mouseenter') span.shop_gold - span {{Shared.gold(user.stats.gp) | goldRoundThousandsToK}} + span {{Shared.gold(user.stats.gp) | roundLargeNumbers}} li.toolbar-currency.silver(popover=env.t('silver'), popover-placement='bottom',popover-trigger='mouseenter') span.shop_silver span {{Shared.silver(user.stats.gp)}} diff --git a/website/views/shared/new-stuff.jade b/website/views/shared/new-stuff.jade index ec2703ca20..1b86e5b522 100644 --- a/website/views/shared/new-stuff.jade +++ b/website/views/shared/new-stuff.jade @@ -1,33 +1,65 @@ -h5 6/1/2015 - NEW EQUIPMENT: THE ENCHANTED ARMOIRE, JUNE BACKGROUNDS, AND NEW MOUNT POSITIONING! +h5 6/11/2015 - REPEATING TASKS, START DATE, AND MOBILE APP UPDATES! + p + br + p.small.muted by Blade and fallenpanda hr tr td - .promo_enchanted_armoire.pull-right - h5 New Equipment: The Enchanted Armoire! - p Now after you achieve Ultimate Gear, you'll unlock a new Reward: THE ENCHANTED ARMOIRE! + h5 New Repeat Option for Dailies + p Dailies now have a new Advanced Option: Repeat Every X Days. You've wanted this feature for a long time, and it's finally here! br - p Click on the Enchanted Armoire, a 100 GP Reward in the Rewards Column, for a random chance at special Equipment! It may also give you random XP or food items. We'll be adding new equipment to it every month, but even when you've exhausted the current supply, you can keep clicking for a chance at food and XP. + p First, please note that this new option is OPT-IN only. We won't make any changes to your preexisting Dailies without you knowing it. We wouldn't do that! br - p Now go spend all that accumulated Gold! May the Random Number Generator smile upon you... - p.small.muted by Lemoness and SabreCat - p.small.muted Art by Kiwibot, Starsystemic, UncommonCriminal, Zoebeagle, and Andrews38 + p That being said, here are the new features: tr td - .background_island_waterfalls.pull-right - h5 June Backgrounds Revealed - p There are three new avatar backgrounds in the Background Shop! Now your avatar can paddle a Drifting Raft, float through a sea of Shimmery Bubbles, or picnic near Island Waterfalls! - p.small.muted by (in order): Teto is Great, beffymaroo, and UncommonCriminal + h5 Repeating Tasks + p Use the "Every X Days" function under Dailies Advanced Options to create tasks that repeat after a certain number of days have passed, whether every 2 days, every 15 days, every 30 days... You choose the number that works for you! + br + p These Dailies are only due on those given dates. Need to pay your rent every 30 days? Take medicine every other day? Water your plants every 4 days? No longer a problem. tr td - h5 New Mount Positioning! - p The mount positioning has been fixed for all the base mounts where it looked like the avatar was riding extreme sidesaddle. Now avatars sit properly, no longer clinging to the sides of their mounts for dear life. - p.small.muted by Kiwibot, Lemoness, and SabreCat + h5 Start Date + p Dailies now have a Start Date. They will not be due before this date. This means that if you want to add a new Daily while you're thinking about it, but not have it be due until later, you can achieve that by setting a future Start Date! + tr + td + h5 Mobile App Updates + p New Android and iOS updates are available to support this feature. Please, update your apps before using it, or the new repeating Dailies will not display normally on the mobile apps! + tr + td + h5 Other Notes + p For a short period of time, the Data Display Tool will not be able to calculate damage correctly for Repeat Every X Dailies. We'll get that updated very soon so that it will be accurate again! + br + p If you still have questions about Repeat Every X Dailies, don't hesitate to ask in the Newbies Guild! hr a(href='/static/old-news', target='_blank') Read older news mixin oldNews + h5 6/1/2015 - NEW EQUIPMENT: THE ENCHANTED ARMOIRE, JUNE BACKGROUNDS, AND NEW MOUNT POSITIONING! + tr + td + .promo_enchanted_armoire.pull-right + h5 New Equipment: The Enchanted Armoire! + p Now after you achieve Ultimate Gear, you'll unlock a new Reward: THE ENCHANTED ARMOIRE! + br + p Click on the Enchanted Armoire, a 100 GP Reward in the Rewards Column, for a random chance at special Equipment! It may also give you random XP or food items. We'll be adding new equipment to it every month, but even when you've exhausted the current supply, you can keep clicking for a chance at food and XP. + br + p Now go spend all that accumulated Gold! May the Random Number Generator smile upon you... + p.small.muted by Lemoness and SabreCat + p.small.muted Art by Kiwibot, Starsystemic, UncommonCriminal, Zoebeagle, and Andrews38 + tr + td + .background_island_waterfalls.pull-right + h5 June Backgrounds Revealed + p There are three new avatar backgrounds in the Background Shop! Now your avatar can paddle a Drifting Raft, float through a sea of Shimmery Bubbles, or picnic near Island Waterfalls! + p.small.muted by (in order): Teto is Great, beffymaroo, and UncommonCriminal + tr + td + h5 New Mount Positioning! + p The mount positioning has been fixed for all the base mounts where it looked like the avatar was riding extreme sidesaddle. Now avatars sit properly, no longer clinging to the sides of their mounts for dear life. + p.small.muted by Kiwibot, Lemoness, and SabreCat h5 6/1/2015 - JUNE MYSTERY ITEM! tr td diff --git a/website/views/shared/tasks/edit/advanced_options.jade b/website/views/shared/tasks/edit/advanced_options.jade new file mode 100644 index 0000000000..a34051ddf5 --- /dev/null +++ b/website/views/shared/tasks/edit/advanced_options.jade @@ -0,0 +1,70 @@ +div(ng-if='::task.type!="reward"') + button.advanced-options-toggle.option-title.mega(type='button', + ng-click='task._advanced = !task._advanced', tooltip=env.t('expandCollapse')) + =env.t('advancedOptions') + + div(ng-show='task._advanced') + div(ng-if='::task.type == "daily"') + .form-group + legend.option-title + span.hint(popover-title=env.t('startDateHelpTitle'), popover=env.t("startDateHelp"), popover-trigger='mouseenter') + =env.t('startDate') + + input.form-control(type='text', ng-model='task.startDate', + datepicker-popup='{{::user.preferences.dateFormat}}', is-open='datepickerOpened', + ng-click='datepickerOpened = true', ng-disabled='task.challenge.id') + + hr + + .form-group + legend.option-title=env.t('repeat') + select.form-control(ng-model='task.frequency', ng-disabled='task.challenge.id') + option(value='weekly')=env.t('repeatWeek') + option(value='daily')=env.t('repeatDays') + + include ./dailies/repeat_options + + hr + + fieldset.option-group.advanced-option(ng-show="task._advanced") + + legend.option-title + a.hint.priority-multiplier-help(href='http://habitrpg.wikia.com/wiki/Difficulty', target='_blank', popover-title=env.t('difficultyHelpTitle'), popover-trigger='mouseenter', popover=env.t('difficultyHelpContent'))=env.t('difficulty') + ul.priority-multiplier + li + button(type='button', ng-class='{active: task.priority==1 || !task.priority}', + ng-click='task.challenge.id || (task.priority=1)') + =env.t('easy') + li + button(type='button', ng-class='{active: task.priority==1.5}', + ng-click='task.challenge.id || (task.priority=1.5)') + =env.t('medium') + li + button(type='button', ng-class='{active: task.priority==2}', + ng-click='task.challenge.id || (task.priority=2)') + =env.t('hard') + + span(ng-if='task.type=="daily"') + legend.option-title.pull-left=env.t('restoreStreak') + input.option-content(type='number', ng-model='task.streak') + + div(ng-if='::(user.preferences.allocationMode == "taskbased" && user.preferences.automaticAllocation) || $state.is("options.social.challenges")') + legend.option-title.pull-left=env.t('attributes') + ul.task-attributes + li + button(type='button', ng-class='{active: task.attribute=="str"}', + ng-click='task.attribute="str"') + =env.t('physical') + li + button(type='button', ng-class='{active: task.attribute=="int"}', + ng-click='task.attribute="int"') + =env.t('mental') + li + button(type='button', ng-class='{active: task.attribute=="con"}', + ng-click='task.attribute="con"') + =env.t('social') + li + button(type='button', ng-class='{active: task.attribute=="per"}', + ng-click='task.attribute="per"', + popover=env.t('otherExamples'), popover-trigger='mouseenter', popover-placement='top') + =env.t('other') diff --git a/website/views/shared/tasks/edit/checklist.jade b/website/views/shared/tasks/edit/checklist.jade new file mode 100644 index 0000000000..ed77bb0a76 --- /dev/null +++ b/website/views/shared/tasks/edit/checklist.jade @@ -0,0 +1,23 @@ +.task-checklist-edit(ng-if='::!$state.includes("options.social.challenges") && (task.type=="daily" || task.type=="todo")') + ul + li + button(type='button', ng-if='!task.checklist[0]' + popover=env.t('checklistText'), popover-trigger='mouseenter', popover-placement='bottom', + ng-click='addChecklist(task)') + span.glyphicon.glyphicon-tasks + span=env.t('addChecklist') + + .checklist-form(ng-if='task.checklist') + fieldset.option-group(ng-if='!$state.includes("options.social.challenges")') + legend.option-title(ng-if='task.checklist[0]') + span.hint(popover=env.t('checklistText'), popover-trigger='mouseenter', popover-placement='bottom') + =env.t('checklist') + ul(hrpg-sort-checklist) + li(ng-repeat='item in task.checklist') + //input(type='checkbox',ng-model='item.completed',ng-change='saveTask(task,true)') + //-,ng-blur='saveTask(task,true)') + span.checklist-icon.glyphicon.glyphicon-resize-vertical + input(type='text', ng-model='item.text', + ui-keyup="{'13':'addChecklistItem(task,$event,$index)','38 40':'navigateChecklist(task,$index,$event)'}") + a(ng-click='removeChecklistItem(task,$event,$index,true)') + span.glyphicon.glyphicon-trash(tooltip=env.t('delete')) diff --git a/website/views/shared/tasks/edit/dailies/calendar.jade b/website/views/shared/tasks/edit/dailies/calendar.jade new file mode 100644 index 0000000000..0ffb033acb --- /dev/null +++ b/website/views/shared/tasks/edit/dailies/calendar.jade @@ -0,0 +1,3 @@ +fieldset.option-group.calendar(ng-if='::task.type=="daily"', class="option-group") + .dailies + include ./repeat_options diff --git a/website/views/shared/tasks/edit/dailies/repeat_options.jade b/website/views/shared/tasks/edit/dailies/repeat_options.jade new file mode 100644 index 0000000000..b27755bd03 --- /dev/null +++ b/website/views/shared/tasks/edit/dailies/repeat_options.jade @@ -0,0 +1,25 @@ +legend.option-title=env.t('repeatEvery') + +// If frequency is daily +ng-form.form-group(name='everyX' ng-if='task.frequency=="daily"') + .input-group + input.form-control(type='number', ng-model='task.everyX', ng-disabled='task.challenge.id', min='0', required) + span.input-group-addon {{task.everyX == 1 ? env.t('day') : env.t('days')}} + +// If frequency is weekly +.form-group(ng-if='task.frequency=="weekly"') + ul.repeat-days + // note, does not use data-toggle="buttons-checkbox" - it would interfere with our own click binding + mixin dayOfWeek(day, num) + li + button(type='button', ng-class='{active: task.repeat.#{day}}', + ng-disabled='task.challenge.id', ng-click='task.repeat.#{day} = !task.repeat.#{day}') + | {{::moment.weekdaysMin(#{num})}} + + +dayOfWeek('su', 0) + +dayOfWeek('m', 1) + +dayOfWeek('t', 2) + +dayOfWeek('w', 3) + +dayOfWeek('th', 4) + +dayOfWeek('f', 5) + +dayOfWeek('s', 6) diff --git a/website/views/shared/tasks/edit/habits/plus_minus.jade b/website/views/shared/tasks/edit/habits/plus_minus.jade new file mode 100644 index 0000000000..bbe17b7d4e --- /dev/null +++ b/website/views/shared/tasks/edit/habits/plus_minus.jade @@ -0,0 +1,8 @@ +fieldset.option-group.plusminus(ng-if='task.type=="habit" && !task.challenge.id') + legend.option-title=env.t('direction/Actions') + span.task-checker + input.visuallyhidden.focusable(id='{{obj._id}}_{{task.id}}-option-plus', type='checkbox', ng-model='task.up') + label(for='{{obj._id}}_{{task.id}}-option-plus') + span.task-checker + input.visuallyhidden.focusable(id='{{obj._id}}_{{task.id}}-option-minus', type='checkbox', ng-model='task.down') + label(for='{{obj._id}}_{{task.id}}-option-minus') diff --git a/website/views/shared/tasks/edit/index.jade b/website/views/shared/tasks/edit/index.jade new file mode 100644 index 0000000000..ce29ee8896 --- /dev/null +++ b/website/views/shared/tasks/edit/index.jade @@ -0,0 +1,52 @@ +div(ng-if='task._editing') + .task-options + + // Broken Challenge + .well(ng-if='task.challenge.broken') + div(ng-if='task.challenge.broken=="TASK_DELETED"') + p=env.t('brokenTask') + p + a(ng-click='unlink(task, "keep")')=env.t('keepIt') + |    + a(ng-click="removeTask(task, obj[list.type+'s'])")=env.t('removeIt') + div(ng-if='task.challenge.broken=="CHALLENGE_DELETED"') + p + |  + =env.t('brokenChallenge') + p + a(ng-click='unlink(task, "keep-all")')=env.t('keepThem') + |  |  + a(ng-click='unlink(task, "remove-all")')=env.t('removeThem') + div(ng-if='task.challenge.broken=="CHALLENGE_CLOSED"') + p + !=env.t('challengeCompleted', {user: "{{task.challenge.winner}}"}) + p + a(ng-click='unlink(task, "keep-all")')=env.t('keepThem') + |  |  + a(ng-click='unlink(task, "remove-all")')=env.t('removeThem') + //div(ng-if='task.challenge.broken=="UNSUBSCRIBED"') + p=env.t('unsubChallenge') + p + a(ng-click="unlink(task, 'keep-all')")=env.t('keepThem') + |  |  + a(ng-click="unlink(task, 'remove-all')")=env.t('removeThem') + + include ./checklist + + form(ng-submit='saveTask(task,false,true)') + include ./text_notes + + include ./habits/plus_minus + + include ./dailies/calendar + + include ./rewards/pricing + + include ./todos/due_date + + include ./tags + + include ./advanced_options + + .save-close + button(type='submit')=env.t('saveAndClose') diff --git a/website/views/shared/tasks/edit/rewards/pricing.jade b/website/views/shared/tasks/edit/rewards/pricing.jade new file mode 100644 index 0000000000..f322dc06d9 --- /dev/null +++ b/website/views/shared/tasks/edit/rewards/pricing.jade @@ -0,0 +1,5 @@ +fieldset.option-group.option-short(ng-if='task.type=="reward" && !task.challenge.id') + legend.option-title=env.t('price') + input.option-content(type='number', size='16', min='0', step='any', ng-model='task.value', required) + .money.input-suffix + span.shop_gold diff --git a/website/views/shared/tasks/edit/tags.jade b/website/views/shared/tasks/edit/tags.jade new file mode 100644 index 0000000000..ab512731fd --- /dev/null +++ b/website/views/shared/tasks/edit/tags.jade @@ -0,0 +1,5 @@ +fieldset.option-group(ng-if='!$state.includes("options.social.challenges")') + p.option-title.mega(ng-click='task._tags = !task._tags', tooltip=env.t('expandCollapse'))=env.t('tags') + label.checkbox(ng-repeat='tag in user.tags', ng-class="{visuallyhidden: task._tags}") + input(type='checkbox', ng-model='task.tags[tag.id]') + markdown(text='tag.name') diff --git a/website/views/shared/tasks/edit/text_notes.jade b/website/views/shared/tasks/edit/text_notes.jade new file mode 100644 index 0000000000..95dc1ef9af --- /dev/null +++ b/website/views/shared/tasks/edit/text_notes.jade @@ -0,0 +1,7 @@ +fieldset.option-group + label.option-title=env.t('text') + input.form-control(type='text', ng-model='task.text', required, ng-disabled='task.challenge.id') + +fieldset.option-group + label.option-title=env.t('extraNotes') + textarea.form-control(rows='3', ng-model='task.notes', ng-model-options="{debounce: 1000}") diff --git a/website/views/shared/tasks/edit/todos/due_date.jade b/website/views/shared/tasks/edit/todos/due_date.jade new file mode 100644 index 0000000000..c9cbfe7d0e --- /dev/null +++ b/website/views/shared/tasks/edit/todos/due_date.jade @@ -0,0 +1,6 @@ +fieldset.option-group(ng-if='task.type=="todo" && !task.challenge.id') + legend.option-title=env.t('dueDate') + input.option-content.datepicker(type='text', ng-model='task.date', + datepicker-popup='{{::user.preferences.dateFormat}}', is-open='datepickerOpened', + ng-click='datepickerOpened = true') + diff --git a/website/views/shared/tasks/index.jade b/website/views/shared/tasks/index.jade new file mode 100644 index 0000000000..972a62f1db --- /dev/null +++ b/website/views/shared/tasks/index.jade @@ -0,0 +1,39 @@ +// Note here, we need this part of Habit to be a directive since we're going to be passing it variables from various +// parts of the app. The alternative would be to create new scopes for different containing sections, but that +// started to get unwieldy + +include ./task_view/mixins +script(id='templates/habitrpg-tasks.html', type="text/ng-template") + .tasks-lists.container-fluid + .row + .col-md-3.col-sm-6(ng-repeat='list in lists', ng-class='::{"rewards-module": list.type==="reward"}') + .task-column(class='{{::list.type}}s') + + include ./task_view/graph + + h2.task-column_title {{::list.header}} + + include ./task_view/help + + .todos-chart(ng-if='::list.type == "todo"', ng-show='charts.todos') + + include ./task_view/add_new + + alert.alert-warning.dailiesRestingInInn(ng-if='::list.type == "daily" && user.preferences.sleep') + i.glyphicon.glyphicon-warning-sign   + =env.t('dailiesRestingInInn') + + +taskColumnTabs('top') + + // Actual List + ul(class='{{::list.type}}s main-list', ng-show='obj[list.type + "s"].length > 0', hrpg-sort-tasks, ng-if='!$state.includes("options.social.challenges")') + include ./task + //Loads the non-sortable lists for challenges + ul(class='{{::list.type}}s main-list', ng-show='obj[list.type + "s"].length > 0', ng-if='$state.includes("options.social.challenges")') + include ./task + + include ./task_view/static_rewards + + include ./task_view/spells + + +taskColumnTabs('bottom') diff --git a/website/views/shared/tasks/lists.jade b/website/views/shared/tasks/lists.jade deleted file mode 100644 index 013f653bb8..0000000000 --- a/website/views/shared/tasks/lists.jade +++ /dev/null @@ -1,188 +0,0 @@ -// Note here, we need this part of Habit to be a directive since we're going to be passing it variables from various -// parts of the app. The alternative would be to create new scopes for different containing sections, but that -// started to get unwieldy -script(id='templates/habitrpg-tasks.html', type="text/ng-template") - .tasks-lists.container-fluid - .row - .col-md-3.col-sm-6(bindonce='lists', ng-repeat='list in lists', ng-class='::{"rewards-module": list.type==="reward"}') - .task-column(class='{{list.type}}s') - - // Todos export/graph options - span.option-box.pull-right(ng-if='::main') - a.option-action(ng-if='list.type=="todo"', ng-show='obj.history.todos', ng-click='toggleChart("todos")', tooltip=env.t('progress'), style='margin-right:5px;') - span.glyphicon.glyphicon-signal - //a.option-action(ng-href='/v1/users/{{user.id}}/calendar.ics?apiToken={{user.apiToken}}', tooltip='iCal') - //-a.option-action(ng-if='list.type=="todo"', ng-click='notPorted()', tooltip='iCal', ng-show='false') - span.glyphicon.glyphicon-calendar - // - a.option-action(ng-click='list.help=!list.help', tooltip='Click for help') - span.glyphicon.glyphicon-question-sign(style={'zoom':1.5,'vertical-align':'-webkit-baseline-middle'}) - - // Header - h2.task-column_title - | {{list.header}} - - div(ng-if='list.help', ng-switch='::list.type') - div(ng-switch-when='habit') - ul - li!=env.t('habitHelp1', {plusIcon:""}) - li!=env.t('habitHelp2', {minusIcon:""}) - li!=env.t('habitHelp3') - li!=env.t('newbieGuild', {linkStart:"", linkEnd: ""}) - div(ng-switch-when='daily') - ul - li!=env.t('dailyHelp1', {emphasisStart:"", emphasisEnd:"", pencilIcon:""}) - li=env.t('dailyHelp2') - li!=env.t('dailyHelp3', {emphasisStart:"", emphasisEnd:""}) - li!=env.t('dailyHelp4', {linkStart:"", linkEnd:""}) - li!=env.t('dailyHelp5') - li!=env.t('newbieGuild', {linkStart:"", linkEnd: ""}) - div(ng-switch-when='todo') - ul - li=env.t('toDoHelp1') - li=env.t('toDoHelp2') - li=env.t('toDoHelp3') - li!=env.t('toDoHelp4') - li!=env.t('newbieGuild', {linkStart:"", linkEnd: ""}) - div(ng-switch-when='reward') - ul - li!=env.t('rewardHelp1', {linkStart:"", linkEnd: ""}) - li!=env.t('rewardHelp2', {linkStart:"", linkEnd: ""}) - li=env.t('rewardHelp3') - li!=env.t('rewardHelp4') - li!=env.t('newbieGuild', {linkStart:"", linkEnd: ""}) - - // Todo Chart - .todos-chart(ng-if='::list.type == "todo"', ng-show='charts.todos') - - // Add New - form.task-add(name='new{{list.type}}form', ng-hide='obj._locked', ng-submit='addTask(obj[list.type+"s"],list)' novalidate) - textarea(rows='6', task-focus='list.bulk && list.focus', ng-model='list.newTask', placeholder='{{list.placeHolderBulk}}', ng-if='list.bulk', ui-keydown='{"meta-enter ctrl-enter":"addTask(obj[list.type+\'s\'],list)"}', required) - input(type='text', task-focus='!list.bulk && list.focus', ng-model='list.newTask', placeholder='{{list.placeHolder}}', ng-if='!list.bulk', required) - button(type='submit', ng-disabled='new{{list.type}}form.$invalid') - div.empty-task-notification( ng-show='new{{list.type}}form.$invalid', tooltip=env.t("emptyTask") ) - span.glyphicon.glyphicon-plus - span.glyphicon.glyphicon-plus(ng-show='!new{{list.type}}form.$invalid') - small.help-block.btn-link.pull-right(ng-click='toggleBulk(list)') - span(ng-if='!list.bulk')=env.t('addmultiple') - span(ng-if='list.bulk')=env.t('addsingle') - - alert.alert-warning.dailiesRestingInInn(ng-if='::list.type == "daily" && user.preferences.sleep') - i.glyphicon.glyphicon-warning-sign   - =env.t('dailiesRestingInInn') - - mixin taskColumnTabs(position) - // Habits Tabs - div(ng-if='::main && list.type=="habit"', class='tabbable tabs-below') - ul.task-filter - li(ng-class='{active: list.view == "all"}') - a(ng-click='list.view = "all"')=env.t('all') - li(ng-class='{active: list.view == "yellowred"}') - a(ng-click='list.view = "yellowred"')=env.t('yellowred') - li(ng-class='{active: list.view == "greenblue"}') - a(ng-click='list.view = "greenblue"')=env.t('greenblue') - // Daily Tabs - div(ng-if='::main && list.type=="daily"', class='tabbable tabs-below') - // remaining/completed tabs - ul.task-filter - li(ng-class='{active: list.view == "all"}') - a(ng-click='list.view = "all"')=env.t('all') - li(ng-class='{active: list.view == "remaining"}') - a(ng-click='list.view = "remaining"')=env.t('due') - li(ng-class='{active: list.view == "complete"}') - a(ng-click='list.view = "complete"')=env.t('grey') - // Todo Tabs - div(ng-if='::main && list.type=="todo"', ng-class='::{"tabbable tabs-below": list.type=="todo"}') - if position=="bottom" - div(ng-show='list.view == "complete"') - .alert - =env.t('lotOfToDos') - button.task-action-btn.tile.spacious.bright(ng-click='user.ops.clearCompleted({})',popover=env.t('deleteToDosExplanation'),popover-trigger='mouseenter')=env.t('clearCompleted') - p!=env.t('beeminderDeleteWarning') - // remaining/completed tabs - ul.task-filter - li(ng-class='{active: list.view == "remaining"}') - a(ng-click='list.view = "remaining"')=env.t('remaining') - li(ng-class='{active: list.view == "dated"}') - a(ng-click='list.view = "dated"')=env.t('dated') - li(ng-class='{active: list.view == "complete"}') - a(ng-click='list.view = "complete"')=env.t('complete') - // Rewards Tabs - div(ng-if='::main && list.type=="reward"', class='tabbable tabs-below') - ul.task-filter - li(ng-class='{active: list.view == "all"}') - a(ng-click='list.view = "all"')=env.t('all') - li(ng-class='{active: list.view == "ingamerewards"}') - a(ng-click='list.view = "ingamerewards"')=env.t('ingamerewards') - - +taskColumnTabs('top') - - // Actual List - ul(class='{{list.type}}s main-list', ng-show='obj[list.type + "s"].length > 0', hrpg-sort-tasks, ng-if='!$state.includes("options.social.challenges")') - include ./task - //Loads the non-sortable lists for challenges - ul(class='{{list.type}}s main-list', ng-show='obj[list.type + "s"].length > 0', ng-if='$state.includes("options.social.challenges")') - include ./task - - // Static Rewards - ul.items.rewards(ng-if='main && list.type=="reward"') - li.task.reward-item(ng-repeat='item in itemStore',popover-trigger='mouseenter', popover-placement='top', popover='{{item.key == "armoire" && !user.flags.armoireEmpty ? env.t("armoireNotesFull") + armoireCount(user.items.gear.owned) : item.notes()}}') - // right-hand side control buttons - .task-meta-controls - span.task-notes - span.glyphicon.glyphicon-comment - //left-hand size commands - .task-controls.task-primary - a.money.btn-buy.item-btn(ng-class='{highValue: item.value >= 1000}', ng-click='buy(item)') - span.shop_gold - span.reward-cost {{item.value}} - // main content - span(ng-class='::{"shop_{{item.key}} shop-sprite item-img": true}').reward-img - p.task-text {{item.text()}} - - // Events - ul.items.rewards(ng-if='main && list.type=="reward" && (user.items.special.snowball>0 || user.stats.buffs.snowball || user.items.special.spookDust>0 || user.stats.buffs.spookDust || user.items.special.shinySeed>0 || user.stats.buffs.shinySeed)') - - mixin specialSpell(k,canceler) - li.task.reward-item(ng-if='#{canceler ? "user.stats.buffs."+canceler : "user.items.special."+k+">0"}',popover-trigger='mouseenter', popover-placement='top', popover='{{Content.spells.special.#{k}.notes()}}') - .task-meta-controls - span.task-notes - span.glyphicon.glyphicon-comment - //left-hand size commands - .task-controls.task-primary - a.money.btn-buy.item-btn(ng-click='castStart(Content.spells.special.#{k})', ng-class='{active: Content.spells.special.#{k}.key == spell.key}') - if canceler - span.shop_gold - span.reward-cost {{Content.spells.special.#{k}.value}} - else - span.shop_spell(class='shop_#{k}') - span.reward-cost {{user.items.special.#{k}}} - // main content - p.task-text {{Content.spells.special.#{k}.text()}} - - +specialSpell('snowball') - +specialSpell('spookDust') - +specialSpell('shinySeed') - +specialSpell('salt','snowball') - +specialSpell('opaquePotion','spookDust') - +specialSpell('petalFreePotion','shinySeed') - - // Spells - ul.items(ng-if='main && list.type=="reward" && user.stats.class && !user.preferences.disableClasses') - li.task.reward-item(ng-repeat='(k,skill) in Content.spells[user.stats.class]', ng-if='user.stats.lvl >= skill.lvl',popover-trigger='mouseenter', popover-placement='top', popover='{{skill.notes()}}') - .task-meta-controls - span.task-notes - span.glyphicon.glyphicon-comment - //left-hand size commands - .task-controls.task-primary - a.money.btn-buy.item-btn(ng-click='castStart(skill)', ng-class='{active: skill.key == spell.key}') - span.reward-cost - strong {{skill.mana}} - =env.t('mp') - // main content - span(ng-class='{"shop_{{skill.key}} shop-sprite item-img": true}') - p.task-text {{skill.text()}} - - br - - +taskColumnTabs('bottom') diff --git a/website/views/shared/tasks/meta_controls.jade b/website/views/shared/tasks/meta_controls.jade new file mode 100644 index 0000000000..acff83ef6f --- /dev/null +++ b/website/views/shared/tasks/meta_controls.jade @@ -0,0 +1,56 @@ +.task-meta-controls + + // Due Date + span(ng-if='task.type=="todo" && task.date') + span(ng-class='{"label label-danger":(moment(task.date).isBefore(_today, "days") && !task.completed)}') {{task.date | date:(user.preferences.dateFormat.indexOf('yyyy') == 0 ? user.preferences.dateFormat.substr(5) : user.preferences.dateFormat.substr(0,5))}} + + // Streak + |   + span(ng-show='task.streak') {{task.streak}}  + span(tooltip=env.t('streakCounter')) + span.glyphicon.glyphicon-forward + |   + + // Icons only available if you own the tasks (aka, hidden from challenge stats) + span(ng-if='!obj._locked') + a(ng-click='pushTask(task,$index,"top")', tooltip=env.t('pushTaskToTop')) + span.glyphicon.glyphicon-open + // a(ng-click='pushTask(task,$index,"bottom")', tooltip=env.t('pushTaskToBottom')) + // span.glyphicon.glyphicon-import + // // glyphicon-import or glyphicon-save or glyphicon-sort-by-attributes + a.badge(ng-if='task.checklist[0]', ng-class='{"badge-success":checklistCompletion(task.checklist) == task.checklist.length}', ng-click='collapseChecklist(task)', tooltip=env.t('expandCollapse')) + |{{checklistCompletion(task.checklist)}}/{{task.checklist.length}} + span.glyphicon.glyphicon-tags(tooltip='{{Shared.appliedTags(user.tags, task.tags)}}', ng-hide='Shared.noTags(task.tags)') + // edit + a(ng-hide='task._editing', ng-click='editTask(task)', tooltip=env.t('edit')) + |   + span.glyphicon.glyphicon-pencil(ng-hide='task._editing') + |   + a(ng-hide='!task._editing', ng-click='editTask(task)', tooltip=env.t('cancel')) + span.glyphicon.glyphicon-remove(ng-hide='!task._editing') + |   + // save + a(ng-hide='!task._editing', ng-click='editTask(task);saveTask(task)', tooltip=env.t('save')) + span.glyphicon.glyphicon-ok(ng-hide='!task._editing') + |   + //challenges + span(ng-if='task.challenge.id') + span(ng-if='task.challenge.broken') + span.glyphicon.glyphicon-bullhorn(style='background-color:red;', ng-click='task._editing = true', tooltip=env.t('brokenChaLink') tooltip-placement='right') + |   + span(ng-if='!task.challenge.broken') + span.glyphicon.glyphicon-bullhorn(tooltip=env.t('challenge')) + |   + // delete + a(ng-if='!task.challenge.id', ng-click='removeTask(task, obj[list.type+"s"])', tooltip=env.t('delete')) + span.glyphicon.glyphicon-trash + |   + + // chart + a(ng-show='task.history', ng-click='toggleChart(obj._id+task.id, task)', tooltip=env.t('progress')) + span.glyphicon.glyphicon-signal + |   + // notes + span.task-notes(ng-show='task.notes && !task._editing', ng-click='task.popoverOpen = !task.popoverOpen', popover-trigger='click', data-popover-html="{{task.notes | markdown}}", popover-placement="top", popover-append-to-body='{{::modal ? "false":"true"}}') + span.glyphicon.glyphicon-comment + |   diff --git a/website/views/shared/tasks/task.jade b/website/views/shared/tasks/task.jade index 9fa9a68bbe..3675e158f5 100644 --- a/website/views/shared/tasks/task.jade +++ b/website/views/shared/tasks/task.jade @@ -1,241 +1,17 @@ -li(bindonce='list', id='task-{{::task.id}}', ng-repeat='task in obj[list.type+"s"] | conditionalOrderBy: list.view=="dated":"date"', class='task {{Shared.taskClasses(task, user.filters, user.preferences.dayStart, user.lastCron, list.showCompleted, main)}}', ng-click='spell && (list.type != "reward") && castEnd(task, "task", $event)', ng-class='{"cast-target":spell && (list.type != "reward"), "locked-task":obj._locked === true}', popover-trigger='mouseenter', data-popover-html="{{task.popoverOpen ? '' : task.notes | markdown}}", popover-placement="top", popover-append-to-body='{{::modal ? "false":"true"}}', ng-show='shouldShow(task, list, user.preferences)') - // right-hand side control buttons - .task-meta-controls +li(id='task-{{::task.id}}', + ng-repeat='task in obj[list.type+"s"] | filterByTextAndNotes: obj.filterQuery | conditionalOrderBy: list.view=="dated":"date"', + class='task {{Shared.taskClasses(task, user.filters, user.preferences.dayStart, user.lastCron, list.showCompleted, main)}}', + ng-class='{"cast-target":spell && (list.type != "reward"), "locked-task":obj._locked === true}', + ng-click='spell && (list.type != "reward") && castEnd(task, "task", $event)', + ng-show='shouldShow(task, list, user.preferences)', + popover-trigger='mouseenter', popover-placement="top", popover-append-to-body='{{::modal ? "false":"true"}}', + data-popover-html="{{task.popoverOpen ? '' : task.notes | markdown}}") - // Due Date - span(ng-if='task.type=="todo" && task.date') - span(ng-class='{"label label-danger":(moment(task.date).isBefore(_today, "days") && !task.completed)}') {{task.date | date:(user.preferences.dateFormat.indexOf('yyyy') == 0 ? user.preferences.dateFormat.substr(5) : user.preferences.dateFormat.substr(0,5))}} + ng-form(name='taskForm') + include ./meta_controls - // Streak - |   - span(ng-show='task.streak') {{task.streak}}  - span(tooltip=env.t('streakCounter')) - span.glyphicon.glyphicon-forward - |   + include ./task_view/index - // Icons only available if you own the tasks (aka, hidden from challenge stats) - span(ng-if='!obj._locked') - a(ng-click='pushTask(task,$index,"top")', tooltip=env.t('pushTaskToTop')) - span.glyphicon.glyphicon-open - // a(ng-click='pushTask(task,$index,"bottom")', tooltip=env.t('pushTaskToBottom')) - // span.glyphicon.glyphicon-import - // // glyphicon-import or glyphicon-save or glyphicon-sort-by-attributes - a.badge(ng-if='task.checklist[0]', ng-class='{"badge-success":checklistCompletion(task.checklist) == task.checklist.length}', ng-click='collapseChecklist(task)', tooltip=env.t('expandCollapse')) - |{{checklistCompletion(task.checklist)}}/{{task.checklist.length}} - span.glyphicon.glyphicon-tags(tooltip='{{Shared.appliedTags(user.tags, task.tags)}}', ng-hide='Shared.noTags(task.tags)') - // edit - a(ng-hide='task._editing', ng-click='editTask(task)', tooltip=env.t('edit')) - |   - span.glyphicon.glyphicon-pencil(ng-hide='task._editing') - |   - a(ng-hide='!task._editing', ng-click='editTask(task)', tooltip=env.t('cancel')) - span.glyphicon.glyphicon-remove(ng-hide='!task._editing') - |   - // save - a(ng-hide='!task._editing', ng-click='editTask(task);saveTask(task)', tooltip=env.t('save')) - span.glyphicon.glyphicon-ok(ng-hide='!task._editing') - |   - //challenges - span(ng-if='task.challenge.id') - span(ng-if='task.challenge.broken') - span.glyphicon.glyphicon-bullhorn(style='background-color:red;', ng-click='task._editing = true', tooltip=env.t('brokenChaLink') tooltip-placement='right') - |   - span(ng-if='!task.challenge.broken') - span.glyphicon.glyphicon-bullhorn(tooltip=env.t('challenge')) - |   - // delete - a(ng-if='!task.challenge.id', ng-click='removeTask(task, obj[list.type+"s"])', tooltip=env.t('delete')) - span.glyphicon.glyphicon-trash - |   - - // chart - a(ng-show='task.history', ng-click='toggleChart(obj._id+task.id, task)', tooltip=env.t('progress')) - span.glyphicon.glyphicon-signal - |   - // notes - span.task-notes(ng-show='task.notes && !task._editing', ng-click='task.popoverOpen = !task.popoverOpen', popover-trigger='click', data-popover-html="{{task.notes | markdown}}", popover-placement="top", popover-append-to-body='{{::modal ? "false":"true"}}') - span.glyphicon.glyphicon-comment - |   - - // left-hand side checkbox - .task-controls.task-primary(ng-if='!task._editing') - - // Habits - .task-actions(ng-if='::task.type=="habit"') - // score() is overridden in challengesCtrl to do nothing - a(ng-if='task.up', ng-click='applyingAction || score(task,"up")') - span.glyphicon.glyphicon-plus - a(ng-if='task.down', ng-click='applyingAction || score(task,"down")') - span.glyphicon.glyphicon-minus - - // Rewards - span(ng-show='task.type=="reward"') - a.money.btn-buy(ng-class='{highValue: task.value >= 1000}', ng-click='score(task, "down")') - span.shop_gold - span.reward-cost {{task.value}} - // Daily & Todos - span.task-checker.action-yesno(ng-if='::task.type=="daily" || task.type=="todo"') - input.visuallyhidden.focusable(ng-if='$state.includes("tasks")', id='box-{{obj._id}}_{{task.id}}', type='checkbox', ng-model='task.completed', ng-change='task.type=="todo" && pushTask(task,$index,"bottom"); changeCheck(task)') - input.visuallyhidden.focusable(ng-if='!$state.includes("tasks")', id='box-{{obj._id}}_{{task.id}}', type='checkbox') - label(for='box-{{obj._id}}_{{task.id}}') - - // main content - div.task-text(ng-dblclick='task._editing ? saveTask(task) : editTask(task)') - markdown(text='task.text',target='_blank') - //-| {{task.text}} - - div(ng-if='task.checklist && !$state.includes("options.social.challenges") && !task.collapseChecklist && !task._editing') - fieldset.option-group.task-checklist - label.checkbox(ng-repeat='item in task.checklist') - input(type='checkbox',ng-model='item.completed',ng-change='saveTask(task,true)') - markdown(text='item.text',target='_blank') - - // edit/options dialog - div(ng-if='task._editing') - .task-options - - // Broken Challenge - .well(ng-if='task.challenge.broken') - div(ng-if='task.challenge.broken=="TASK_DELETED"') - p=env.t('brokenTask') - p - a(ng-click='unlink(task, "keep")')=env.t('keepIt') - |    - a(ng-click="removeTask(task, obj[list.type+'s'])")=env.t('removeIt') - div(ng-if='task.challenge.broken=="CHALLENGE_DELETED"') - p - |  - =env.t('brokenChallenge') - p - a(ng-click='unlink(task, "keep-all")')=env.t('keepThem') - |  |  - a(ng-click='unlink(task, "remove-all")')=env.t('removeThem') - div(ng-if='task.challenge.broken=="CHALLENGE_CLOSED"') - p - !=env.t('challengeCompleted', {user: "{{task.challenge.winner}}"}) - p - a(ng-click='unlink(task, "keep-all")')=env.t('keepThem') - |  |  - a(ng-click='unlink(task, "remove-all")')=env.t('removeThem') - //div(ng-if='task.challenge.broken=="UNSUBSCRIBED"') - p=env.t('unsubChallenge') - p - a(ng-click="unlink(task, 'keep-all')")=env.t('keepThem') - |  |  - a(ng-click="unlink(task, 'remove-all')")=env.t('removeThem') - - - form(ng-submit='saveTask(task,false,true)') - // Title text input - label.option-title=env.t('title') - input.option-content(type='text', ng-model='task.text', required, ng-disabled='task.challenge.id') - - // Checklists - .task-checklist-edit(ng-if='!$state.includes("options.social.challenges")') - ul - li - button(type='button', ng-if='!task.checklist[0] && (task.type=="daily" || task.type=="todo")',ng-click='addChecklist(task)') - span.glyphicon.glyphicon-tasks - span=env.t('addChecklist') - form.checklist-form(ng-if='task.checklist') - fieldset.option-group(ng-if='!$state.includes("options.social.challenges")') - legend.option-title - span.hint(popover=env.t('checklistText'),popover-trigger='mouseenter',popover-placement='bottom')=env.t('checklist') - ul(hrpg-sort-checklist) - li(ng-repeat='item in task.checklist') - //input(type='checkbox',ng-model='item.completed',ng-change='saveTask(task,true)') - //-,ng-blur='saveTask(task,true)') - span.checklist-icon.glyphicon.glyphicon-resize-vertical() - input(type='text',ng-model='item.text', ui-keyup="{'13':'addChecklistItem(task,$event,$index)','38 40':'navigateChecklist(task,$index,$event)'}") - a(ng-click='removeChecklistItem(task,$event,$index,true)') - span.glyphicon.glyphicon-trash(tooltip=env.t('delete')) - - form(ng-submit='saveTask(task,false,true)') - // Notes text input - fieldset.option-group - label.option-title=env.t('extraNotes') - textarea.option-content(rows='3', ng-model='task.notes', ng-model-options="{debounce: 1000}") - - // if Habit, plus/minus command options - fieldset.option-group.plusminus(ng-if='task.type=="habit" && !task.challenge.id') - legend.option-title=env.t('direction/Actions') - span.task-checker - input.visuallyhidden.focusable(id='{{obj._id}}_{{task.id}}-option-plus', type='checkbox', ng-model='task.up') - label(for='{{obj._id}}_{{task.id}}-option-plus') - span.task-checker - input.visuallyhidden.focusable(id='{{obj._id}}_{{task.id}}-option-minus', type='checkbox', ng-model='task.down') - label(for='{{obj._id}}_{{task.id}}-option-minus') - - // if Daily, calendar - fieldset(ng-if='::task.type=="daily"', class="option-group") - legend.option-title=env.t('repeat') - ul.repeat-days(bindonce) - // note, does not use data-toggle="buttons-checkbox" - it would interfere with our own click binding - li - button(ng-class='{active: task.repeat.su}', type='button', ng-click='task.challenge.id || (task.repeat.su = !task.repeat.su)') {{::moment.weekdaysMin(0)}} - li - button(ng-class='{active: task.repeat.m}', type='button', ng-click='task.challenge.id || (task.repeat.m = !task.repeat.m)') {{::moment.weekdaysMin(1)}} - li - button(ng-class='{active: task.repeat.t}', type='button', ng-click='task.challenge.id || (task.repeat.t = !task.repeat.t)') {{::moment.weekdaysMin(2)}} - li - button(ng-class='{active: task.repeat.w}', type='button', ng-click='task.challenge.id || (task.repeat.w = !task.repeat.w)') {{::moment.weekdaysMin(3)}} - li - button(ng-class='{active: task.repeat.th}', type='button', ng-click='task.challenge.id || (task.repeat.th = !task.repeat.th)') {{::moment.weekdaysMin(4)}} - li - button(ng-class='{active: task.repeat.f}', type='button', ng-click='task.challenge.id || (task.repeat.f= !task.repeat.f)') {{::moment.weekdaysMin(5)}} - li - button(ng-class='{active: task.repeat.s}', type='button', ng-click='task.challenge.id || (task.repeat.s = !task.repeat.s)') {{::moment.weekdaysMin(6)}} - - // if Reward, pricing - fieldset.option-group.option-short(ng-if='task.type=="reward" && !task.challenge.id') - legend.option-title=env.t('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" && !task.challenge.id') - legend.option-title=env.t('dueDate') - input.option-content.datepicker(type='text', datepicker-popup='{{user.preferences.dateFormat}}', ng-model='task.date', is-open='datepickerOpened', ng-click='datepickerOpened = true') - - // Tags - fieldset.option-group(ng-if='!$state.includes("options.social.challenges")') - p.option-title.mega(ng-click='task._tags = !task._tags', tooltip=env.t('expandCollapse'))=env.t('tags') - label.checkbox(ng-repeat='tag in user.tags', ng-class="{visuallyhidden: task._tags}") - input(type='checkbox', ng-model='task.tags[tag.id]') - markdown(text='tag.name') - - // Advanced Options - span(ng-if='::task.type!="reward"') - p.option-title.mega(ng-click='task._advanced = !task._advanced', tooltip=env.t('expandCollapse'))=env.t('advancedOptions') - fieldset.option-group.advanced-option(ng-class="{visuallyhidden: task._advanced}") - legend.option-title - a.hint.priority-multiplier-help(href='http://habitrpg.wikia.com/wiki/Difficulty', target='_blank', popover-title=env.t('difficultyHelpTitle'), popover-trigger='mouseenter', popover=env.t('difficultyHelpContent'))=env.t('difficulty') - ul.priority-multiplier - li - button(type='button', ng-class='{active: task.priority==1 || !task.priority}', ng-click='task.challenge.id || (task.priority=1)')=env.t('easy') - li - button(type='button', ng-class='{active: task.priority==1.5}', ng-click='task.challenge.id || (task.priority=1.5)')=env.t('medium') - li - button(type='button', ng-class='{active: task.priority==2}', ng-click='task.challenge.id || (task.priority=2)')=env.t('hard') - //span(ng-if='task.type=="daily" && !task.challenge.id') - span(ng-if='task.type=="daily"') - legend.option-title.pull-left=env.t('restoreStreak') - input.option-content(type='number', ng-model='task.streak') - - div(ng-if='(user.preferences.allocationMode == "taskbased" && user.preferences.automaticAllocation) || $state.is("options.social.challenges")') - legend.option-title.pull-left=env.t('attributes') - ul.task-attributes - li - button(type='button', ng-class='{active: task.attribute=="str"}', ng-click='task.attribute="str"')=env.t('physical') - li - button(type='button', ng-class='{active: task.attribute=="int"}', ng-click='task.attribute="int"')=env.t('mental') - li - button(type='button', ng-class='{active: task.attribute=="con"}', ng-click='task.attribute="con"')=env.t('social') - li - button(type='button', ng-class='{active: task.attribute=="per"}', ng-click='task.attribute="per"', popover=env.t('otherExamples'), popover-trigger='mouseenter', popover-placement='top')=env.t('other') - - .save-close - button(type='submit')=env.t('saveAndClose') + include ./edit/index div(class='{{obj._id}}{{task.id}}-chart', ng-show='charts[obj._id+task.id]') diff --git a/website/views/shared/tasks/task_view/add_new.jade b/website/views/shared/tasks/task_view/add_new.jade new file mode 100644 index 0000000000..aee14a33b6 --- /dev/null +++ b/website/views/shared/tasks/task_view/add_new.jade @@ -0,0 +1,10 @@ +form.task-add(name='new{{list.type}}form', ng-hide='obj._locked', ng-submit='addTask(obj[list.type+"s"],list)', novalidate) + textarea(rows='6', task-focus='list.bulk && list.focus', ng-model='list.newTask', placeholder='{{list.placeHolderBulk}}', ng-if='list.bulk', ui-keydown='{"meta-enter ctrl-enter":"addTask(obj[list.type+\'s\'],list)"}', required) + input(type='text', task-focus='!list.bulk && list.focus', ng-model='list.newTask', placeholder='{{list.placeHolder}}', ng-if='!list.bulk', required) + button(type='submit', ng-disabled='new{{list.type}}form.$invalid') + div.empty-task-notification( ng-show='new{{list.type}}form.$invalid', tooltip=env.t("emptyTask") ) + span.glyphicon.glyphicon-plus + span.glyphicon.glyphicon-plus(ng-show='!new{{list.type}}form.$invalid') + small.help-block.btn-link.pull-right(ng-click='toggleBulk(list)') + span(ng-if='!list.bulk')=env.t('addmultiple') + span(ng-if='list.bulk')=env.t('addsingle') diff --git a/website/views/shared/tasks/task_view/graph.jade b/website/views/shared/tasks/task_view/graph.jade new file mode 100644 index 0000000000..dfc3359ff5 --- /dev/null +++ b/website/views/shared/tasks/task_view/graph.jade @@ -0,0 +1,9 @@ +span.option-box.pull-right(ng-if='::main') + a.option-action(ng-if='list.type=="todo"', ng-show='obj.history.todos', ng-click='toggleChart("todos")', tooltip=env.t('progress'), style='margin-right:5px;') + span.glyphicon.glyphicon-signal + //a.option-action(ng-href='/v1/users/{{user.id}}/calendar.ics?apiToken={{user.apiToken}}', tooltip='iCal') + //-a.option-action(ng-if='list.type=="todo"', ng-click='notPorted()', tooltip='iCal', ng-show='false') + span.glyphicon.glyphicon-calendar + // + a.option-action(ng-click='list.help=!list.help', tooltip=env.t('clickForHelp')) + span.glyphicon.glyphicon-question-sign(style={'zoom':1.5,'vertical-align':'-webkit-baseline-middle'}) diff --git a/website/views/shared/tasks/task_view/help.jade b/website/views/shared/tasks/task_view/help.jade new file mode 100644 index 0000000000..718d82e411 --- /dev/null +++ b/website/views/shared/tasks/task_view/help.jade @@ -0,0 +1,25 @@ +div(ng-if='list.help', ng-switch='::list.type') + ul(ng-switch-when='habit') + li!=env.t('habitHelp1', {plusIcon:""}) + li!=env.t('habitHelp2', {minusIcon:""}) + li!=env.t('habitHelp3') + li!=env.t('newbieGuild', {linkStart:"", linkEnd: ""}) + ul(ng-switch-when='daily') + li!=env.t('dailyHelp1', {emphasisStart:"", emphasisEnd:"", pencilIcon:""}) + li=env.t('dailyHelp2') + li!=env.t('dailyHelp3', {emphasisStart:"", emphasisEnd:""}) + li!=env.t('dailyHelp4', {linkStart:"", linkEnd:""}) + li!=env.t('dailyHelp5') + li!=env.t('newbieGuild', {linkStart:"", linkEnd: ""}) + ul(ng-switch-when='todo') + li=env.t('toDoHelp1') + li=env.t('toDoHelp2') + li=env.t('toDoHelp3') + li!=env.t('toDoHelp4') + li!=env.t('newbieGuild', {linkStart:"", linkEnd: ""}) + ul(ng-switch-when='reward') + li!=env.t('rewardHelp1', {linkStart:"", linkEnd: ""}) + li!=env.t('rewardHelp2', {linkStart:"", linkEnd: ""}) + li=env.t('rewardHelp3') + li!=env.t('rewardHelp4') + li!=env.t('newbieGuild', {linkStart:"", linkEnd: ""}) diff --git a/website/views/shared/tasks/task_view/index.jade b/website/views/shared/tasks/task_view/index.jade new file mode 100644 index 0000000000..f15490969d --- /dev/null +++ b/website/views/shared/tasks/task_view/index.jade @@ -0,0 +1,35 @@ +// left-hand side checkbox +.task-controls.task-primary(ng-if='!task._editing') + + // Habits + .task-actions(ng-if='::task.type=="habit"') + // score() is overridden in challengesCtrl to do nothing + a(ng-if='task.up', ng-click='applyingAction || score(task,"up")') + span.glyphicon.glyphicon-plus + a(ng-if='task.down', ng-click='applyingAction || score(task,"down")') + span.glyphicon.glyphicon-minus + + // Rewards + span(ng-if='::task.type=="reward"') + a.money.btn-buy(ng-class='{highValue: task.value >= 1000}', ng-click='score(task, "down")') + span.shop_gold + span.reward-cost {{task.value}} + + // Daily & Todos + span.task-checker.action-yesno(ng-if='::task.type=="daily" || task.type=="todo"') + input.visuallyhidden.focusable(id='box-{{::obj._id}}_{{::task.id}}', type='checkbox', + ng-model='task.completed', ng-if='$state.includes("tasks")', + ng-change='task.type=="todo" && pushTask(task,$index,"bottom"); changeCheck(task)') + input.visuallyhidden.focusable(id='box-{{::obj._id}}_{{::task.id}}', type='checkbox', + ng-if='!$state.includes("tasks")') + label(for='box-{{::obj._id}}_{{::task.id}}') + +// main content +.task-text(ng-dblclick='task._editing ? saveTask(task) : editTask(task)') + markdown(text='task.text',target='_blank') + + div(ng-if='task.checklist && !$state.includes("options.social.challenges") && !task.collapseChecklist && !task._editing') + fieldset.option-group.task-checklist + label.checkbox(ng-repeat='item in task.checklist') + input(type='checkbox', ng-model='item.completed', ng-change='saveTask(task,true)') + markdown(text='item.text', target='_blank') diff --git a/website/views/shared/tasks/task_view/mixins.jade b/website/views/shared/tasks/task_view/mixins.jade new file mode 100644 index 0000000000..775f194a76 --- /dev/null +++ b/website/views/shared/tasks/task_view/mixins.jade @@ -0,0 +1,60 @@ +mixin taskColumnTabs(position) + // Habits Tabs + div(ng-if='::main && list.type=="habit"', class='tabbable tabs-below') + ul.task-filter + li(ng-class='{active: list.view == "all"}') + a(ng-click='list.view = "all"')=env.t('all') + li(ng-class='{active: list.view == "yellowred"}') + a(ng-click='list.view = "yellowred"')=env.t('yellowred') + li(ng-class='{active: list.view == "greenblue"}') + a(ng-click='list.view = "greenblue"')=env.t('greenblue') + // Daily Tabs + div(ng-if='::main && list.type=="daily"', class='tabbable tabs-below') + // remaining/completed tabs + ul.task-filter + li(ng-class='{active: list.view == "all"}') + a(ng-click='list.view = "all"')=env.t('all') + li(ng-class='{active: list.view == "remaining"}') + a(ng-click='list.view = "remaining"')=env.t('due') + li(ng-class='{active: list.view == "complete"}') + a(ng-click='list.view = "complete"')=env.t('grey') + // Todo Tabs + div(ng-if='::main && list.type=="todo"', ng-class='::{"tabbable tabs-below": list.type=="todo"}') + if position=="bottom" + div(ng-show='list.view == "complete"') + .alert + =env.t('lotOfToDos') + button.task-action-btn.tile.spacious.bright(ng-click='user.ops.clearCompleted({})',popover=env.t('deleteToDosExplanation'),popover-trigger='mouseenter')=env.t('clearCompleted') + p!=env.t('beeminderDeleteWarning') + // remaining/completed tabs + ul.task-filter + li(ng-class='{active: list.view == "remaining"}') + a(ng-click='list.view = "remaining"')=env.t('remaining') + li(ng-class='{active: list.view == "dated"}') + a(ng-click='list.view = "dated"')=env.t('dated') + li(ng-class='{active: list.view == "complete"}') + a(ng-click='list.view = "complete"')=env.t('complete') + // Rewards Tabs + div(ng-if='::main && list.type=="reward"', class='tabbable tabs-below') + ul.task-filter + li(ng-class='{active: list.view == "all"}') + a(ng-click='list.view = "all"')=env.t('all') + li(ng-class='{active: list.view == "ingamerewards"}') + a(ng-click='list.view = "ingamerewards"')=env.t('ingamerewards') + +mixin specialSpell(k,canceler) + li.task.reward-item(ng-if='#{canceler ? "user.stats.buffs."+canceler : "user.items.special."+k+">0"}',popover-trigger='mouseenter', popover-placement='top', popover='{{Content.spells.special.#{k}.notes()}}') + .task-meta-controls + span.task-notes + span.glyphicon.glyphicon-comment + //left-hand size commands + .task-controls.task-primary + a.money.btn-buy.item-btn(ng-click='castStart(Content.spells.special.#{k})', ng-class='{active: Content.spells.special.#{k}.key == spell.key}') + if canceler + span.shop_gold + span.reward-cost {{Content.spells.special.#{k}.value}} + else + span.shop_spell(class='shop_#{k}') + span.reward-cost {{user.items.special.#{k}}} + // main content + p.task-text {{Content.spells.special.#{k}.text()}} diff --git a/website/views/shared/tasks/task_view/spells.jade b/website/views/shared/tasks/task_view/spells.jade new file mode 100644 index 0000000000..9360b0e656 --- /dev/null +++ b/website/views/shared/tasks/task_view/spells.jade @@ -0,0 +1,25 @@ +// Events +ul.items.rewards(ng-if='main && list.type=="reward" && (user.items.special.snowball>0 || user.stats.buffs.snowball || user.items.special.spookDust>0 || user.stats.buffs.spookDust || user.items.special.shinySeed>0 || user.stats.buffs.shinySeed)') + + +specialSpell('snowball') + +specialSpell('spookDust') + +specialSpell('shinySeed') + +specialSpell('salt','snowball') + +specialSpell('opaquePotion','spookDust') + +specialSpell('petalFreePotion','shinySeed') + +// Spells +ul.items(ng-if='main && list.type=="reward" && user.stats.class && !user.preferences.disableClasses') + li.task.reward-item(ng-repeat='(k,skill) in Content.spells[user.stats.class]', ng-if='user.stats.lvl >= skill.lvl',popover-trigger='mouseenter', popover-placement='top', popover='{{skill.notes()}}') + .task-meta-controls + span.task-notes + span.glyphicon.glyphicon-comment + //left-hand size commands + .task-controls.task-primary + a.money.btn-buy.item-btn(ng-click='castStart(skill)', ng-class='{active: skill.key == spell.key}') + span.reward-cost + strong {{skill.mana}} + =env.t('mp') + // main content + span(ng-class='{"shop_{{skill.key}} shop-sprite item-img": true}') + p.task-text {{skill.text()}} diff --git a/website/views/shared/tasks/task_view/static_rewards.jade b/website/views/shared/tasks/task_view/static_rewards.jade new file mode 100644 index 0000000000..7a19f66db3 --- /dev/null +++ b/website/views/shared/tasks/task_view/static_rewards.jade @@ -0,0 +1,14 @@ +ul.items.rewards(ng-if='main && list.type=="reward"') + li.task.reward-item(ng-repeat='item in itemStore',popover-trigger='mouseenter', popover-placement='top', popover='{{item.key == "armoire" && !user.flags.armoireEmpty ? env.t("armoireNotesFull") + armoireCount(user.items.gear.owned) : item.notes()}}') + // right-hand side control buttons + .task-meta-controls + span.task-notes + span.glyphicon.glyphicon-comment + //left-hand size commands + .task-controls.task-primary + a.money.btn-buy.item-btn(ng-class='{highValue: item.value >= 1000}', ng-click='buy(item)') + span.shop_gold + span.reward-cost {{item.value}} + // main content + span(ng-class='::{"shop_{{item.key}} shop-sprite item-img": true}').reward-img + p.task-text {{item.text()}}