diff --git a/bower.json b/bower.json index b9f2bfae79..e08e2674d4 100644 --- a/bower.json +++ b/bower.json @@ -43,7 +43,8 @@ "smart-app-banner": "78ef9c0679723b25be1a0ae04f7b4aef7cbced4f", "habitica-markdown": "1.2.2", "pusher-js-auth": "^2.0.0", - "pusher-websocket-iso": "pusher#^3.2.0" + "pusher-websocket-iso": "pusher#^3.2.0", + "taggle": "^1.11.1" }, "devDependencies": { "angular-mocks": "1.3.9" diff --git a/test/api/v3/integration/tasks/groups/checklists/DELETE-group_tasks_taskId_checklist_item.test.js b/test/api/v3/integration/tasks/groups/checklists/DELETE-group_tasks_taskId_checklist_item.test.js new file mode 100644 index 0000000000..ace15b6039 --- /dev/null +++ b/test/api/v3/integration/tasks/groups/checklists/DELETE-group_tasks_taskId_checklist_item.test.js @@ -0,0 +1,83 @@ +import { + createAndPopulateGroup, + translate as t, +} from '../../../../../../helpers/api-integration/v3'; +import { v4 as generateUUID } from 'uuid'; + +describe('DELETE group /tasks/:taskId/checklist/:itemId', () => { + let user, guild, task; + + before(async () => { + let {group, groupLeader} = await createAndPopulateGroup({ + groupDetails: { + name: 'Test Guild', + type: 'guild', + }, + members: 2, + }); + + guild = group; + user = groupLeader; + }); + + it('deletes a checklist item', async () => { + task = await user.post(`/tasks/group/${guild._id}`, { + type: 'daily', + text: 'Daily with checklist', + }); + + let savedTask = await user.post(`/tasks/${task._id}/checklist`, {text: 'Checklist Item 1', completed: false}); + + await user.del(`/tasks/${task._id}/checklist/${savedTask.checklist[0].id}`); + savedTask = await user.get(`/tasks/group/${guild._id}`); + + expect(savedTask[0].checklist.length).to.equal(0); + }); + + it('does not work with habits', async () => { + let habit = await user.post(`/tasks/group/${guild._id}`, { + type: 'habit', + text: 'habit with checklist', + }); + + await expect(user.del(`/tasks/${habit._id}/checklist/${generateUUID()}`)).to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: t('checklistOnlyDailyTodo'), + }); + }); + + it('does not work with rewards', async () => { + let reward = await user.post(`/tasks/group/${guild._id}`, { + type: 'reward', + text: 'reward with checklist', + }); + + await expect(user.del(`/tasks/${reward._id}/checklist/${generateUUID()}`)).to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: t('checklistOnlyDailyTodo'), + }); + }); + + it('fails on task not found', async () => { + await expect(user.del(`/tasks/${generateUUID()}/checklist/${generateUUID()}`)).to.eventually.be.rejected.and.eql({ + code: 404, + error: 'NotFound', + message: t('taskNotFound'), + }); + }); + + it('fails on checklist item not found', async () => { + let createdTask = await user.post(`/tasks/group/${guild._id}`, { + type: 'daily', + text: 'daily with checklist', + }); + + await expect(user.del(`/tasks/${createdTask._id}/checklist/${generateUUID()}`)).to.eventually.be.rejected.and.eql({ + code: 404, + error: 'NotFound', + message: t('checklistItemNotFound'), + }); + }); +}); diff --git a/test/api/v3/integration/tasks/groups/checklists/POST-group_tasks_taskId_checklist.test.js b/test/api/v3/integration/tasks/groups/checklists/POST-group_tasks_taskId_checklist.test.js new file mode 100644 index 0000000000..f6e18027a7 --- /dev/null +++ b/test/api/v3/integration/tasks/groups/checklists/POST-group_tasks_taskId_checklist.test.js @@ -0,0 +1,85 @@ +import { + createAndPopulateGroup, + translate as t, +} from '../../../../../../helpers/api-integration/v3'; +import { v4 as generateUUID } from 'uuid'; + +describe('POST group /tasks/:taskId/checklist/', () => { + let user, guild, task; + + before(async () => { + let {group, groupLeader} = await createAndPopulateGroup({ + groupDetails: { + name: 'Test Guild', + type: 'guild', + }, + members: 2, + }); + + guild = group; + user = groupLeader; + }); + + it('adds a checklist item to a task', async () => { + task = await user.post(`/tasks/group/${guild._id}`, { + type: 'daily', + text: 'Daily with checklist', + }); + + await user.post(`/tasks/${task._id}/checklist`, { + text: 'Checklist Item 1', + ignored: false, + _id: 123, + }); + + let updatedTasks = await user.get(`/tasks/group/${guild._id}`); + let updatedTask = updatedTasks[0]; + + expect(updatedTask.checklist.length).to.equal(1); + expect(updatedTask.checklist[0].text).to.equal('Checklist Item 1'); + expect(updatedTask.checklist[0].completed).to.equal(false); + expect(updatedTask.checklist[0].id).to.be.a('string'); + expect(updatedTask.checklist[0].id).to.not.equal('123'); + expect(updatedTask.checklist[0].ignored).to.be.an('undefined'); + }); + + it('does not add a checklist to habits', async () => { + let habit = await user.post(`/tasks/group/${guild._id}`, { + type: 'habit', + text: 'habit with checklist', + }); + + await expect(user.post(`/tasks/${habit._id}/checklist`, { + text: 'Checklist Item 1', + })).to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: t('checklistOnlyDailyTodo'), + }); + }); + + it('does not add a checklist to rewards', async () => { + let reward = await user.post(`/tasks/group/${guild._id}`, { + type: 'reward', + text: 'reward with checklist', + }); + + await expect(user.post(`/tasks/${reward._id}/checklist`, { + text: 'Checklist Item 1', + })).to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: t('checklistOnlyDailyTodo'), + }); + }); + + it('fails on task not found', async () => { + await expect(user.post(`/tasks/${generateUUID()}/checklist`, { + text: 'Checklist Item 1', + })).to.eventually.be.rejected.and.eql({ + code: 404, + error: 'NotFound', + message: t('taskNotFound'), + }); + }); +}); diff --git a/test/api/v3/integration/tasks/groups/checklists/PUT-group_tasks_taskId_checklist_itemId.test.js b/test/api/v3/integration/tasks/groups/checklists/PUT-group_tasks_taskId_checklist_itemId.test.js new file mode 100644 index 0000000000..27b3e19b11 --- /dev/null +++ b/test/api/v3/integration/tasks/groups/checklists/PUT-group_tasks_taskId_checklist_itemId.test.js @@ -0,0 +1,92 @@ +import { + createAndPopulateGroup, + translate as t, +} from '../../../../../../helpers/api-integration/v3'; +import { v4 as generateUUID } from 'uuid'; + +describe('PUT group /tasks/:taskId/checklist/:itemId', () => { + let user, guild, task; + + before(async () => { + let {group, groupLeader} = await createAndPopulateGroup({ + groupDetails: { + name: 'Test Guild', + type: 'guild', + }, + members: 2, + }); + + guild = group; + user = groupLeader; + }); + + it('updates a checklist item', async () => { + task = await user.post(`/tasks/group/${guild._id}`, { + type: 'daily', + text: 'Daily with checklist', + }); + + let savedTask = await user.post(`/tasks/${task._id}/checklist`, { + text: 'Checklist Item 1', + completed: false, + }); + + savedTask = await user.put(`/tasks/${task._id}/checklist/${savedTask.checklist[0].id}`, { + text: 'updated', + completed: true, + _id: 123, // ignored + }); + + expect(savedTask.checklist.length).to.equal(1); + expect(savedTask.checklist[0].text).to.equal('updated'); + expect(savedTask.checklist[0].completed).to.equal(true); + expect(savedTask.checklist[0].id).to.not.equal('123'); + }); + + it('fails on habits', async () => { + let habit = await user.post(`/tasks/group/${guild._id}`, { + type: 'habit', + text: 'habit with checklist', + }); + + await expect(user.put(`/tasks/${habit._id}/checklist/${generateUUID()}`)).to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: t('checklistOnlyDailyTodo'), + }); + }); + + it('fails on rewards', async () => { + let reward = await user.post(`/tasks/group/${guild._id}`, { + type: 'reward', + text: 'reward with checklist', + }); + + await expect(user.put(`/tasks/${reward._id}/checklist/${generateUUID()}`)).to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: t('checklistOnlyDailyTodo'), + }); + }); + + it('fails on task not found', async () => { + await expect(user.put(`/tasks/${generateUUID()}/checklist/${generateUUID()}`)).to.eventually.be.rejected.and.eql({ + code: 404, + error: 'NotFound', + message: t('taskNotFound'), + }); + }); + + it('fails on checklist item not found', async () => { + let createdTask = await user.post(`/tasks/group/${guild._id}`, { + type: 'daily', + text: 'daily with checklist', + }); + + await expect(user.put(`/tasks/${createdTask._id}/checklist/${generateUUID()}`)).to.eventually.be.rejected.and.eql({ + code: 404, + error: 'NotFound', + message: t('checklistItemNotFound'), + }); + }); +}); diff --git a/test/api/v3/integration/tasks/groups/tags/DELETE-group_tasks_taskId_tags_tagId.test.js b/test/api/v3/integration/tasks/groups/tags/DELETE-group_tasks_taskId_tags_tagId.test.js new file mode 100644 index 0000000000..05a792dee5 --- /dev/null +++ b/test/api/v3/integration/tasks/groups/tags/DELETE-group_tasks_taskId_tags_tagId.test.js @@ -0,0 +1,51 @@ +import { + createAndPopulateGroup, + translate as t, +} from '../../../../../../helpers/api-integration/v3'; +import { v4 as generateUUID } from 'uuid'; +// Currently we do not support adding tags to group original tasks, but if we do in the future, these tests will check +xdescribe('DELETE group /tasks/:taskId/tags/:tagId', () => { + let user, guild, task; + + before(async () => { + let {group, groupLeader} = await createAndPopulateGroup({ + groupDetails: { + name: 'Test Guild', + type: 'guild', + }, + members: 2, + }); + + guild = group; + user = groupLeader; + }); + + it('removes a tag from a task', async () => { + task = await user.post(`/tasks/group/${guild._id}`, { + type: 'habit', + text: 'Task with tag', + }); + + let tag = await user.post('/tags', {name: 'Tag 1'}); + + await user.post(`/tasks/${task._id}/tags/${tag.id}`); + await user.del(`/tasks/${task._id}/tags/${tag.id}`); + + let updatedTask = await user.get(`/tasks/group/${guild._id}`); + + expect(updatedTask[0].tags.length).to.equal(0); + }); + + it('only deletes existing tags', async () => { + let createdTask = await user.post(`/tasks/group/${guild._id}`, { + type: 'habit', + text: 'Task with tag', + }); + + await expect(user.del(`/tasks/${createdTask._id}/tags/${generateUUID()}`)).to.eventually.be.rejected.and.eql({ + code: 404, + error: 'NotFound', + message: t('tagNotFound'), + }); + }); +}); diff --git a/test/api/v3/integration/tasks/groups/tags/POST-tasks_taskId_tags.test.js b/test/api/v3/integration/tasks/groups/tags/POST-tasks_taskId_tags.test.js new file mode 100644 index 0000000000..bfa5f31840 --- /dev/null +++ b/test/api/v3/integration/tasks/groups/tags/POST-tasks_taskId_tags.test.js @@ -0,0 +1,64 @@ +import { + createAndPopulateGroup, + translate as t, +} from '../../../../../../helpers/api-integration/v3'; +import { v4 as generateUUID } from 'uuid'; +// Currently we do not support adding tags to group original tasks, but if we do in the future, these tests will check +xdescribe('POST group /tasks/:taskId/tags/:tagId', () => { + let user, guild, task; + + before(async () => { + let {group, groupLeader} = await createAndPopulateGroup({ + groupDetails: { + name: 'Test Guild', + type: 'guild', + }, + members: 2, + }); + + guild = group; + user = groupLeader; + }); + + it('adds a tag to a task', async () => { + task = await user.post(`/tasks/group/${guild._id}`, { + type: 'habit', + text: 'Task with tag', + }); + + let tag = await user.post('/tags', {name: 'Tag 1'}); + let savedTask = await user.post(`/tasks/${task._id}/tags/${tag.id}`); + + expect(savedTask.tags[0]).to.equal(tag.id); + }); + + it('does not add a tag to a task twice', async () => { + task = await user.post(`/tasks/group/${guild._id}`, { + type: 'habit', + text: 'Task with tag', + }); + + let tag = await user.post('/tags', {name: 'Tag 1'}); + + await user.post(`/tasks/${task._id}/tags/${tag.id}`); + + await expect(user.post(`/tasks/${task._id}/tags/${tag.id}`)).to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: t('alreadyTagged'), + }); + }); + + it('does not add a non existing tag to a task', async () => { + task = await user.post(`/tasks/group/${guild._id}`, { + type: 'habit', + text: 'Task with tag', + }); + + await expect(user.post(`/tasks/${task._id}/tags/${generateUUID()}`)).to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: t('invalidReqParams'), + }); + }); +}); diff --git a/website/client-old/css/groups.styl b/website/client-old/css/groups.styl new file mode 100644 index 0000000000..20cee8e5e5 --- /dev/null +++ b/website/client-old/css/groups.styl @@ -0,0 +1,217 @@ +group-members-autocomplete + .clearfix:before, .clearfix:after + display block + content "" + line-height 0 + clear both + + .taggle_list + float left + width 100% + li + float left + display inline-block + white-space nowrap + font-weight 500 + margin-bottom 5px + .taggle + margin-right 8px + background #E2E1DF + padding 5px 10px + border-radius 3px + position relative + cursor pointer + transition all .3s + -webkit-animation-duration 1s + animation-duration 1s + -webkit-animation-fill-mode both + animation-fill-mode both + .close + font-size 1.1rem + position absolute + top 5px + right 3px + text-decoration none + padding-left 2px + padding-top 3px + line-height 0.5 + color #ccc + color rgba(0, 0, 0, 0.2) + padding-bottom 4px + display none + border 0 + background none + cursor pointer + &:hover + color $color-purple + &:hover + padding 5px + padding-right 15px + background #ccc + transition all .3s + & > .close + display block + .taggle_hot + background #cac8c4 + + .taggle_input + border none + outline none + font-size 16px + font-weight 300 + padding 8px + padding-left 0 + float left + margin-top -5px + background none + width 100% + max-width 100% + + .taggle_placeholder + position absolute + color #CCC + top 12px + left 8px + transition opacity, .25s + -webkit-user-select none + -moz-user-select none + -ms-user-select none + user-select none + + .taggle_sizer + padding 0 + margin 0 + position absolute + top -500px + z-index -1 + visibility hidden + + textarea.input, + .textarea.input + border 0 + background #FDFDFD + box-shadow inset 0 1px 2px rgba(0, 0, 0, 0.2), 0 1px 1px rgba(255, 255, 255, 0.7) + min-height 60px + padding 8px + border-radius 3px + color #555 + transition all .25s + cursor text + margin-bottom 10px + position relative + + .textarea.input:focus, + .textarea.input.active, + textarea.input:focus, + textarea.input.active + background #fff + transition all .25s + + .textarea.input, + textarea.input + height auto + + .textarea + &.tags + position relative + * + box-sizing content-box + + .placeholder_input + position relative + span + position absolute + color #AAA + top 50% + margin-top -11px + left 10px + input + width 120px + + .ui-autocomplete + position absolute + top 0 + left 0 + max-height 200px + width 99% !important + + .ui-menu + list-style none + padding 2px + margin 0 + display block + outline none + .ui-menu-item + margin 0 + padding 0 + width 100% + a + text-decoration none + display block + padding 2px .4em + line-height 1.5 + min-height 0 + font-weight normal + color #8a8a8a + cursor pointer + &:hover + color #fff + background $color-purple + + .ui-widget-content + background #fff + color $color-purple + + .ui-state-hover, + .ui-widget-content .ui-state-hover, + .ui-widget-header .ui-state-hover, + .ui-state-focus, + .ui-widget-content .ui-state-focus, + .ui-widget-header .ui-state-focus + background $color-purple + color #fff !important + + .ui-state-hover a, + .ui-state-hover a:hover, + .ui-state-hover a:link, + .ui-state-hover a:visited + color #fff + + .ui-state-active, + .ui-widget-content .ui-state-active, + .ui-widget-header .ui-state-active + border 1px solid #aaaaaa + background #ffffff + font-weight normal + color #212121 + + .ui-helper-hidden + display none + + .ui-helper-hidden-accessible + border 0 + clip rect(0 0 0 0) + height 1px + margin -1px + overflow hidden + padding 0 + position absolute + width 1px + + .autocomplete + max-height 200px + position absolute + top 66px + background white + width 99.5% + left 0.25% + z-index 2 + ul + li + display block + padding 6px 8px + + .autocomplete ul li.selected, .autocomplete ul li:hover + background #ff6633 + color #fff + cursor pointer diff --git a/website/client-old/css/index.styl b/website/client-old/css/index.styl index 153c3c9d58..ef23201d3c 100644 --- a/website/client-old/css/index.styl +++ b/website/client-old/css/index.styl @@ -21,6 +21,7 @@ @import "./filters.styl" @import "./scrollbars.styl" @import "./game-pane.styl" +@import "./groups.styl" @import "./npcs.styl" @import "./challenges.styl" @import "./classes.styl" diff --git a/website/client-old/front/css/blockScroll.css b/website/client-old/front/css/blockScroll.css index 808faa2ef7..12ad10dd9c 100755 --- a/website/client-old/front/css/blockScroll.css +++ b/website/client-old/front/css/blockScroll.css @@ -1,10 +1,10 @@ -#main-wrap>div -{ - position: relative; - margin: auto; - text-align: center; -} - -#block-up-arrow, #block-down-arrow { - cursor: pointer; -} +#main-wrap>div +{ + position: relative; + margin: auto; + text-align: center; +} + +#block-up-arrow, #block-down-arrow { + cursor: pointer; +} diff --git a/website/client-old/front/js/blockScroll.js b/website/client-old/front/js/blockScroll.js index 193cbf1def..5ac308b313 100755 --- a/website/client-old/front/js/blockScroll.js +++ b/website/client-old/front/js/blockScroll.js @@ -1,215 +1,215 @@ -(function( $ ) { - - $.fn.blockScroll = function(options) { - var settings = $.extend({ - // These are the defaults. - startDiv : 1, - fadeDuration : "slow", - paddingRatio : 0.05, - triggerRatio : 0.005, - scrollDuration: "fast", - fadeBlocks: true - }, options ); - - if(settings.triggerRatio > settings.paddingRatio*.95) { settings.triggerRatio = settings.paddingRatio*.95 } - - var theDivs = this.children().filter("div"); - var activeDiv = settings.startDiv-1; //Active did is 0-index, settings is 1-index - var windowHeight; - var paddingHeight; - var triggerHeight; - var currentDownTrigger; - var currentUpTrigger; - var totalDivs = theDivs.length; - var lastScrollPos; - var activelyScrolling = false; - var activeBackground= 0; - - // Ensure that all of the elements are hidden just in case the css is not setup properly - if(settings.fadeBlocks) - { - this.children().each(function() { - $(this).css('opacity','0'); - }); - } - - arrange(); - // Fade in the first div - $(theDivs[activeDiv]).animate({opacity: 1},settings.fadeDuration,'linear', function() { - $(window).scrollTop(0); - calcTriggers(); - bindEvents(); - lastScrollPos = $(window).scrollTop(); - }); - - function bindEvents() - { - $(window).on('scroll', function(e) { - var scrollPosition = $(window).scrollTop(); - var scrollDistance = $(window).height(); - var indexOfClosest = 0; - - theDivs.each(function(index, element) { - var $this = $(this); - var topPosition = $this.offset().top; - var newScrollDistance = Math.abs(scrollPosition - topPosition); - if(newScrollDistance < scrollDistance) - { - indexOfClosest = index; - scrollDistance = newScrollDistance; - } - }); - gotoDiv(indexOfClosest); - }, 250); - - $(window).resize(function() { - arrange(); - }); - - $("#block-up-arrow").click(function() { - goUp(); - }); - - $("#block-down-arrow").click(function() { - goDown(); - }); - - $(document).keydown(function(e){ - if (e.keyCode == 37 || e.keyCode == 38) { - goUp(); - return false; - } - - if (e.keyCode == 39 || e.keyCode == 40) { - goDown(); - return false; - } - }); - $(window).bind('mousewheel', function(e){ - if(e.originalEvent.wheelDelta > 119) { - goUp(); - } - else if (e.originalEvent.wheelDelta < -119) { - goDown(); - } - }); - } - - function goUp() - { - if(activeDiv > 0 && !activelyScrolling) - { - gotoDiv(activeDiv-1); - } - } - - function goDown() - { - if(activeDiv < totalDivs - 1 && !activelyScrolling) - { - gotoDiv(activeDiv+1); - } - } - - function gotoDiv(number) - { - if(number == 0) - $("#block-up-arrow").hide(); - else - $("#block-up-arrow").show(); - if(number == totalDivs-1) - $("#block-down-arrow").hide(); - else - $("#block-down-arrow").show(); - activeDiv = number; - activelyScrolling = true; - $('html, body').animate({scrollTop: $(theDivs[activeDiv]).offset().top}, settings.scrollDuration, 'linear', function() { - $(theDivs[activeDiv]).animate({opacity: 1}, settings.fadeDuration,'linear', function() { - setTimeout(function(){ - activelyScrolling = false; lastScrollPos = $(window).scrollTop(); - },50); - }); - }); - calcTriggers(); - } - - function calcTriggers() - { - if (activeDiv < totalDivs -1) - { - currentDownTrigger = $(theDivs[activeDiv+1]).offset().top; - } else { - currentDownTrigger = -1; - } - - if (activeDiv > 0) { - currentUpTrigger = $(theDivs[activeDiv-1]).offset().top; - } else { - currentUpTrigger = -1; - } - } - - function calcDims() - { - windowHeight = $(window).height(); - paddingHeight = windowHeight * settings.paddingRatio; - triggerHeight = windowHeight * settings.triggerRatio; - } - - - function arrange() - { - calcDims(); - theDivs.each(function(index, element) { - var $this = $(this); - $this.height('auto'); - if($this.height() < windowHeight) - { - var margin = windowHeight/2 - $this.height()/2; - $this.height(windowHeight-margin); - $this.css('padding-top', margin + "px"); - var $innerDiv = $($this.children().filter('div')[0]); - // $innerDiv.css('padding-top', margin + "px"); - } - if(index != totalDivs - 1) - { - //$this.css('padding-bottom',paddingHeight + 'px'); - } - }); - gotoDiv(activeDiv); - } - - var gotoView = function(number) - { - gotoDiv(number-1); - } - - return { - goto: gotoView - }; - } - -}( jQuery )); - -;(function ($) { - var on = $.fn.on, timer; - $.fn.on = function () { - var args = Array.apply(null, arguments); - var last = args[args.length - 1]; - - if (isNaN(last) || (last === 1 && args.pop())) return on.apply(this, args); - - var delay = args.pop(); - var fn = args.pop(); - - args.push(function () { - var self = this, params = arguments; - clearTimeout(timer); - timer = setTimeout(function () { - fn.apply(self, params); - }, delay); - }); - - return on.apply(this, args); - }; -}(this.jQuery || this.Zepto)); +(function( $ ) { + + $.fn.blockScroll = function(options) { + var settings = $.extend({ + // These are the defaults. + startDiv : 1, + fadeDuration : "slow", + paddingRatio : 0.05, + triggerRatio : 0.005, + scrollDuration: "fast", + fadeBlocks: true + }, options ); + + if(settings.triggerRatio > settings.paddingRatio*.95) { settings.triggerRatio = settings.paddingRatio*.95 } + + var theDivs = this.children().filter("div"); + var activeDiv = settings.startDiv-1; //Active did is 0-index, settings is 1-index + var windowHeight; + var paddingHeight; + var triggerHeight; + var currentDownTrigger; + var currentUpTrigger; + var totalDivs = theDivs.length; + var lastScrollPos; + var activelyScrolling = false; + var activeBackground= 0; + + // Ensure that all of the elements are hidden just in case the css is not setup properly + if(settings.fadeBlocks) + { + this.children().each(function() { + $(this).css('opacity','0'); + }); + } + + arrange(); + // Fade in the first div + $(theDivs[activeDiv]).animate({opacity: 1},settings.fadeDuration,'linear', function() { + $(window).scrollTop(0); + calcTriggers(); + bindEvents(); + lastScrollPos = $(window).scrollTop(); + }); + + function bindEvents() + { + $(window).on('scroll', function(e) { + var scrollPosition = $(window).scrollTop(); + var scrollDistance = $(window).height(); + var indexOfClosest = 0; + + theDivs.each(function(index, element) { + var $this = $(this); + var topPosition = $this.offset().top; + var newScrollDistance = Math.abs(scrollPosition - topPosition); + if(newScrollDistance < scrollDistance) + { + indexOfClosest = index; + scrollDistance = newScrollDistance; + } + }); + gotoDiv(indexOfClosest); + }, 250); + + $(window).resize(function() { + arrange(); + }); + + $("#block-up-arrow").click(function() { + goUp(); + }); + + $("#block-down-arrow").click(function() { + goDown(); + }); + + $(document).keydown(function(e){ + if (e.keyCode == 37 || e.keyCode == 38) { + goUp(); + return false; + } + + if (e.keyCode == 39 || e.keyCode == 40) { + goDown(); + return false; + } + }); + $(window).bind('mousewheel', function(e){ + if(e.originalEvent.wheelDelta > 119) { + goUp(); + } + else if (e.originalEvent.wheelDelta < -119) { + goDown(); + } + }); + } + + function goUp() + { + if(activeDiv > 0 && !activelyScrolling) + { + gotoDiv(activeDiv-1); + } + } + + function goDown() + { + if(activeDiv < totalDivs - 1 && !activelyScrolling) + { + gotoDiv(activeDiv+1); + } + } + + function gotoDiv(number) + { + if(number == 0) + $("#block-up-arrow").hide(); + else + $("#block-up-arrow").show(); + if(number == totalDivs-1) + $("#block-down-arrow").hide(); + else + $("#block-down-arrow").show(); + activeDiv = number; + activelyScrolling = true; + $('html, body').animate({scrollTop: $(theDivs[activeDiv]).offset().top}, settings.scrollDuration, 'linear', function() { + $(theDivs[activeDiv]).animate({opacity: 1}, settings.fadeDuration,'linear', function() { + setTimeout(function(){ + activelyScrolling = false; lastScrollPos = $(window).scrollTop(); + },50); + }); + }); + calcTriggers(); + } + + function calcTriggers() + { + if (activeDiv < totalDivs -1) + { + currentDownTrigger = $(theDivs[activeDiv+1]).offset().top; + } else { + currentDownTrigger = -1; + } + + if (activeDiv > 0) { + currentUpTrigger = $(theDivs[activeDiv-1]).offset().top; + } else { + currentUpTrigger = -1; + } + } + + function calcDims() + { + windowHeight = $(window).height(); + paddingHeight = windowHeight * settings.paddingRatio; + triggerHeight = windowHeight * settings.triggerRatio; + } + + + function arrange() + { + calcDims(); + theDivs.each(function(index, element) { + var $this = $(this); + $this.height('auto'); + if($this.height() < windowHeight) + { + var margin = windowHeight/2 - $this.height()/2; + $this.height(windowHeight-margin); + $this.css('padding-top', margin + "px"); + var $innerDiv = $($this.children().filter('div')[0]); + // $innerDiv.css('padding-top', margin + "px"); + } + if(index != totalDivs - 1) + { + //$this.css('padding-bottom',paddingHeight + 'px'); + } + }); + gotoDiv(activeDiv); + } + + var gotoView = function(number) + { + gotoDiv(number-1); + } + + return { + goto: gotoView + }; + } + +}( jQuery )); + +;(function ($) { + var on = $.fn.on, timer; + $.fn.on = function () { + var args = Array.apply(null, arguments); + var last = args[args.length - 1]; + + if (isNaN(last) || (last === 1 && args.pop())) return on.apply(this, args); + + var delay = args.pop(); + var fn = args.pop(); + + args.push(function () { + var self = this, params = arguments; + clearTimeout(timer); + timer = setTimeout(function () { + fn.apply(self, params); + }, delay); + }); + + return on.apply(this, args); + }; +}(this.jQuery || this.Zepto)); diff --git a/website/client-old/js/app.js b/website/client-old/js/app.js index e37de136dc..f811eab17b 100644 --- a/website/client-old/js/app.js +++ b/website/client-old/js/app.js @@ -159,11 +159,11 @@ window.habitrpg = angular.module('habitrpg', url: '/:gid', templateUrl: 'partials/options.social.guilds.detail.html', title: env.t('titleGuilds'), - controller: ['$scope', 'Groups', 'Chat', '$stateParams', 'Members', 'Challenges', - function($scope, Groups, Chat, $stateParams, Members, Challenges){ + controller: ['$scope', 'Groups', 'Chat', '$stateParams', 'Members', 'Challenges', 'Tasks', + function($scope, Groups, Chat, $stateParams, Members, Challenges, Tasks) { Groups.Group.get($stateParams.gid) .then(function (response) { - $scope.group = response.data.data; + $scope.obj = $scope.group = response.data.data; Chat.markChatSeen($scope.group._id); Members.getGroupMembers($scope.group._id) .then(function (response) { @@ -177,7 +177,16 @@ window.habitrpg = angular.module('habitrpg', .then(function (response) { $scope.group.challenges = response.data.data; }); - }); + //@TODO: Add this back when group tasks go live + // return Tasks.getGroupTasks($scope.group._id); + }) + // .then(function (response) { + // var tasks = response.data.data; + // tasks.forEach(function (element, index, array) { + // if (!$scope.group[element.type + 's']) $scope.group[element.type + 's'] = []; + // $scope.group[element.type + 's'].push(element); + // }) + // }); }] }) diff --git a/website/client-old/js/components/groupMembersAutocomplete/groupMembersAutocompleteDirective.js b/website/client-old/js/components/groupMembersAutocomplete/groupMembersAutocompleteDirective.js new file mode 100644 index 0000000000..d8c28b38d7 --- /dev/null +++ b/website/client-old/js/components/groupMembersAutocomplete/groupMembersAutocompleteDirective.js @@ -0,0 +1,73 @@ +'use strict'; + +(function(){ + angular + .module('habitrpg') + .directive('groupMembersAutocomplete', groupMembersAutocomplete); + + groupMembersAutocomplete.$inject = [ + '$parse', + '$rootScope', + ]; + + function groupMembersAutocomplete($parse, $rootScope) { + + return { + templateUrl: 'partials/groups.members.autocomplete.html', + compile: function (element, attrs) { + var modelAccessor = $parse(attrs.ngModel); + + return function (scope, element, attrs, controller) { + var availableTags = _.pluck(scope.group.members, 'profile.name'); + var memberProfileNameToIdMap = _.object(_.map(scope.group.members, function(item) { + return [item.profile.name, item.id] + })); + var memberIdToProfileNameMap = _.object(_.map(scope.group.members, function(item) { + return [item.id, item.profile.name] + })); + + var currentTags = []; + _.each(scope.task.group.assignedUsers, function(userId) { currentTags.push(memberIdToProfileNameMap[userId]) }) + + var taggle = new Taggle('taggle', { + tags: currentTags, + allowedTags: currentTags, + allowDuplicates: false, + onBeforeTagAdd: function(event, tag) { + return confirm(window.env.t('confirmAddTag', {tag: tag})); + }, + onTagAdd: function(event, tag) { + $rootScope.$broadcast('addedGroupMember', memberProfileNameToIdMap[tag]); + }, + onBeforeTagRemove: function(event, tag) { + return confirm(window.env.t('confirmRemoveTag', {tag: tag})) + }, + onTagRemove: function(event, tag) { + $rootScope.$broadcast('removedGroupMember', memberProfileNameToIdMap[tag]); + } + }); + var container = taggle.getContainer(); + var input = taggle.getInput(); + + $(input).autocomplete({ + source: availableTags, // See jQuery UI documentaton for options + appendTo: container, + position: { at: "left bottom", of: container }, + select: function(event, data) { + event.preventDefault(); + //Add the tag if user clicks + if (event.which === 1) { + taggle.add(data.item.value); + var taggleTags = taggle.getTags(); + scope.$apply(function (scope) { + // Change bound variable + modelAccessor.assign(scope, taggleTags.values); + }); + } + } + }); + }; + }, + }; + } +}()); diff --git a/website/client-old/js/components/groupTaskActions/groupTaskActionsController.js b/website/client-old/js/components/groupTaskActions/groupTaskActionsController.js new file mode 100644 index 0000000000..677ead8bfc --- /dev/null +++ b/website/client-old/js/components/groupTaskActions/groupTaskActionsController.js @@ -0,0 +1,13 @@ +habitrpg.controller('GroupTaskActionsCtrl', ['$scope', 'Shared', 'Tasks', 'User', + function ($scope, Shared, Tasks, User) { + $scope.assignedMembers = []; + $scope.user = User.user; + + $scope.$on('addedGroupMember', function(evt, userId) { + Tasks.assignTask($scope.task.id, userId); + }); + + $scope.$on('removedGroupMember', function(evt, userId) { + Tasks.unAssignTask($scope.task.id, userId); + }); + }]); diff --git a/website/client-old/js/components/groupTaskActions/groupTaskActionsDirective.js b/website/client-old/js/components/groupTaskActions/groupTaskActionsDirective.js new file mode 100644 index 0000000000..0fdd32a0ed --- /dev/null +++ b/website/client-old/js/components/groupTaskActions/groupTaskActionsDirective.js @@ -0,0 +1,22 @@ +'use strict'; + +(function(){ + angular + .module('habitrpg') + .directive('groupTasksActions', hrpgSortTags); + + hrpgSortTags.$inject = [ + ]; + + function hrpgSortTags() { + + return { + scope: { + task: '=', + group: '=', + }, + templateUrl: 'partials/groups.tasks.actions.html', + controller: 'GroupTaskActionsCtrl', + }; + } +}()); diff --git a/website/client-old/js/components/groupTasks/groupTasksController.js b/website/client-old/js/components/groupTasks/groupTasksController.js new file mode 100644 index 0000000000..fa7eded874 --- /dev/null +++ b/website/client-old/js/components/groupTasks/groupTasksController.js @@ -0,0 +1,79 @@ +habitrpg.controller('GroupTasksCtrl', ['$scope', 'Shared', 'Tasks', 'User', function ($scope, Shared, Tasks, User) { + $scope.editTask = Tasks.editTask; + $scope.toggleBulk = Tasks.toggleBulk; + $scope.cancelTaskEdit = Tasks.cancelTaskEdit; + + function addTask (listDef, task) { + var task = Shared.taskDefaults({text: task, type: listDef.type}); + //If the group has not been created, we bulk add tasks on save + var group = $scope.obj; + if (group._id) Tasks.createGroupTasks(group._id, task); + if (!group[task.type + 's']) group[task.type + 's'] = []; + group[task.type + 's'].unshift(task); + delete listDef.newTask; + }; + + $scope.addTask = function(listDef) { + Tasks.addTasks(listDef, addTask); + }; + + $scope.removeTask = function(task, group) { + if (!Tasks.removeTask(task)) return; + //We only pass to the api if the group exists, otherwise, the tasks only exist on the client + if (group._id) Tasks.deleteTask(task._id); + var index = group[task.type + 's'].indexOf(task); + group[task.type + 's'].splice(index, 1); + }; + + $scope.saveTask = function(task, stayOpen, isSaveAndClose) { + Tasks.saveTask (task, stayOpen, isSaveAndClose); + Tasks.updateTask(task._id, task); + }; + + $scope.shouldShow = function(task, list, prefs){ + return true; + }; + + $scope.canEdit = function(task) { + return true; + }; + + /* + ------------------------ + Tags + ------------------------ + */ + $scope.updateTaskTags = function (tagId, task) { + var tagIndex = task.tags.indexOf(tagId); + if (tagIndex === -1) { + Tasks.addTagToTask(task._id, tagId); + task.tags.push(tagId); + } else { + Tasks.removeTagFromTask(task._id, tagId); + task.tags.splice(tagIndex, 1); + } + }; + + /* + ------------------------ + Checklists + ------------------------ + */ + $scope.addChecklist = Tasks.addChecklist; + + $scope.addChecklistItem = Tasks.addChecklistItemToUI; + + $scope.removeChecklistItem = Tasks.removeChecklistItemFromUI; + + $scope.swapChecklistItems = Tasks.swapChecklistItems; + + $scope.navigateChecklist = Tasks.navigateChecklist; + + $scope.checklistCompletion = Tasks.checklistCompletion; + + $scope.collapseChecklist = function (task) { + Tasks.collapseChecklist(task); + //@TODO: Currently the api save of the task is separate, so whenever we need to save the task we need to call the respective api + Tasks.updateTask(task._id, task); + }; + }]); diff --git a/website/client-old/js/components/groupTasks/groupTasksDirective.js b/website/client-old/js/components/groupTasks/groupTasksDirective.js new file mode 100644 index 0000000000..88c12cb74e --- /dev/null +++ b/website/client-old/js/components/groupTasks/groupTasksDirective.js @@ -0,0 +1,21 @@ +'use strict'; + +(function(){ + angular + .module('habitrpg') + .directive('groupTasks', hrpgSortTags); + + hrpgSortTags.$inject = [ + ]; + + function hrpgSortTags() { + + return { + scope: true, + templateUrl: 'partials/groups.tasks.html', + controller: 'GroupTasksCtrl', + link: function($scope, element, attrs, ngModel) { + }, + }; + } +}()); diff --git a/website/client-old/js/controllers/tasksCtrl.js b/website/client-old/js/controllers/tasksCtrl.js index 5517c8dd89..b10d6e3949 100644 --- a/website/client-old/js/controllers/tasksCtrl.js +++ b/website/client-old/js/controllers/tasksCtrl.js @@ -101,24 +101,8 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','N }; $scope.saveTask = function(task, stayOpen, isSaveAndClose) { - if (task._edit) { - angular.copy(task._edit, task); - } - task._edit = undefined; - - if (task.checklist) { - task.checklist = _.filter(task.checklist, function (i) { - return !!i.text - }); - } - + Tasks.saveTask (task, stayOpen, isSaveAndClose); User.updateTask(task, {body: task}); - if (!stayOpen) task._editing = false; - - if (isSaveAndClose) { - $("#task-" + task._id).parent().children('.popover').removeClass('in'); - } - if (task.type == 'habit') Guide.goto('intro', 3); }; @@ -132,8 +116,7 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','N $scope.cancelTaskEdit = Tasks.cancelTaskEdit; $scope.removeTask = function(task) { - if (!confirm(window.env.t('sureDelete', {taskType: window.env.t(task.type), taskText: task.text}))) return; - task._edit = undefined; + if (!Tasks.removeTask(task)) return; User.deleteTask({params:{id: task._id, taskType: task.type}}) }; @@ -190,60 +173,28 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','N Checklists ------------------------ */ - function focusChecklist(task,index) { - window.setTimeout(function(){ - $('#task-'+task._id+' .checklist-form input[type="text"]')[index].focus(); - }); - } + /* + ------------------------ + Checklists + ------------------------ + */ + $scope.addChecklist = Tasks.addChecklist; - $scope.addChecklist = function(task) { - task._edit.checklist = [{completed:false, text:""}]; - focusChecklist(task._edit,0); - } + $scope.addChecklistItem = Tasks.addChecklistItemToUI; - $scope.addChecklistItem = function(task, $event, $index) { - if (task._edit.checklist[$index].text) { - if ($index === task._edit.checklist.length - 1) { - task._edit.checklist.push({ completed: false, text: '' }); - } - focusChecklist(task._edit, $index + 1); - } else { - // TODO Provide UI feedback that this item is still blank - } - } + $scope.removeChecklistItem = Tasks.removeChecklistItemFromUI; - $scope.removeChecklistItem = function(task, $event, $index, force) { - // Remove item if clicked on trash icon - if (force) { - task._edit.checklist.splice($index, 1); - } else if (!task._edit.checklist[$index].text) { - // User deleted all the text and is now wishing to delete the item - // saveTask will prune the empty item - // Move focus if the list is still non-empty - if ($index > 0) - focusChecklist(task._edit, $index-1); - // Don't allow the backspace key to navigate back now that the field is gone - $event.preventDefault(); - } - } + $scope.swapChecklistItems = Tasks.swapChecklistItems; - $scope.swapChecklistItems = function(task, oldIndex, newIndex) { - var toSwap = task._edit.checklist.splice(oldIndex, 1)[0]; - task._edit.checklist.splice(newIndex, 0, toSwap); - } + $scope.navigateChecklist = Tasks.navigateChecklist; - $scope.navigateChecklist = function(task,$index,$event){ - focusChecklist(task, $event.keyCode == '40' ? $index+1 : $index-1); - } + $scope.checklistCompletion = Tasks.checklistCompletion; - $scope.checklistCompletion = function(checklist){ - return _.reduce(checklist,function(m,i){return m+(i.completed ? 1 : 0);},0) - } - - $scope.collapseChecklist = function(task) { - task.collapseChecklist = !task.collapseChecklist; - $scope.saveTask(task,true); - } + $scope.collapseChecklist = function (task) { + Tasks.collapseChecklist(task); + //@TODO: Currently the api save of the task is separate, so whenever we need to save the task we need to call the respective api + Tasks.updateTask(task._id, task); + }; /* ------------------------ diff --git a/website/client-old/js/services/taskServices.js b/website/client-old/js/services/taskServices.js index 545b1c2a19..833ccefba7 100644 --- a/website/client-old/js/services/taskServices.js +++ b/website/client-old/js/services/taskServices.js @@ -29,6 +29,33 @@ angular.module('habitrpg') list.focus = true; }; + function removeTask (task) { + if (!confirm(window.env.t('sureDelete', {taskType: window.env.t(task.type), taskText: task.text}))) { + return false; + }; + task._edit = undefined; + return true; + } + + function saveTask (task, stayOpen, isSaveAndClose) { + if (task._edit) { + angular.copy(task._edit, task); + } + task._edit = undefined; + + if (task.checklist) { + task.checklist = _.filter(task.checklist, function (i) { + return !!i.text + }); + } + + if (!stayOpen) task._editing = false; + + if (isSaveAndClose) { + $("#task-" + task._id).parent().children('.popover').removeClass('in'); + } + } + function getUserTasks (getCompletedTodos) { var url = '/api/v3/tasks/user'; @@ -64,6 +91,21 @@ angular.module('habitrpg') }); }; + function getGroupTasks (groupId) { + return $http({ + method: 'GET', + url: '/api/v3/tasks/group/' + groupId, + }); + }; + + function createGroupTasks (groupId, taskDetails) { + return $http({ + method: 'POST', + url: '/api/v3/tasks/group/' + groupId, + data: taskDetails, + }); + }; + function getTask (taskId) { return $http({ method: 'GET', @@ -175,6 +217,20 @@ angular.module('habitrpg') }); }; + function assignTask (taskId, userId) { + return $http({ + method: 'POST', + url: '/api/v3/tasks/' + taskId + '/assign/' + userId, + }); + }; + + function unAssignTask (taskId, userId) { + return $http({ + method: 'POST', + url: '/api/v3/tasks/' + taskId + '/unassign/' + userId, + }); + }; + function editTask(task, user) { task._editing = true; task._tags = !user.preferences.tagsCollapsed; @@ -211,14 +267,79 @@ angular.module('habitrpg') return cleansedTask; } + /* + ------------------------ + Checklists + ------------------------ + */ + + function focusChecklist(task, index) { + window.setTimeout(function(){ + $('#task-'+task._id+' .checklist-form input[type="text"]')[index].focus(); + }); + } + + function addChecklist(task) { + task._edit.checklist = [{completed:false, text:""}]; + focusChecklist(task._edit,0); + } + + function addChecklistItemToUI(task, $event, $index) { + if (task._edit.checklist[$index].text) { + if ($index === task._edit.checklist.length - 1) { + task._edit.checklist.push({ completed: false, text: '' }); + } + focusChecklist(task._edit, $index + 1); + } else { + // TODO Provide UI feedback that this item is still blank + } + } + + function removeChecklistItemFromUI(task, $event, $index, force) { + // Remove item if clicked on trash icon + if (force) { + task._edit.checklist.splice($index, 1); + } else if (!task._edit.checklist[$index].text) { + // User deleted all the text and is now wishing to delete the item + // saveTask will prune the empty item + // Move focus if the list is still non-empty + if ($index > 0) + focusChecklist(task._edit, $index-1); + // Don't allow the backspace key to navigate back now that the field is gone + $event.preventDefault(); + } + } + + function swapChecklistItems(task, oldIndex, newIndex) { + var toSwap = task._edit.checklist.splice(oldIndex, 1)[0]; + task._edit.checklist.splice(newIndex, 0, toSwap); + } + + function navigateChecklist(task,$index,$event) { + focusChecklist(task, $event.keyCode == '40' ? $index+1 : $index-1); + } + + function checklistCompletion(checklist) { + return _.reduce(checklist,function(m,i){return m+(i.completed ? 1 : 0);},0) + } + + function collapseChecklist(task) { + task.collapseChecklist = !task.collapseChecklist; + saveTask(task, true); + } + return { addTasks: addTasks, toggleBulk: toggleBulk, getUserTasks: getUserTasks, + removeTask: removeTask, + saveTask: saveTask, loadedCompletedTodos: false, createUserTasks: createUserTasks, getChallengeTasks: getChallengeTasks, createChallengeTasks: createChallengeTasks, + getGroupTasks: getGroupTasks, + createGroupTasks: createGroupTasks, getTask: getTask, updateTask: updateTask, deleteTask: deleteTask, @@ -235,6 +356,16 @@ angular.module('habitrpg') clearCompletedTodos: clearCompletedTodos, editTask: editTask, cancelTaskEdit: cancelTaskEdit, - cloneTask: cloneTask + cloneTask: cloneTask, + assignTask: assignTask, + unAssignTask: unAssignTask, + + addChecklist: addChecklist, + addChecklistItemToUI: addChecklistItemToUI, + removeChecklistItemFromUI: removeChecklistItemFromUI, + swapChecklistItems: swapChecklistItems, + navigateChecklist: navigateChecklist, + checklistCompletion: checklistCompletion, + collapseChecklist: collapseChecklist, }; }]); diff --git a/website/client-old/logo/habitrpg_bl.eps b/website/client-old/logo/habitrpg_bl.eps index c4e871c2cf..b67581a928 100644 --- a/website/client-old/logo/habitrpg_bl.eps +++ b/website/client-old/logo/habitrpg_bl.eps @@ -10,7 +10,7 @@ %%LanguageLevel: 2 %%DocumentData: Clean7Bit %ADOBeginClientInjection: DocumentHeader "AI11EPS" -%%AI8_CreatorVersion: 14.0.0 %AI9_PrintingDataBegin %ADO_BuildNumber: Adobe Illustrator(R) 14.0.0 x367 R agm 4.4890 ct 5.1541 %ADO_ContainsXMP: MainFirst %AI7_Thumbnail: 128 40 8 %%BeginData: 8480 Hex Bytes %0000330000660000990000CC0033000033330033660033990033CC0033FF %0066000066330066660066990066CC0066FF009900009933009966009999 %0099CC0099FF00CC0000CC3300CC6600CC9900CCCC00CCFF00FF3300FF66 %00FF9900FFCC3300003300333300663300993300CC3300FF333300333333 %3333663333993333CC3333FF3366003366333366663366993366CC3366FF %3399003399333399663399993399CC3399FF33CC0033CC3333CC6633CC99 %33CCCC33CCFF33FF0033FF3333FF6633FF9933FFCC33FFFF660000660033 %6600666600996600CC6600FF6633006633336633666633996633CC6633FF %6666006666336666666666996666CC6666FF669900669933669966669999 %6699CC6699FF66CC0066CC3366CC6666CC9966CCCC66CCFF66FF0066FF33 %66FF6666FF9966FFCC66FFFF9900009900339900669900999900CC9900FF %9933009933339933669933999933CC9933FF996600996633996666996699 %9966CC9966FF9999009999339999669999999999CC9999FF99CC0099CC33 %99CC6699CC9999CCCC99CCFF99FF0099FF3399FF6699FF9999FFCC99FFFF %CC0000CC0033CC0066CC0099CC00CCCC00FFCC3300CC3333CC3366CC3399 %CC33CCCC33FFCC6600CC6633CC6666CC6699CC66CCCC66FFCC9900CC9933 %CC9966CC9999CC99CCCC99FFCCCC00CCCC33CCCC66CCCC99CCCCCCCCCCFF %CCFF00CCFF33CCFF66CCFF99CCFFCCCCFFFFFF0033FF0066FF0099FF00CC %FF3300FF3333FF3366FF3399FF33CCFF33FFFF6600FF6633FF6666FF6699 %FF66CCFF66FFFF9900FF9933FF9966FF9999FF99CCFF99FFFFCC00FFCC33 %FFCC66FFCC99FFCCCCFFCCFFFFFF33FFFF66FFFF99FFFFCC110000001100 %000011111111220000002200000022222222440000004400000044444444 %550000005500000055555555770000007700000077777777880000008800 %000088888888AA000000AA000000AAAAAAAABB000000BB000000BBBBBBBB %DD000000DD000000DDDDDDDDEE000000EE000000EEEEEEEE0000000000FF %00FF0000FFFFFF0000FF00FFFFFF00FFFFFF %524C455227522752275227FFA85227522752275227FD05FFA85227522752 %275252FD05FF7D522752275227522752527DA8FD05FF5252275227522752 %7DFF275227522752275227522752277DA8522752275227522752527DA8FD %05FF2752275227522752275252A8FD08FFA852FD05F87DFFFFFFFD08F8FF %A8FD08F8FD05FFA8FD07F827FD05FF7DFD0CF852FD04FF27FD07F87D7DFD %0DF852A8FD0CF827FD04FFFD0CF827A8FD05FFA827F8F8F827F8F8F852FF %FFFFA827F8F8F8FD05FFA8F8F8F827FD07FF7DF8F8F82727F8F8F8FD07FF %52F8F8F852A87DA852F8F8F852FD05FF27F8F8F852FFFFA8F8F87DA87DF8 %F8F852A8A827F87DFFFF7DF8F8F852A8A8A852F8F8F827FD05FF27F8F8F8 %A8A87DA827F8F827A8FD04FF27F8F827FFFF7DF8F8F87DFFFFFF27F8F827 %FD06FFF8F8F827FD07FF7DF8F8F8A827F8F8F8FD07FFA8F8F8F87DFD04FF %52F8F8F87DFD04FF7DF8F8F8A8FFFF7DF8F8FFFFA8F8F8F87DFFFF52F852 %FFFFFFF8F8F87DFD04FF52F8F8F87DFD04FF27F8F8F8FD05FFF8F8F827FF %FFFFA8F8F8F87DFFFFFF27F8F8F8FFFFFF52F8F827FD06FFF8F8F827FD07 %FF52F8F8F8A87DF8F8F8FD07FFA8F8F8F852FD05FFF8F8F852FD04FF52F8 %F8F8A8FFFFA8F852FFFFA8F8F8F852FFFFA8F87DFFFFFFF8F8F852FD05FF %27F8F827FD04FF27F8F8F8FD05FF7DF8F8F8A8FFFF52F8F8F8FD04FF7DF8 %F8F8A8FFFF27F8F827FD06FFF8F8F827FD07FF52F8F8F8FF52F8F8F87DFD %06FFA8F8F8F87DFD05FF52F8F8F8FD04FF7DF8F8F8A8FFFFA8277DFFFFA8 %F8F8F87DFFFFA8277DFFFFFFF8F8F87DFD05FF52F8F8F8FD04FF27F8F8F8 %FD05FFA8F8F8F852FFFF27F8F827FD04FFA8F8F8F87DFFFF27F8F827FD06 %FFF8F8F827FD07FF27F8F8F8FF7DF8F8F8A8FD06FF7DF8F8F852FD05FF52 %F8F8F8A8FFFFFF52F8F8F8A8FD07FFA8F8F8F852FD08FFF8F8F852FD05FF %7DF8F8F87DFFFFFF27F8F8F8FD06FFF8F8F852FFFFF8F8F852FD05FFF8F8 %F827FFFF27F8F827FD06FFF8F8F827FD07FF27F8F827FF7DF8F8F87DFD06 %FFA8F8F8F87DFD05FFA8F8F8F8A8FFFFFF7DF8F8F8A8FD07FFA8F8F8F87D %FD08FFF8F8F87DFD05FFA8F8F8F87DFFFFFF27F8F8F8FD06FF27F8F827FF %A8F8F8F852FD05FFF8F8F827FFFF52F8F827FD06FFF8F8F827FD07FFF8F8 %F852FFA8F8F8F87DFD06FFA8F8F8F852FD05FFA8F8F8F8A8FFFFFF52F8F8 %F8A8FD07FFA8F8F8F852FD08FFF8F8F852FD05FFA8F8F8F852FFFFFF27F8 %F8F8FD06FF27F8F852FFA8F8F8F87DFD05FF52F8F8F8FFFF27F8F827FD06 %FFF8F8F827FD07FFF8F8F827FFA8F8F8F852FD06FFA8F8F8F87DFD05FFA8 %F8F8F8A8FFFFFF7DF8F8F8A8FD07FFA8F8F8F87DFD08FFF8F8F87DFD05FF %A8F8F8F87DFFFFFF27F8F8F8FD06FF27F8F827FF7DF8F8F87DFD05FF27F8 %F8F8FFFF52F8F827FD06FFF8F8F827FD07FFF8F8F87DFFFFF8F8F852FD06 %FFA8F8F8F852FD05FF7DF8F8F87DFFFFFF52F8F8F87DFD07FF7DF8F8F852 %FD08FFF8F8F852FD05FFA8F8F8F852FFFFFF27F8F8F8FD06FF27F8F827FF %A8F8F8F8A8FD05FF7DF8F8F8FFFF27F8F827FD06FFF8F8F827FD06FF7DF8 %F8F852FFFFF8F8F827FD06FFA8F8F8F87DFD05FF7DF8F8F8A8FFFFFF7DF8 %F8F8A8FD07FFA8F8F8F87DFD08FFF8F8F87DFD05FFA8F8F8F87DFFFFFF27 %F8F8F8FD06FF27F8F827FF7DF8F8F87DFD05FF7D7D527DFFFF52F8F827FD %06FFF8F8F827FD06FFA8F8F8F8A8FFFF27F8F852FD06FFA8F8F8F852FD05 %FF52F8F8F8A8FFFFFF52F8F8F8A8FD07FFA8F8F8F852FD08FFF8F8F852FD %05FF7DF8F8F8A8FFFFFF27F8F8F8FD06FFF8F8F852FF7DF8F8F8A8FD0BFF %27F8F827FD06FFF8F8F827FD06FF7DF8F8F87DFFFF27F8F827FD06FFA8F8 %F8F87DFD05FF27F8F8F8FD04FF7DF8F8F8A8FD07FFA8F8F8F87DFD08FFF8 %F8F87DFD05FF52F8F8F8FD04FF27F8F8F8FD05FF7DF8F8F87DFF52F8F8F8 %7DFD0BFF27F8F827FD06FFF8F8F827FD06FF7DF8F8F8A8FFFF27F8F8F8FD %06FF7DF8F8F852FD05FFF8F8F852FD04FF52F8F8F8A8FD07FFA8F8F8F852 %FD08FFF8F8F852FD05FFF8F8F827FD04FF27F8F8F8FD05FF52F8F8F8FFFF %7DF8F8F8A8FD0BFF27F8F827FD06FFF8F8F827FD06FF52F8F8F8A8FFFF7D %F8F8F8FD06FFA8F8F8F87DFFFFFFA8FD04F87DFD04FF7DF8F8F8A8FD07FF %A8F8F8F87DFD08FFF8F8F87DFD04FF27F8F8F87DFD04FF27F8F8F8FD04FF %7DF8F8F827FFFF52F8F8F87DFD0BFF52FD0CF827FD06FF7DFD0AF8A8FD05 %FFA8FD0BF852FD05FF52F8F8F8A8FD07FFA8F8F8F852FD08FFFD0BF852FD %05FF27FD0AF827A8FFFF7DF8F8F8A8FD0BFF27FD0CF827FD06FF27FD0AF8 %A8FD05FFA8FD0BF8A8FD05FF7DF8F8F8A8FD07FFA8F8F8F87DFD08FFFD0A %F852FD06FF27FD09F827A8FFFFFF52F8F8F87DFFFFFF7DA87DA87DA8FFFF %52F8F8F87D527D527D52F8F8F827FD06FF52F8F8F87D527D52F8F8F87DFD %05FFA8F8F8F8277D527D52FD04F8FD05FF52F8F8F87DFD07FF7DF8F8F852 %FD08FFF8F8F8277D5227F8F8F8FD07FF27F8F8F8527D527D7DA8FD05FF7D %F8F8F8A8FFFF52FD06F8FFFF27F8F827FD06FFF8F8F827FD06FFF8F8F827 %FFFFFFA8F8F8F87DFD05FFA8F8F8F87DFD04FF7DF8F8F827FD04FF7DF8F8 %F8A8FD07FFA8F8F8F87DFD08FFF8F8F87DFFFF52F8F8F8A8FD06FF27F8F8 %F8FD0BFF52F8F8F87DFFFF7DFD06F8FFFF52F8F827FD06FFF8F8F827FD06 %FFF8F8F827FFFFFFA8F8F8F852FD05FFA8F8F8F852FD05FF27F8F827FD04 %FF52F8F8F8A8FD07FFA8F8F8F852FD08FFF8F8F852FFFF7DF8F8F87DFD06 %FF27F8F8F8FD0BFF7DF8F8F8A8FFFF52F87D7DF8F8F8FFFF27F8F827FD06 %FFF8F8F827FD06FFF8F8F852FD04FFF8F8F87DFD05FFA8F8F8F87DFD05FF %7DF8F8F8A8FFFFFF7DF8F8F8A8FD07FFA8F8F8F87DFD08FFF8F8F87DFFFF %A8F8F8F852FD06FF27F8F8F8FD0BFF52F8F8F87DFFFF7DF8A852F8F8F8FF %FF27F8F827FD06FFF8F8F827FD05FFA8F8F8F852FD04FFF8F8F827FD05FF %7DF8F8F852FD05FF7DF8F8F8A8FFFFFF52F8F8F8A8FD07FFA8F8F8F852FD %08FFF8F8F852FFFFFFF8F8F852FD06FF27F8F8F8FD0BFFA8F8F8F8A8FFFF %52F8A87DF8F8F8FFFF27F8F827FD06FFF8F8F827FD05FFA8F8F8F87DFD04 %FFF8F8F827FD05FFA8F8F8F87DFD05FFA8F8F8F8A8FFFFFF7DF8F8F8A8FD %07FFA8F8F8F87DFD08FFF8F8F87DFFFFFF27F8F8F8FD06FF27F8F8F8FD0B %FF7DF8F8F87DFFFFFF7DFF52F8F8F8FFFF52F8F827FD06FFF8F8F827FD05 %FFA8F8F8F87DFD04FF52F8F827FD05FFA8F8F8F852FD05FFA8F8F8F8A8FF %FFFF52F8F8F8A8FD07FFA8F8F8F852FD08FFF8F8F852FFFFFF52F8F8F8A8 %FD05FF27F8F8F8FD0BFFA8F8F8F8A8FD05FF52F8F8F8FFFF27F8F827FD06 %FFF8F8F827FD05FF7DF8F8F8A8FD04FF27F8F8F8FD05FFA8F8F8F87DFD05 %FFA8F8F8F8A8FFFFFF7DF8F8F8A8FD07FFA8F8F8F87DFD08FFF8F8F87DFF %FFFF7DF8F8F8A8FD05FF27F8F8F8FD0BFF7DF8F8F852FD05FF27F8F827FF %FF52F8F827FD06FFF8F8F827FD05FF52F8F8F87DFD04FF52F8F8F8FD05FF %A8F8F8F852FD05FF7DF8F8F87DFFFFFF52F8F8F87DFD07FF7DF8F8F852FD %08FFF8F8F852FFFFFFA8F8F8F852FD05FF27F8F8F8FD0CFFF8F8F87DFD05 %FF27F8F827FFFF27F8F827FD06FFF8F8F827FD05FF7DF8F8F8FD05FF52F8 %F8F8FD05FFA8F8F8F87DFD05FF7DF8F8F8A8FFFFFF7DF8F8F8A8FD07FFA8 %F8F8F87DFD08FFF8F8F87DFD04FFF8F8F827FD05FF27F8F8F8FD0CFFF8F8 %F827FD05FFF8F8F852FFFF52F8F827FD06FFF8F8F827FD05FF27F8F8F8FD %05FF7DF8F8F8A8FD04FFA8F8F8F852FD05FF27F8F827FD04FF52F8F8F8A8 %FD07FFA8F8F8F852FD08FFF8F8F852FD04FF52F8F8F8FD05FF27F8F8F8FD %0CFF27F8F827FD04FFA8F8F8F87DFFFF27F8F827FD06FFF8F8F827FD05FF %27F8F8F8FD05FF52F8F8F87DFD04FFA8F8F8F87DFD04FFA8F8F8F852FD04 %FF7DF8F8F8A8FD07FFA8F8F8F87DFD08FFF8F8F87DFD04FF27F8F8F8A8FD %04FF27F8F8F8FD0CFF7DF8F8F8FD04FF7DF8F8F8A8FFFF27F8F827FD06FF %F8F8F827FD05FF27F8F852FD05FFA8F8F8F87DFD04FF7DF8F8F852FD04FF %7DF8F8F8A8FD04FF52F8F8F8A8FD07FFA8F8F8F852FD08FFF8F8F852FD04 %FF7DF8F8F8A8FD04FF27F8F8F8FD0CFFA8F8F8F852FFFFFF27F8F827FF7D %A8FD04F87DA8FFFF7D7DFD04F87DA8A8A87DFD04F8A87DFFA8A827F8F8F8 %27A8A8FF7D52F8F8F8527DA87D52F8F8F852FFFFFFA87D27F8F8F8527DA8 %FFFFFFA87D52F8F8F8277DA8FD04FF7D7DF8F8F8277DA8FFFF7DF8F8F852 %FFFFA87DFD04F87D7DFD0BFF27F8F8F87DFF52F8F8F87DFFFD08F8FFA8FD %08F8A8FD08F8FF7DFD07F85252FD0CF87DFD04FF27FD07F852FFFFFF52FD %07F827FFFFFFA8FD07F827FFFFFFF8F8F852FFFFFD08F8A8FD0BFF27FD07 %F852FFFF527D527D527D527DFFFF527D527D527D527DA87D527D527D527D %52FF7D7D527D527D527D7DA8527D527D527D527D527D7DFD06FF7D527D52 %7D527D52A8FFFFFFA8527D527D527D527DFFFFFFA8527D527D527D527DFF %FFFF27F8F8F8FFFF7D527D527D527D52FD0DFF5227F8F8F8527DFD5EFF52 %F8F8F8FD7CFF52F8F8F87DFD79FF27FD07F852FD77FF52FD07277DFDFCFF %FD21FFFF %%EndData +%%AI8_CreatorVersion: 14.0.0 %AI9_PrintingDataBegin %ADO_BuildNumber: Adobe Illustrator(R) 14.0.0 x367 R agm 4.4890 ct 5.1541 %ADO_ContainsXMP: MainFirst %AI7_Thumbnail: 128 40 8 %%BeginData: 8480 Hex Bytes %0000330000660000990000CC0033000033330033660033990033CC0033FF %0066000066330066660066990066CC0066FF009900009933009966009999 %0099CC0099FF00CC0000CC3300CC6600CC9900CCCC00CCFF00FF3300FF66 %00FF9900FFCC3300003300333300663300993300CC3300FF333300333333 %3333663333993333CC3333FF3366003366333366663366993366CC3366FF %3399003399333399663399993399CC3399FF33CC0033CC3333CC6633CC99 %33CCCC33CCFF33FF0033FF3333FF6633FF9933FFCC33FFFF660000660033 %6600666600996600CC6600FF6633006633336633666633996633CC6633FF %6666006666336666666666996666CC6666FF669900669933669966669999 %6699CC6699FF66CC0066CC3366CC6666CC9966CCCC66CCFF66FF0066FF33 %66FF6666FF9966FFCC66FFFF9900009900339900669900999900CC9900FF %9933009933339933669933999933CC9933FF996600996633996666996699 %9966CC9966FF9999009999339999669999999999CC9999FF99CC0099CC33 %99CC6699CC9999CCCC99CCFF99FF0099FF3399FF6699FF9999FFCC99FFFF %CC0000CC0033CC0066CC0099CC00CCCC00FFCC3300CC3333CC3366CC3399 %CC33CCCC33FFCC6600CC6633CC6666CC6699CC66CCCC66FFCC9900CC9933 %CC9966CC9999CC99CCCC99FFCCCC00CCCC33CCCC66CCCC99CCCCCCCCCCFF %CCFF00CCFF33CCFF66CCFF99CCFFCCCCFFFFFF0033FF0066FF0099FF00CC %FF3300FF3333FF3366FF3399FF33CCFF33FFFF6600FF6633FF6666FF6699 %FF66CCFF66FFFF9900FF9933FF9966FF9999FF99CCFF99FFFFCC00FFCC33 %FFCC66FFCC99FFCCCCFFCCFFFFFF33FFFF66FFFF99FFFFCC110000001100 %000011111111220000002200000022222222440000004400000044444444 %550000005500000055555555770000007700000077777777880000008800 %000088888888AA000000AA000000AAAAAAAABB000000BB000000BBBBBBBB %DD000000DD000000DDDDDDDDEE000000EE000000EEEEEEEE0000000000FF %00FF0000FFFFFF0000FF00FFFFFF00FFFFFF %524C455227522752275227FFA85227522752275227FD05FFA85227522752 %275252FD05FF7D522752275227522752527DA8FD05FF5252275227522752 %7DFF275227522752275227522752277DA8522752275227522752527DA8FD %05FF2752275227522752275252A8FD08FFA852FD05F87DFFFFFFFD08F8FF %A8FD08F8FD05FFA8FD07F827FD05FF7DFD0CF852FD04FF27FD07F87D7DFD %0DF852A8FD0CF827FD04FFFD0CF827A8FD05FFA827F8F8F827F8F8F852FF %FFFFA827F8F8F8FD05FFA8F8F8F827FD07FF7DF8F8F82727F8F8F8FD07FF %52F8F8F852A87DA852F8F8F852FD05FF27F8F8F852FFFFA8F8F87DA87DF8 %F8F852A8A827F87DFFFF7DF8F8F852A8A8A852F8F8F827FD05FF27F8F8F8 %A8A87DA827F8F827A8FD04FF27F8F827FFFF7DF8F8F87DFFFFFF27F8F827 %FD06FFF8F8F827FD07FF7DF8F8F8A827F8F8F8FD07FFA8F8F8F87DFD04FF %52F8F8F87DFD04FF7DF8F8F8A8FFFF7DF8F8FFFFA8F8F8F87DFFFF52F852 %FFFFFFF8F8F87DFD04FF52F8F8F87DFD04FF27F8F8F8FD05FFF8F8F827FF %FFFFA8F8F8F87DFFFFFF27F8F8F8FFFFFF52F8F827FD06FFF8F8F827FD07 %FF52F8F8F8A87DF8F8F8FD07FFA8F8F8F852FD05FFF8F8F852FD04FF52F8 %F8F8A8FFFFA8F852FFFFA8F8F8F852FFFFA8F87DFFFFFFF8F8F852FD05FF %27F8F827FD04FF27F8F8F8FD05FF7DF8F8F8A8FFFF52F8F8F8FD04FF7DF8 %F8F8A8FFFF27F8F827FD06FFF8F8F827FD07FF52F8F8F8FF52F8F8F87DFD %06FFA8F8F8F87DFD05FF52F8F8F8FD04FF7DF8F8F8A8FFFFA8277DFFFFA8 %F8F8F87DFFFFA8277DFFFFFFF8F8F87DFD05FF52F8F8F8FD04FF27F8F8F8 %FD05FFA8F8F8F852FFFF27F8F827FD04FFA8F8F8F87DFFFF27F8F827FD06 %FFF8F8F827FD07FF27F8F8F8FF7DF8F8F8A8FD06FF7DF8F8F852FD05FF52 %F8F8F8A8FFFFFF52F8F8F8A8FD07FFA8F8F8F852FD08FFF8F8F852FD05FF %7DF8F8F87DFFFFFF27F8F8F8FD06FFF8F8F852FFFFF8F8F852FD05FFF8F8 %F827FFFF27F8F827FD06FFF8F8F827FD07FF27F8F827FF7DF8F8F87DFD06 %FFA8F8F8F87DFD05FFA8F8F8F8A8FFFFFF7DF8F8F8A8FD07FFA8F8F8F87D %FD08FFF8F8F87DFD05FFA8F8F8F87DFFFFFF27F8F8F8FD06FF27F8F827FF %A8F8F8F852FD05FFF8F8F827FFFF52F8F827FD06FFF8F8F827FD07FFF8F8 %F852FFA8F8F8F87DFD06FFA8F8F8F852FD05FFA8F8F8F8A8FFFFFF52F8F8 %F8A8FD07FFA8F8F8F852FD08FFF8F8F852FD05FFA8F8F8F852FFFFFF27F8 %F8F8FD06FF27F8F852FFA8F8F8F87DFD05FF52F8F8F8FFFF27F8F827FD06 %FFF8F8F827FD07FFF8F8F827FFA8F8F8F852FD06FFA8F8F8F87DFD05FFA8 %F8F8F8A8FFFFFF7DF8F8F8A8FD07FFA8F8F8F87DFD08FFF8F8F87DFD05FF %A8F8F8F87DFFFFFF27F8F8F8FD06FF27F8F827FF7DF8F8F87DFD05FF27F8 %F8F8FFFF52F8F827FD06FFF8F8F827FD07FFF8F8F87DFFFFF8F8F852FD06 %FFA8F8F8F852FD05FF7DF8F8F87DFFFFFF52F8F8F87DFD07FF7DF8F8F852 %FD08FFF8F8F852FD05FFA8F8F8F852FFFFFF27F8F8F8FD06FF27F8F827FF %A8F8F8F8A8FD05FF7DF8F8F8FFFF27F8F827FD06FFF8F8F827FD06FF7DF8 %F8F852FFFFF8F8F827FD06FFA8F8F8F87DFD05FF7DF8F8F8A8FFFFFF7DF8 %F8F8A8FD07FFA8F8F8F87DFD08FFF8F8F87DFD05FFA8F8F8F87DFFFFFF27 %F8F8F8FD06FF27F8F827FF7DF8F8F87DFD05FF7D7D527DFFFF52F8F827FD %06FFF8F8F827FD06FFA8F8F8F8A8FFFF27F8F852FD06FFA8F8F8F852FD05 %FF52F8F8F8A8FFFFFF52F8F8F8A8FD07FFA8F8F8F852FD08FFF8F8F852FD %05FF7DF8F8F8A8FFFFFF27F8F8F8FD06FFF8F8F852FF7DF8F8F8A8FD0BFF %27F8F827FD06FFF8F8F827FD06FF7DF8F8F87DFFFF27F8F827FD06FFA8F8 %F8F87DFD05FF27F8F8F8FD04FF7DF8F8F8A8FD07FFA8F8F8F87DFD08FFF8 %F8F87DFD05FF52F8F8F8FD04FF27F8F8F8FD05FF7DF8F8F87DFF52F8F8F8 %7DFD0BFF27F8F827FD06FFF8F8F827FD06FF7DF8F8F8A8FFFF27F8F8F8FD %06FF7DF8F8F852FD05FFF8F8F852FD04FF52F8F8F8A8FD07FFA8F8F8F852 %FD08FFF8F8F852FD05FFF8F8F827FD04FF27F8F8F8FD05FF52F8F8F8FFFF %7DF8F8F8A8FD0BFF27F8F827FD06FFF8F8F827FD06FF52F8F8F8A8FFFF7D %F8F8F8FD06FFA8F8F8F87DFFFFFFA8FD04F87DFD04FF7DF8F8F8A8FD07FF %A8F8F8F87DFD08FFF8F8F87DFD04FF27F8F8F87DFD04FF27F8F8F8FD04FF %7DF8F8F827FFFF52F8F8F87DFD0BFF52FD0CF827FD06FF7DFD0AF8A8FD05 %FFA8FD0BF852FD05FF52F8F8F8A8FD07FFA8F8F8F852FD08FFFD0BF852FD %05FF27FD0AF827A8FFFF7DF8F8F8A8FD0BFF27FD0CF827FD06FF27FD0AF8 %A8FD05FFA8FD0BF8A8FD05FF7DF8F8F8A8FD07FFA8F8F8F87DFD08FFFD0A %F852FD06FF27FD09F827A8FFFFFF52F8F8F87DFFFFFF7DA87DA87DA8FFFF %52F8F8F87D527D527D52F8F8F827FD06FF52F8F8F87D527D52F8F8F87DFD %05FFA8F8F8F8277D527D52FD04F8FD05FF52F8F8F87DFD07FF7DF8F8F852 %FD08FFF8F8F8277D5227F8F8F8FD07FF27F8F8F8527D527D7DA8FD05FF7D %F8F8F8A8FFFF52FD06F8FFFF27F8F827FD06FFF8F8F827FD06FFF8F8F827 %FFFFFFA8F8F8F87DFD05FFA8F8F8F87DFD04FF7DF8F8F827FD04FF7DF8F8 %F8A8FD07FFA8F8F8F87DFD08FFF8F8F87DFFFF52F8F8F8A8FD06FF27F8F8 %F8FD0BFF52F8F8F87DFFFF7DFD06F8FFFF52F8F827FD06FFF8F8F827FD06 %FFF8F8F827FFFFFFA8F8F8F852FD05FFA8F8F8F852FD05FF27F8F827FD04 %FF52F8F8F8A8FD07FFA8F8F8F852FD08FFF8F8F852FFFF7DF8F8F87DFD06 %FF27F8F8F8FD0BFF7DF8F8F8A8FFFF52F87D7DF8F8F8FFFF27F8F827FD06 %FFF8F8F827FD06FFF8F8F852FD04FFF8F8F87DFD05FFA8F8F8F87DFD05FF %7DF8F8F8A8FFFFFF7DF8F8F8A8FD07FFA8F8F8F87DFD08FFF8F8F87DFFFF %A8F8F8F852FD06FF27F8F8F8FD0BFF52F8F8F87DFFFF7DF8A852F8F8F8FF %FF27F8F827FD06FFF8F8F827FD05FFA8F8F8F852FD04FFF8F8F827FD05FF %7DF8F8F852FD05FF7DF8F8F8A8FFFFFF52F8F8F8A8FD07FFA8F8F8F852FD %08FFF8F8F852FFFFFFF8F8F852FD06FF27F8F8F8FD0BFFA8F8F8F8A8FFFF %52F8A87DF8F8F8FFFF27F8F827FD06FFF8F8F827FD05FFA8F8F8F87DFD04 %FFF8F8F827FD05FFA8F8F8F87DFD05FFA8F8F8F8A8FFFFFF7DF8F8F8A8FD %07FFA8F8F8F87DFD08FFF8F8F87DFFFFFF27F8F8F8FD06FF27F8F8F8FD0B %FF7DF8F8F87DFFFFFF7DFF52F8F8F8FFFF52F8F827FD06FFF8F8F827FD05 %FFA8F8F8F87DFD04FF52F8F827FD05FFA8F8F8F852FD05FFA8F8F8F8A8FF %FFFF52F8F8F8A8FD07FFA8F8F8F852FD08FFF8F8F852FFFFFF52F8F8F8A8 %FD05FF27F8F8F8FD0BFFA8F8F8F8A8FD05FF52F8F8F8FFFF27F8F827FD06 %FFF8F8F827FD05FF7DF8F8F8A8FD04FF27F8F8F8FD05FFA8F8F8F87DFD05 %FFA8F8F8F8A8FFFFFF7DF8F8F8A8FD07FFA8F8F8F87DFD08FFF8F8F87DFF %FFFF7DF8F8F8A8FD05FF27F8F8F8FD0BFF7DF8F8F852FD05FF27F8F827FF %FF52F8F827FD06FFF8F8F827FD05FF52F8F8F87DFD04FF52F8F8F8FD05FF %A8F8F8F852FD05FF7DF8F8F87DFFFFFF52F8F8F87DFD07FF7DF8F8F852FD %08FFF8F8F852FFFFFFA8F8F8F852FD05FF27F8F8F8FD0CFFF8F8F87DFD05 %FF27F8F827FFFF27F8F827FD06FFF8F8F827FD05FF7DF8F8F8FD05FF52F8 %F8F8FD05FFA8F8F8F87DFD05FF7DF8F8F8A8FFFFFF7DF8F8F8A8FD07FFA8 %F8F8F87DFD08FFF8F8F87DFD04FFF8F8F827FD05FF27F8F8F8FD0CFFF8F8 %F827FD05FFF8F8F852FFFF52F8F827FD06FFF8F8F827FD05FF27F8F8F8FD %05FF7DF8F8F8A8FD04FFA8F8F8F852FD05FF27F8F827FD04FF52F8F8F8A8 %FD07FFA8F8F8F852FD08FFF8F8F852FD04FF52F8F8F8FD05FF27F8F8F8FD %0CFF27F8F827FD04FFA8F8F8F87DFFFF27F8F827FD06FFF8F8F827FD05FF %27F8F8F8FD05FF52F8F8F87DFD04FFA8F8F8F87DFD04FFA8F8F8F852FD04 %FF7DF8F8F8A8FD07FFA8F8F8F87DFD08FFF8F8F87DFD04FF27F8F8F8A8FD %04FF27F8F8F8FD0CFF7DF8F8F8FD04FF7DF8F8F8A8FFFF27F8F827FD06FF %F8F8F827FD05FF27F8F852FD05FFA8F8F8F87DFD04FF7DF8F8F852FD04FF %7DF8F8F8A8FD04FF52F8F8F8A8FD07FFA8F8F8F852FD08FFF8F8F852FD04 %FF7DF8F8F8A8FD04FF27F8F8F8FD0CFFA8F8F8F852FFFFFF27F8F827FF7D %A8FD04F87DA8FFFF7D7DFD04F87DA8A8A87DFD04F8A87DFFA8A827F8F8F8 %27A8A8FF7D52F8F8F8527DA87D52F8F8F852FFFFFFA87D27F8F8F8527DA8 %FFFFFFA87D52F8F8F8277DA8FD04FF7D7DF8F8F8277DA8FFFF7DF8F8F852 %FFFFA87DFD04F87D7DFD0BFF27F8F8F87DFF52F8F8F87DFFFD08F8FFA8FD %08F8A8FD08F8FF7DFD07F85252FD0CF87DFD04FF27FD07F852FFFFFF52FD %07F827FFFFFFA8FD07F827FFFFFFF8F8F852FFFFFD08F8A8FD0BFF27FD07 %F852FFFF527D527D527D527DFFFF527D527D527D527DA87D527D527D527D %52FF7D7D527D527D527D7DA8527D527D527D527D527D7DFD06FF7D527D52 %7D527D52A8FFFFFFA8527D527D527D527DFFFFFFA8527D527D527D527DFF %FFFF27F8F8F8FFFF7D527D527D527D52FD0DFF5227F8F8F8527DFD5EFF52 %F8F8F8FD7CFF52F8F8F87DFD79FF27FD07F852FD77FF52FD07277DFDFCFF %FD21FFFF %%EndData %ADOEndClientInjection: DocumentHeader "AI11EPS" %%Pages: 1 %%DocumentNeededResources: @@ -4798,7 +4798,7 @@ currentdict Adobe_AGM_Utils eq {end} if %%EndPageComments %%BeginPageSetup %ADOBeginClientInjection: PageSetup Start "AI11EPS" -%AI12_RMC_Transparency: Balance=75 RasterRes=300 GradRes=150 Text=0 Stroke=1 Clip=1 OP=0 +%AI12_RMC_Transparency: Balance=75 RasterRes=300 GradRes=150 Text=0 Stroke=1 Clip=1 OP=0 %ADOEndClientInjection: PageSetup Start "AI11EPS" Adobe_AGM_Utils begin Adobe_AGM_Core/ps gx @@ -5517,7 +5517,7 @@ Adobe_CoolType_Core/ps get exec Adobe_AGM_Image/ps gx - % &&end XMP packet marker&& [{ai_metadata_stream_123} <> /PUT AI11_PDFMark5 [/Document 1 dict begin /Metadata {ai_metadata_stream_123} def currentdict end /BDC AI11_PDFMark5 + % &&end XMP packet marker&& [{ai_metadata_stream_123} <> /PUT AI11_PDFMark5 [/Document 1 dict begin /Metadata {ai_metadata_stream_123} def currentdict end /BDC AI11_PDFMark5 %ADOEndClientInjection: PageSetup End "AI11EPS" %%EndPageSetup 1 -1 scale 0 -78.2041 translate @@ -5780,14 +5780,14 @@ f cp f %ADOBeginClientInjection: EndPageContent "AI11EPS" -userdict /annotatepage 2 copy known {get exec}{pop pop} ifelse +userdict /annotatepage 2 copy known {get exec}{pop pop} ifelse %ADOEndClientInjection: EndPageContent "AI11EPS" grestore grestore pgrs %%PageTrailer %ADOBeginClientInjection: PageTrailer Start "AI11EPS" -[/EMC AI11_PDFMark5 [/NamespacePop AI11_PDFMark5 +[/EMC AI11_PDFMark5 [/NamespacePop AI11_PDFMark5 %ADOEndClientInjection: PageTrailer Start "AI11EPS" [ [/CSA [/0 ]] diff --git a/website/client-old/merch/teespring-logo.svg b/website/client-old/merch/teespring-logo.svg index 11372d4b3a..d2430d6796 100644 --- a/website/client-old/merch/teespring-logo.svg +++ b/website/client-old/merch/teespring-logo.svg @@ -1,63 +1,63 @@ - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + diff --git a/website/common/locales/en/groups.json b/website/common/locales/en/groups.json index 89d65260b1..5f71900cc0 100644 --- a/website/common/locales/en/groups.json +++ b/website/common/locales/en/groups.json @@ -209,5 +209,8 @@ "exportInboxPopoverBody": "HTML allows easy reading of messages in a browser. For a machine-readable format, use Data > Export Data", "to": "To:", "from": "From:", - "desktopNotificationsText": "We need your permission to enable desktop notifications for new messages in party chat! Follow your browser's instructions to turn them on.

You'll receive these notifications only while you have Habitica open. If you decide you don't like them, they can be disabled in your browser's settings.

This box will close automatically when a decision is made." + "desktopNotificationsText": "We need your permission to enable desktop notifications for new messages in party chat! Follow your browser's instructions to turn them on.

You'll receive these notifications only while you have Habitica open. If you decide you don't like them, they can be disabled in your browser's settings.

This box will close automatically when a decision is made.", + "confirmAddTag": "Do you really want to add \"<%= tag %>\"?", + "confirmRemoveTag": "Do you really want to remove \"<%= tag %>\"?", + "assignTask": "Assign Task" } diff --git a/website/server/controllers/api-v3/tasks.js b/website/server/controllers/api-v3/tasks.js index eb2d4c3ad5..22dbceff1b 100644 --- a/website/server/controllers/api-v3/tasks.js +++ b/website/server/controllers/api-v3/tasks.js @@ -259,6 +259,8 @@ api.updateTask = { if (challenge) { challenge.updateTask(savedTask); + } else if (group && task.group.id && task.group.assignedUsers.length > 0) { + await group.updateTask(savedTask); } else { taskActivityWebhook.send(user.webhooks, { type: 'updated', @@ -435,6 +437,7 @@ api.addChecklistItem = { async handler (req, res) { let user = res.locals.user; let challenge; + let group; req.checkParams('taskId', res.t('taskIdRequired')).notEmpty(); @@ -446,6 +449,10 @@ api.addChecklistItem = { if (!task) { throw new NotFound(res.t('taskNotFound')); + } else if (task.group.id && !task.userId) { + group = await Group.getGroup({user, groupId: task.group.id, fields: requiredGroupFields}); + if (!group) throw new NotFound(res.t('groupNotFound')); + if (group.leader !== user._id) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks')); } else if (task.challenge.id && !task.userId) { // If the task belongs to a challenge make sure the user has rights challenge = await Challenge.findOne({_id: task.challenge.id}).exec(); if (!challenge) throw new NotFound(res.t('challengeNotFound')); @@ -461,6 +468,9 @@ api.addChecklistItem = { res.respond(200, savedTask); if (challenge) challenge.updateTask(savedTask); + if (group && task.group.id && task.group.assignedUsers.length > 0) { + await group.updateTask(savedTask); + } }, }; @@ -522,6 +532,7 @@ api.updateChecklistItem = { async handler (req, res) { let user = res.locals.user; let challenge; + let group; req.checkParams('taskId', res.t('taskIdRequired')).notEmpty(); req.checkParams('itemId', res.t('itemIdRequired')).notEmpty().isUUID(); @@ -534,6 +545,10 @@ api.updateChecklistItem = { if (!task) { throw new NotFound(res.t('taskNotFound')); + } else if (task.group.id && !task.userId) { + group = await Group.getGroup({user, groupId: task.group.id, fields: requiredGroupFields}); + if (!group) throw new NotFound(res.t('groupNotFound')); + if (group.leader !== user._id) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks')); } else if (task.challenge.id && !task.userId) { // If the task belongs to a challenge make sure the user has rights challenge = await Challenge.findOne({_id: task.challenge.id}).exec(); if (!challenge) throw new NotFound(res.t('challengeNotFound')); @@ -551,6 +566,9 @@ api.updateChecklistItem = { res.respond(200, savedTask); if (challenge) challenge.updateTask(savedTask); + if (group && task.group.id && task.group.assignedUsers.length > 0) { + await group.updateTask(savedTask); + } }, }; @@ -572,6 +590,7 @@ api.removeChecklistItem = { async handler (req, res) { let user = res.locals.user; let challenge; + let group; req.checkParams('taskId', res.t('taskIdRequired')).notEmpty(); req.checkParams('itemId', res.t('itemIdRequired')).notEmpty().isUUID(); @@ -584,6 +603,10 @@ api.removeChecklistItem = { if (!task) { throw new NotFound(res.t('taskNotFound')); + } else if (task.group.id && !task.userId) { + group = await Group.getGroup({user, groupId: task.group.id, fields: requiredGroupFields}); + if (!group) throw new NotFound(res.t('groupNotFound')); + if (group.leader !== user._id) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks')); } else if (task.challenge.id && !task.userId) { // If the task belongs to a challenge make sure the user has rights challenge = await Challenge.findOne({_id: task.challenge.id}).exec(); if (!challenge) throw new NotFound(res.t('challengeNotFound')); @@ -599,6 +622,9 @@ api.removeChecklistItem = { let savedTask = await task.save(); res.respond(200, savedTask); if (challenge) challenge.updateTask(savedTask); + if (group && task.group.id && task.group.assignedUsers.length > 0) { + await group.updateTask(savedTask); + } }, }; diff --git a/website/server/models/group.js b/website/server/models/group.js index 6d504801e2..da25eef480 100644 --- a/website/server/models/group.js +++ b/website/server/models/group.js @@ -90,13 +90,16 @@ export let schema = new Schema({ todos: [{type: String, ref: 'Task'}], rewards: [{type: String, ref: 'Task'}], }, + purchased: { + active: {type: Boolean, default: false}, + }, }, { strict: true, minimize: false, // So empty objects are returned }); schema.plugin(baseModel, { - noSet: ['_id', 'balance', 'quest', 'memberCount', 'chat', 'challengeCount', 'tasksOrder'], + noSet: ['_id', 'balance', 'quest', 'memberCount', 'chat', 'challengeCount', 'tasksOrder', 'purchased'], }); // A list of additional fields that cannot be updated (but can be set on creation) diff --git a/website/views/options/social/group.jade b/website/views/options/social/group.jade index 9ee08c8002..05c7a9f3d3 100644 --- a/website/views/options/social/group.jade +++ b/website/views/options/social/group.jade @@ -147,7 +147,20 @@ a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter h3.popover-title {{group.leader.profile.name}} .popover-content markdown(text='group._editing ? groupCopy.leaderMessage : group.leaderMessage') - div(ng-controller='ChatCtrl') + + ul.options-menu(ng-init="groupPane = 'chat'", ng-show="group.purchased.active") + li + a(ng-click="groupPane = 'chat'") + | Chat + li + a(ng-click="groupPane = 'tasks'") + | Tasks + + + .tab-content + .tab-pane.active + + div(ng-controller='ChatCtrl', ng-show="groupPane == 'chat'") .alert.alert-info.alert-sm(ng-if='group.memberCount > Shared.constants.LARGE_GROUP_COUNT_MESSAGE_CUTOFF')=env.t('largeGroupNote') h3=env.t('chat') include ./chat-box @@ -155,3 +168,5 @@ a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter +chatMessages() h4(ng-if='group.chat.length < 1 && group.type === "party"')=env.t('partyChatEmpty') h4(ng-if='group.chat.length < 1 && group.type === "guild"')=env.t('guildChatEmpty') + + group-tasks(ng-show="groupPane == 'tasks'") diff --git a/website/views/options/social/groups/group-members-autocomplete.jade b/website/views/options/social/groups/group-members-autocomplete.jade new file mode 100644 index 0000000000..69a420b528 --- /dev/null +++ b/website/views/options/social/groups/group-members-autocomplete.jade @@ -0,0 +1,2 @@ +script(type='text/ng-template', id='partials/groups.members.autocomplete.html') + div#taggle.input.textarea.clearfix diff --git a/website/views/options/social/groups/group-tasks-actions.jade b/website/views/options/social/groups/group-tasks-actions.jade new file mode 100644 index 0000000000..cf6be842b7 --- /dev/null +++ b/website/views/options/social/groups/group-tasks-actions.jade @@ -0,0 +1,4 @@ +script(type='text/ng-template', id='partials/groups.tasks.actions.html') + div(ng-if="group.leader._id === user._id", class="col-md-12") + strong=env.t('assignTask') + group-members-autocomplete(ng-model="assignedMembers") diff --git a/website/views/options/social/groups/group-tasks.jade b/website/views/options/social/groups/group-tasks.jade new file mode 100644 index 0000000000..e992454e15 --- /dev/null +++ b/website/views/options/social/groups/group-tasks.jade @@ -0,0 +1,5 @@ +include ./group-tasks-actions +include ./group-members-autocomplete + +script(type='text/ng-template', id='partials/groups.tasks.html') + habitrpg-tasks(main=false) diff --git a/website/views/options/social/index.jade b/website/views/options/social/index.jade index a98187adaf..fddc0be8a0 100644 --- a/website/views/options/social/index.jade +++ b/website/views/options/social/index.jade @@ -6,6 +6,7 @@ include ./hall include ./quests/index include ./chat-message include ./party +include ./groups/group-tasks script(type='text/ng-template', id='partials/options.social.inbox.html') .options-blurbmenu diff --git a/website/views/shared/tasks/edit/advanced_options.jade b/website/views/shared/tasks/edit/advanced_options.jade index 74cef48845..bf4d6d867a 100644 --- a/website/views/shared/tasks/edit/advanced_options.jade +++ b/website/views/shared/tasks/edit/advanced_options.jade @@ -9,6 +9,9 @@ div(ng-if='::task.type!="reward"') a.hint(href='http://habitica.wikia.com/wiki/Task_Alias', target='_blank', popover-trigger='mouseenter', popover="{{::env.t('taskAliasPopover')}} {{::task._edit.alias ? '\n\n\' + env.t('taskAliasPopoverWarning') : ''}}")=env.t('taskAlias') input.form-control(ng-model='task._edit.alias' type='text' placeholder=env.t('taskAliasPlaceholder')) + fieldset.option-group.advanced-option(ng-show="task._edit._advanced") + group-tasks-actions(ng-if="obj.type == 'guild'", task='task', group='obj') + div(ng-show='task._edit._advanced') div(ng-if='::task.type == "daily"') .form-group diff --git a/website/views/shared/tasks/index.jade b/website/views/shared/tasks/index.jade index f128b0b322..30f40824e9 100644 --- a/website/views/shared/tasks/index.jade +++ b/website/views/shared/tasks/index.jade @@ -6,7 +6,7 @@ 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", "new-row-md": list.type==="todo" }') + .col-md-3.col-sm-6(ng-repeat='list in lists', ng-class='::{ "rewards-module": list.type==="reward", "new-row-md": list.type==="todo", "col-md-2": obj.type }') .task-column(class='{{::list.type}}s') include ./task_view/graph