diff --git a/migrations/20170928_redesign_launch.js b/migrations/20170928_redesign_launch.js
index 3ff9970d83..1139652050 100644
--- a/migrations/20170928_redesign_launch.js
+++ b/migrations/20170928_redesign_launch.js
@@ -1,4 +1,5 @@
-var updateStore = require('../website/common/script/libs/updateStore');
+import { selectGearToPin } from '../website/common/script/ops/pinnedGearUtils';
+
var getItemInfo = require('../website/common/script/libs/getItemInfo');
var migrationName = '20170928_redesign_launch.js';
@@ -69,7 +70,7 @@ function updateUser (user) {
var set = {'migration': migrationName};
- var oldRewardsList = updateStore(user);
+ var oldRewardsList = selectGearToPin(user);
var newPinnedItems = [
{
type: 'armoire',
diff --git a/package-lock.json b/package-lock.json
index e0f5d6b90f..7b84febe65 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "habitica",
- "version": "4.1.2",
+ "version": "4.1.3",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -68,6 +68,15 @@
"@types/mime": "1.3.1"
}
},
+ "JSONStream": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.1.tgz",
+ "integrity": "sha1-cH92HgHa6eFvG8+TcDt4xwlmV5o=",
+ "requires": {
+ "jsonparse": "1.3.1",
+ "through": "2.3.8"
+ }
+ },
"abbrev": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.0.tgz",
@@ -782,6 +791,14 @@
"is-buffer": "1.1.5"
}
},
+ "axios-progress-bar": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/axios-progress-bar/-/axios-progress-bar-0.1.7.tgz",
+ "integrity": "sha512-xStxJUtcQUH0ulLni5qc8YwvNMTUjO7rmUTsvnxS1bD2GJKEemozP6sJ5OeoC6jjd6MS5FZyx5BlkU/lD8JLUw==",
+ "requires": {
+ "nprogress": "0.2.0"
+ }
+ },
"babel-code-frame": {
"version": "6.26.0",
"resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
@@ -2325,9 +2342,9 @@
"resolved": "https://registry.npmjs.org/browser-pack/-/browser-pack-6.0.2.tgz",
"integrity": "sha1-+GzWzvT1MAyOY+B6TVEvZfv/RTE=",
"requires": {
+ "JSONStream": "1.3.1",
"combine-source-map": "0.7.2",
"defined": "1.0.0",
- "JSONStream": "1.3.1",
"through2": "2.0.3",
"umd": "3.0.1"
}
@@ -2357,6 +2374,7 @@
"resolved": "https://registry.npmjs.org/browserify/-/browserify-12.0.2.tgz",
"integrity": "sha1-V/IeXm4wj/WYfE2v1EhAsrmPehk=",
"requires": {
+ "JSONStream": "1.3.1",
"assert": "1.3.0",
"browser-pack": "6.0.2",
"browser-resolve": "1.11.2",
@@ -2378,7 +2396,6 @@
"inherits": "2.0.3",
"insert-module-globals": "7.0.1",
"isarray": "0.0.1",
- "JSONStream": "1.3.1",
"labeled-stream-splicer": "2.0.0",
"module-deps": "4.1.1",
"os-browserify": "0.1.2",
@@ -7389,13 +7406,6 @@
}
}
},
- "string_decoder": {
- "version": "1.0.1",
- "bundled": true,
- "requires": {
- "safe-buffer": "5.0.1"
- }
- },
"string-width": {
"version": "1.0.2",
"bundled": true,
@@ -7405,6 +7415,13 @@
"strip-ansi": "3.0.1"
}
},
+ "string_decoder": {
+ "version": "1.0.1",
+ "bundled": true,
+ "requires": {
+ "safe-buffer": "5.0.1"
+ }
+ },
"stringstream": {
"version": "0.0.5",
"bundled": true,
@@ -10353,10 +10370,10 @@
"resolved": "https://registry.npmjs.org/insert-module-globals/-/insert-module-globals-7.0.1.tgz",
"integrity": "sha1-wDv04BywhtW15azorQr+eInWOMM=",
"requires": {
+ "JSONStream": "1.3.1",
"combine-source-map": "0.7.2",
"concat-stream": "1.5.2",
"is-buffer": "1.1.5",
- "JSONStream": "1.3.1",
"lexical-scope": "1.2.0",
"process": "0.11.10",
"through2": "2.0.3",
@@ -11349,15 +11366,6 @@
"resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz",
"integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk="
},
- "JSONStream": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.1.tgz",
- "integrity": "sha1-cH92HgHa6eFvG8+TcDt4xwlmV5o=",
- "requires": {
- "jsonparse": "1.3.1",
- "through": "2.3.8"
- }
- },
"jsprim": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
@@ -13257,6 +13265,7 @@
"resolved": "https://registry.npmjs.org/module-deps/-/module-deps-4.1.1.tgz",
"integrity": "sha1-IyFYM/HaE/1gbMuAh7RIUty4If0=",
"requires": {
+ "JSONStream": "1.3.1",
"browser-resolve": "1.11.2",
"cached-path-relative": "1.0.1",
"concat-stream": "1.5.2",
@@ -13264,7 +13273,6 @@
"detective": "4.5.0",
"duplexer2": "0.1.4",
"inherits": "2.0.3",
- "JSONStream": "1.3.1",
"parents": "1.0.1",
"readable-stream": "2.0.6",
"resolve": "1.4.0",
@@ -14640,6 +14648,11 @@
"set-blocking": "2.0.0"
}
},
+ "nprogress": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz",
+ "integrity": "sha1-y480xTIT2JVyP8urkH6UIq28r7E="
+ },
"nth-check": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.1.tgz",
@@ -17234,22 +17247,6 @@
}
}
},
- "require_optional": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz",
- "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==",
- "requires": {
- "resolve-from": "2.0.0",
- "semver": "5.4.1"
- },
- "dependencies": {
- "semver": {
- "version": "5.4.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz",
- "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg=="
- }
- }
- },
"require-again": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-again/-/require-again-2.0.0.tgz",
@@ -17289,6 +17286,22 @@
}
}
},
+ "require_optional": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz",
+ "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==",
+ "requires": {
+ "resolve-from": "2.0.0",
+ "semver": "5.4.1"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "5.4.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz",
+ "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg=="
+ }
+ }
+ },
"requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
@@ -18655,11 +18668,6 @@
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
"integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM="
},
- "string_decoder": {
- "version": "0.10.31",
- "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
- "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
- },
"string-length": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/string-length/-/string-length-0.1.2.tgz",
@@ -18693,6 +18701,11 @@
"strip-ansi": "3.0.1"
}
},
+ "string_decoder": {
+ "version": "0.10.31",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+ "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
+ },
"stringify-object": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-1.0.1.tgz",
diff --git a/package.json b/package.json
index 86dc2e7776..0deb21d7fb 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
- "version": "4.1.2",
+ "version": "4.1.3",
"main": "./website/server/index.js",
"dependencies": {
"@slack/client": "^3.8.1",
@@ -14,6 +14,7 @@
"autoprefixer": "^6.4.0",
"aws-sdk": "^2.0.25",
"axios": "^0.16.0",
+ "axios-progress-bar": "^0.1.7",
"babel-core": "^6.0.0",
"babel-eslint": "^7.2.3",
"babel-loader": "^6.0.0",
diff --git a/website/client/app.vue b/website/client/app.vue
index 72318e7db1..618d65da46 100644
--- a/website/client/app.vue
+++ b/website/client/app.vue
@@ -68,6 +68,8 @@
diff --git a/website/client/components/group-plans/index.vue b/website/client/components/group-plans/index.vue
index 037f2fdd1a..1c2cadcd8d 100644
--- a/website/client/components/group-plans/index.vue
+++ b/website/client/components/group-plans/index.vue
@@ -2,9 +2,14 @@
.row
secondary-menu.col-12
router-link.nav-link(:to="{name: 'groupPlanDetailTaskInformation', params: {groupId}}",
- exact, :class="{'active': $route.name === 'groupPlanDetailTaskInformation'}") Task Board
+ exact, :class="{'active': $route.name === 'groupPlanDetailTaskInformation'}") {{ $t('groupTaskBoard') }}
router-link.nav-link(:to="{name: 'groupPlanDetailInformation', params: {groupId}}",
- exact, :class="{'active': $route.name === 'groupPlanDetailInformation'}") Group Information
+ exact, :class="{'active': $route.name === 'groupPlanDetailInformation'}") {{ $t('groupInformation') }}
+ router-link.nav-link(
+ v-if='isLeader',
+ :to="{name: 'groupPlanBilling', params: {groupId}}",
+ exact,
+ :class="{'active': $route.name === 'groupPlanBilling'}") {{ $t('groupBilling') }}
.col-12
router-view
@@ -12,11 +17,26 @@
diff --git a/website/client/components/group-plans/taskInformation.vue b/website/client/components/group-plans/taskInformation.vue
index 4e7f783f20..88fc39021b 100644
--- a/website/client/components/group-plans/taskInformation.vue
+++ b/website/client/components/group-plans/taskInformation.vue
@@ -349,15 +349,21 @@ export default {
groupId: this.searchId,
});
+ let groupedApprovals = this.loadApprovals();
+
+ tasks.forEach((task) => {
+ if (groupedApprovals.length > 0) task.approvals = groupedApprovals[task._id];
+ this.tasksByType[task.type].push(task);
+ });
+ },
+ async loadApprovals () {
+ if (this.group.leader._id !== this.user._id) return [];
+
let approvalRequests = await this.$store.dispatch('tasks:getGroupApprovals', {
groupId: this.searchId,
});
- let groupedApprovals = groupBy(approvalRequests, 'group.taskId');
- tasks.forEach((task) => {
- task.approvals = groupedApprovals[task._id];
- this.tasksByType[task.type].push(task);
- });
+ return groupBy(approvalRequests, 'group.taskId');
},
editTask (task) {
this.taskFormPurpose = 'edit';
diff --git a/website/client/components/notificationMenu.vue b/website/client/components/notificationMenu.vue
index 1fe4a7e868..28be2e485a 100644
--- a/website/client/components/notificationMenu.vue
+++ b/website/client/components/notificationMenu.vue
@@ -1,8 +1,7 @@
div.item-with-icon.item-notifications.dropdown
+ span.message-count.top-count(v-if='notificationsCount > 0') {{ notificationsCount }}
.svg-icon.notifications(v-html="icons.notifications")
- // span.glyphicon(:class='iconClasses()')
- // span.notification-counter(v-if='getNotificationsCount()') {{getNotificationsCount()}}
.dropdown-menu.dropdown-menu-right.user-dropdown
h4.dropdown-item.dropdown-separated(v-if='!hasNoNotifications()') {{ $t('notifications') }}
h4.dropdown-item.toolbar-notifs-no-messages(v-if='hasNoNotifications()') {{ $t('noNotifications') }}
@@ -58,6 +57,25 @@ div.item-with-icon.item-notifications.dropdown
@@ -210,6 +219,7 @@
import bModal from 'bootstrap-vue/lib/components/modal';
import * as Analytics from 'client/libs/analytics';
import spellsMixin from 'client/mixins/spells';
+ import planGemLimits from 'common/script/libs/planGemLimits';
import svgClose from 'assets/svg/close.svg';
import svgGold from 'assets/svg/gold.svg';
@@ -292,6 +302,10 @@
limitedString () {
return this.$t('limitedOffer', {date: moment(seasonalShopConfig.dateRange.end).format('LL')});
},
+ gemsLeft () {
+ if (!this.user.purchased.plan) return 0;
+ return planGemLimits.convCap + this.user.purchased.plan.consecutive.gemCapExtra - this.user.purchased.plan.gemsBought;
+ },
},
watch: {
item: function itemChanged () {
diff --git a/website/client/components/shops/market/index.vue b/website/client/components/shops/market/index.vue
index 0714f1c437..674d842f53 100644
--- a/website/client/components/shops/market/index.vue
+++ b/website/client/components/shops/market/index.vue
@@ -383,6 +383,8 @@
const sortGearTypes = ['sortByType', 'sortByPrice', 'sortByCon', 'sortByPer', 'sortByStr', 'sortByInt'];
import notifications from 'client/mixins/notifications';
+ import buyMixin from 'client/mixins/buy';
+ import currencyMixin from '../_currencyMixin';
const sortGearTypeMap = {
sortByType: 'type',
@@ -393,7 +395,7 @@
};
export default {
- mixins: [notifications],
+ mixins: [notifications, buyMixin, currencyMixin],
components: {
ShopItem,
Item,
@@ -694,6 +696,11 @@ export default {
}
},
itemSelected (item) {
+ if (item.purchaseType !== 'gear' && this.$store.state.recentlyPurchased[item.key]) {
+ this.makeGenericPurchase(item);
+ return;
+ }
+
this.$root.$emit('buyModal::showItem', item);
},
featuredItemSelected (item) {
diff --git a/website/client/components/shops/quests/buyQuestModal.vue b/website/client/components/shops/quests/buyQuestModal.vue
index 6bcdbbd40a..812605765c 100644
--- a/website/client/components/shops/quests/buyQuestModal.vue
+++ b/website/client/components/shops/quests/buyQuestModal.vue
@@ -43,11 +43,9 @@
:currencyNeeded="priceType",
:amountNeeded="item.value"
).float-right
-
-
-
diff --git a/website/client/components/tasks/task.vue b/website/client/components/tasks/task.vue
index 7e9581bc53..466a026509 100644
--- a/website/client/components/tasks/task.vue
+++ b/website/client/components/tasks/task.vue
@@ -17,8 +17,16 @@
h3.task-title(:class="{ 'has-notes': task.notes }", v-markdown="task.text")
.task-notes.small-text(v-markdown="task.notes")
.checklist(v-if="canViewchecklist")
+ .d-inline-flex
+ .collapse-checklist.d-flex.align-items-center.expand-toggle(
+ v-if="isUser",
+ @click="collapseChecklist(task)",
+ :class="{open: !task.collapseChecklist}",
+ )
+ .svg-icon(v-html="icons.checklist")
+ span {{ checklistProgress }}
label.custom-control.custom-checkbox.checklist-item(
- v-if='!castingSpell',
+ v-if='!castingSpell && !task.collapseChecklist',
v-for="item in task.checklist", :class="{'checklist-item-done': item.completed}",
)
input.custom-control-input(type="checkbox", :checked="item.completed", @change="toggleChecklistItem(item)")
@@ -119,6 +127,26 @@
margin-top: 8px;
}
+ .collapse-checklist {
+ padding: 2px 6px;
+ margin-bottom: 9px;
+ border-radius: 1px;
+ background-color: $gray-600;
+ font-size: 10px;
+ line-height: 1.2;
+ text-align: center;
+ color: $gray-200;
+
+ span {
+ margin: 0px 4px;
+ }
+
+ .svg-icon {
+ width: 12px;
+ height: 8px;
+ }
+ }
+
.checklist-item {
color: $gray-50;
font-size: 14px;
@@ -308,6 +336,7 @@ import calendarIcon from 'assets/svg/calendar.svg';
import challengeIcon from 'assets/svg/challenge.svg';
import tagsIcon from 'assets/svg/tags.svg';
import checkIcon from 'assets/svg/check.svg';
+import checklistIcon from 'assets/svg/checklist.svg';
import bPopover from 'bootstrap-vue/lib/components/popover';
import markdownDirective from 'client/directives/markdown';
import notifications from 'client/mixins/notifications';
@@ -336,6 +365,7 @@ export default {
challenge: challengeIcon,
tags: tagsIcon,
check: checkIcon,
+ checklist: checklistIcon,
}),
};
},
@@ -353,6 +383,13 @@ export default {
let userIsTaskUser = this.task.userId ? this.task.userId === this.user._id : true;
return hasChecklist && userIsTaskUser;
},
+ checklistProgress () {
+ const totalItems = this.task.checklist.length;
+ const completedItems = this.task.checklist.reduce((total, item) => {
+ return item.completed ? total + 1 : total;
+ }, 0);
+ return `${completedItems}/${totalItems}`;
+ },
leftControl () {
const task = this.task;
if (task.type === 'reward') return false;
@@ -392,10 +429,13 @@ export default {
},
},
methods: {
- ...mapActions({scoreChecklistItem: 'tasks:scoreChecklistItem'}),
+ ...mapActions({
+ scoreChecklistItem: 'tasks:scoreChecklistItem',
+ collapseChecklist: 'tasks:collapseChecklist',
+ }),
toggleChecklistItem (item) {
if (this.castingSpell) return;
- item.completed = !item.completed;
+ item.completed = !item.completed; // @TODO this should go into the action?
this.scoreChecklistItem({taskId: this.task._id, itemId: item.id});
},
edit (e, task) {
diff --git a/website/client/components/tasks/taskModal.vue b/website/client/components/tasks/taskModal.vue
index 3e9ac99fde..c80b6bc203 100644
--- a/website/client/components/tasks/taskModal.vue
+++ b/website/client/components/tasks/taskModal.vue
@@ -110,29 +110,22 @@
span.custom-control-indicator
span.custom-control-description {{ $t('dayOfWeek') }}
- .option(v-if="isUserTask")
- label(v-once) {{ $t('tags') }}
- .category-wrap(@click="showTagsSelect = !showTagsSelect")
- span.category-select(v-if='task.tags && task.tags.length === 0') {{$t('none')}}
- span.category-select(v-else)
- .category-label(v-for='tagName in getTagsFor(task)') {{tagName}}
- .category-box(v-if="showTagsSelect")
- .container
- .row
- .form-check.col-6(
- v-for="tag in user.tags",
- :key="tag.id",
- )
- label.custom-control.custom-checkbox
- input.custom-control-input(type="checkbox", :value="tag.id", v-model="task.tags")
- span.custom-control-indicator
- span.custom-control-description(v-once) {{ tag.name }}
- .row
- button.btn.btn-primary(@click="showTagsSelect = !showTagsSelect") {{$t('close')}}
+ .tags-select.option(v-if="isUserTask")
+ .tags-inline
+ label(v-once) {{ $t('tags') }}
+ .category-wrap(@click="showTagsSelect = !showTagsSelect", v-bind:class="{ active: showTagsSelect }")
+ span.category-select(v-if='task.tags && task.tags.length === 0')
+ .tags-none {{$t('none')}}
+ .dropdown-toggle
+ span.category-select(v-else)
+ .category-label(v-for='tagName in truncatedSelectedTags', :title="tagName") {{ tagName }}
+ .tags-more(v-if='remainingSelectedTags.length > 0') +{{ $t('more', { count: remainingSelectedTags.length }) }}
+ .dropdown-toggle
+ tags-popup(v-if="showTagsSelect", :tags="user.tags", v-model="task.tags")
.option(v-if="task.type === 'habit'")
label(v-once) {{ $t('resetStreak') }}
- b-dropdown(:text="$t(task.frequency)")
+ b-dropdown.streak-dropdown(:text="$t(task.frequency)")
b-dropdown-item(v-for="frequency in ['daily', 'weekly', 'monthly']", :key="frequency", @click="task.frequency = frequency", :class="{active: task.frequency === frequency}")
| {{ $t(frequency) }}
@@ -328,6 +321,88 @@
}
}
+ .tags-select {
+ position: relative;
+
+ .tags-inline {
+ align-items: center;
+ display: flex;
+ justify-content: flex-start;
+
+ label {
+ margin: 0;
+ }
+
+ .category-wrap {
+ cursor: inherit;
+ position: relative;
+ border: 1px solid transparent;
+ border-radius: 2px;
+ margin-left: 4em;
+
+ &.active {
+ border-color: $purple-500;
+
+ .category-select {
+ box-shadow: none;
+ }
+ }
+
+ .category-select {
+ align-items: center;
+ display: flex;
+ padding: .6em;
+ padding-right: 2.8em;
+ width: 100%;
+
+ .tags-none {
+ margin: .26em 0 .26em .6em;
+
+ & + .dropdown-toggle {
+ right: 1.3em;
+ }
+ }
+
+ .tags-more {
+ color: #a5a1ac;
+ flex: 0 1 auto;
+ font-size: 12px;
+ text-align: left;
+ position: relative;
+ left: .5em;
+ width: 100%;
+ }
+
+ .dropdown-toggle {
+ position: absolute;
+ right: 1em;
+ top: .8em;
+ }
+
+ .category-label {
+ min-width: 68px;
+ overflow: hidden;
+ padding: .5em 1em;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ width: 68px;
+ word-wrap: break-word;
+ }
+ }
+ }
+ }
+
+ .tags-popup {
+ position: absolute;
+ top: 3.5em;
+ left: 6.2em;
+ }
+ }
+
+ .streak-dropdown {
+ margin-left: .5em;
+ }
+
.checklist-group {
border-top: 1px solid $gray-500;
}
@@ -418,6 +493,7 @@