diff --git a/website/client/assets/scss/categories.scss b/website/client/assets/scss/categories.scss new file mode 100644 index 0000000000..7317e9ee5e --- /dev/null +++ b/website/client/assets/scss/categories.scss @@ -0,0 +1,39 @@ +.category-box { + padding: 1em; + max-width: 400px; + position: absolute; + top: -480px; + padding: 2em; + border-radius: 2px; + background-color: $white; + box-shadow: 0 2px 2px 0 rgba($black, 0.15), 0 1px 4px 0 rgba($black, 0.1); +} + +.category-label { + min-width: 100px; + border-radius: 100px; + background-color: $gray-600; + padding: .5em; + display: inline-block; + margin-right: .5em; + font-size: 12px; + font-weight: 500; + line-height: 1.33; + text-align: center; + color: $gray-300; +} + +.category-select { + border-radius: 2px; + background-color: $white; + box-shadow: 0 2px 2px 0 rgba($black, 0.16), 0 1px 4px 0 rgba($black, 0.12); + padding: 1em; +} + +.category-select:hover { + cursor: pointer; +} + +.category-wrap { + margin-top: .5em; +} diff --git a/website/client/assets/scss/icon.scss b/website/client/assets/scss/icon.scss index 0686d9e2e5..21a7e7686d 100644 --- a/website/client/assets/scss/icon.scss +++ b/website/client/assets/scss/icon.scss @@ -1,13 +1,15 @@ .svg-icon { - display: inline-block; - width: 1em; - height: 1em; + display: block; stroke-width: 0; stroke: currentColor; fill: currentColor; transition: none !important; -} -.svg-icon * { - transition: none !important; + svg { + display: block; + } + + * { + transition: none !important; + } } \ No newline at end of file diff --git a/website/client/assets/scss/index.scss b/website/client/assets/scss/index.scss index e1b9ce5731..acd5b2a8ec 100644 --- a/website/client/assets/scss/index.scss +++ b/website/client/assets/scss/index.scss @@ -21,4 +21,6 @@ @import './item'; @import './stats'; @import './icon'; +@import './task'; +@import './categories'; @import './dragdrop'; diff --git a/website/client/assets/scss/page.scss b/website/client/assets/scss/page.scss index 65ca00f521..a888e6c537 100644 --- a/website/client/assets/scss/page.scss +++ b/website/client/assets/scss/page.scss @@ -3,7 +3,7 @@ html { } html, body { - height: 100%; + height: calc(100% - 56px); // 56px is the menu background: $gray-700; } diff --git a/website/client/assets/scss/task.scss b/website/client/assets/scss/task.scss new file mode 100644 index 0000000000..ee64dc34e8 --- /dev/null +++ b/website/client/assets/scss/task.scss @@ -0,0 +1,86 @@ + .task { + // for editing rewards or when a task is created + &-purple { + background: $purple-300; + } + + &-worst { + background: $maroon-100; + &-control { + background: darken($maroon-100, 12%); + } + } + + &-worse { + background: $red-100; + &-control { + background: darken($red-100, 12%); + } + } + + &-bad { + background: $orange-100; + &-control { + background: darken($orange-100, 12%); + } + } + + &-neutral { + background: $yellow-50; + &-control { + background: darken($yellow-50, 12%); + } + } + + &-good { + background: $green-10; + &-control { + background: darken($green-10, 12%); + } + } + + &-better { + background: $blue-50; + &-control { + background: darken($blue-50, 12%); + } + } + + &-best { + background: $teal-50; + &-control { + background: darken($teal-50, 12%); + } + } + + &-reward { + background: rgba($yellow-500, 0.26); + } + + &-daily-todo-disabled { + background: $gray-500; + + &-control { + background: $gray-400; + color: $gray-200; + } + } + + &-daily-todo-content-disabled { + background: $gray-600; + + * { + color: $gray-300 !important; + } + } + + &-habit-disabled { + background: $gray-700; + color: rgba(0, 0, 0, 0.12); + + &-control { + color: rgba(0, 0, 0, 0.12) !important; + border: 1px solid rgba(0, 0, 0, 0.12); + } + } +} \ No newline at end of file diff --git a/website/client/assets/svg/calendar.svg b/website/client/assets/svg/calendar.svg new file mode 100644 index 0000000000..ba08c072e8 --- /dev/null +++ b/website/client/assets/svg/calendar.svg @@ -0,0 +1,3 @@ + + + diff --git a/website/client/assets/svg/challenge.svg b/website/client/assets/svg/challenge.svg new file mode 100644 index 0000000000..1610d4bb2e --- /dev/null +++ b/website/client/assets/svg/challenge.svg @@ -0,0 +1,3 @@ + + + diff --git a/website/client/assets/svg/check.svg b/website/client/assets/svg/check.svg new file mode 100644 index 0000000000..2ae3d23949 --- /dev/null +++ b/website/client/assets/svg/check.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/website/client/assets/svg/daily.svg b/website/client/assets/svg/daily.svg new file mode 100644 index 0000000000..f1e28a1858 --- /dev/null +++ b/website/client/assets/svg/daily.svg @@ -0,0 +1,3 @@ + + + diff --git a/website/client/assets/svg/habit.svg b/website/client/assets/svg/habit.svg new file mode 100644 index 0000000000..3a433ff31d --- /dev/null +++ b/website/client/assets/svg/habit.svg @@ -0,0 +1,3 @@ + + + diff --git a/website/client/assets/svg/negative.svg b/website/client/assets/svg/negative.svg new file mode 100644 index 0000000000..ca3e1d2e65 --- /dev/null +++ b/website/client/assets/svg/negative.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/website/client/assets/svg/positive.svg b/website/client/assets/svg/positive.svg new file mode 100644 index 0000000000..2488512400 --- /dev/null +++ b/website/client/assets/svg/positive.svg @@ -0,0 +1,3 @@ + + + diff --git a/website/client/assets/svg/reward.svg b/website/client/assets/svg/reward.svg new file mode 100644 index 0000000000..52232a2259 --- /dev/null +++ b/website/client/assets/svg/reward.svg @@ -0,0 +1,3 @@ + + + diff --git a/website/client/assets/svg/streak.svg b/website/client/assets/svg/streak.svg new file mode 100644 index 0000000000..e1314778b2 --- /dev/null +++ b/website/client/assets/svg/streak.svg @@ -0,0 +1,3 @@ + + + diff --git a/website/client/assets/svg/tags.svg b/website/client/assets/svg/tags.svg new file mode 100644 index 0000000000..c4becad0b6 --- /dev/null +++ b/website/client/assets/svg/tags.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/website/client/assets/svg/todo.svg b/website/client/assets/svg/todo.svg new file mode 100644 index 0000000000..f7a89b4be6 --- /dev/null +++ b/website/client/assets/svg/todo.svg @@ -0,0 +1,3 @@ + + + diff --git a/website/client/components/guilds/groupFormModal.vue b/website/client/components/guilds/groupFormModal.vue index 48f4871f36..5d434f0e3b 100644 --- a/website/client/components/guilds/groupFormModal.vue +++ b/website/client/components/guilds/groupFormModal.vue @@ -101,31 +101,6 @@ margin-top: 1em; } - .category-box { - padding: 1em; - max-width: 400px; - position: absolute; - top: -480px; - padding: 2em; - border-radius: 2px; - background-color: $white; - box-shadow: 0 2px 2px 0 rgba($black, 0.15), 0 1px 4px 0 rgba($black, 0.1); - } - - .category-label { - min-width: 100px; - border-radius: 100px; - background-color: $gray-600; - padding: .5em; - display: inline-block; - margin-right: .5em; - font-size: 12px; - font-weight: 500; - line-height: 1.33; - text-align: center; - color: $gray-300; - } - .item-with-icon { display: inline-block; @@ -146,21 +121,6 @@ margin-top: 1em; } - .category-select { - border-radius: 2px; - background-color: $white; - box-shadow: 0 2px 2px 0 rgba($black, 0.16), 0 1px 4px 0 rgba($black, 0.12); - padding: 1em; - } - - .category-select:hover { - cursor: pointer; - } - - .category-wrap { - margin-top: .5em; - } - .icon { margin-left: .5em; display: inline-block; diff --git a/website/client/components/guilds/publicGuildItem.vue b/website/client/components/guilds/publicGuildItem.vue index 815fe2948f..40cf650d99 100644 --- a/website/client/components/guilds/publicGuildItem.vue +++ b/website/client/components/guilds/publicGuildItem.vue @@ -35,20 +35,6 @@ box-shadow: 0 2px 2px 0 rgba($black, 0.15), 0 1px 4px 0 rgba($black, 0.1); margin-bottom: 1rem; - .category-label { - min-width: 100px; - border-radius: 100px; - background-color: $gray-600; - padding: .5em; - display: inline-block; - margin-right: .5em; - font-size: 12px; - font-weight: 500; - line-height: 1.33; - text-align: center; - color: $gray-300; - } - .recommend-text { font-size: 12px; font-style: italic; diff --git a/website/client/components/task.vue b/website/client/components/task.vue deleted file mode 100644 index 7256b4ba03..0000000000 --- a/website/client/components/task.vue +++ /dev/null @@ -1,33 +0,0 @@ - - - \ No newline at end of file diff --git a/website/client/components/tasks/column.vue b/website/client/components/tasks/column.vue new file mode 100644 index 0000000000..c4360c47de --- /dev/null +++ b/website/client/components/tasks/column.vue @@ -0,0 +1,194 @@ + + + + + diff --git a/website/client/components/tasks/task.vue b/website/client/components/tasks/task.vue new file mode 100644 index 0000000000..84e5c58efb --- /dev/null +++ b/website/client/components/tasks/task.vue @@ -0,0 +1,293 @@ + + + + + + + \ No newline at end of file diff --git a/website/client/components/tasks/user.vue b/website/client/components/tasks/user.vue new file mode 100644 index 0000000000..41535f70a2 --- /dev/null +++ b/website/client/components/tasks/user.vue @@ -0,0 +1,56 @@ + + + + + diff --git a/website/client/components/userTasks.vue b/website/client/components/userTasks.vue deleted file mode 100644 index 3a46db218e..0000000000 --- a/website/client/components/userTasks.vue +++ /dev/null @@ -1,163 +0,0 @@ - - - diff --git a/website/client/index.html b/website/client/index.html index 6eac2d4fda..73f895a0f0 100644 --- a/website/client/index.html +++ b/website/client/index.html @@ -5,7 +5,7 @@ Habitica - + diff --git a/website/client/libs/asyncResource.js b/website/client/libs/asyncResource.js index 7d2ba8e394..852cac06e5 100644 --- a/website/client/libs/asyncResource.js +++ b/website/client/libs/asyncResource.js @@ -36,8 +36,11 @@ export function loadAsyncResource ({store, path, url, deserialize, forceLoad = f } else if (loadingStatus === 'NOT_LOADED' || loadingStatus === 'LOADED' && forceLoad) { return axios.get(url).then(response => { // TODO support more params resource.loadingStatus = 'LOADED'; - resource.data = deserialize(response); - return resource; + // deserialize can be a promise + return Promise.resolve(deserialize(response)).then(deserializedData => { + resource.data = deserializedData; + return resource; + }); }); } else { return Promise.reject(new Error(`Invalid loading status "${loadingStatus} for resource at "${path}".`)); diff --git a/website/client/main.js b/website/client/main.js index 77a2f4f8f1..dd8809fa1b 100644 --- a/website/client/main.js +++ b/website/client/main.js @@ -47,7 +47,7 @@ export default new Vue({ // Mount the app when user and tasks are loaded const userDataWatcher = this.$store.watch(state => [state.user.data, state.tasks.data], ([user, tasks]) => { - if (user && user._id && Array.isArray(tasks)) { + if (user && user._id && tasks && Array.isArray(tasks.habits)) { userDataWatcher(); // remove the watcher this.$mount('#app'); } diff --git a/website/client/router.js b/website/client/router.js index df807fa7f7..1065f5b65a 100644 --- a/website/client/router.js +++ b/website/client/router.js @@ -7,14 +7,15 @@ import EmptyView from './components/emptyView'; import ParentPage from './components/parentPage'; import Page from './components/page'; -// Tasks -import UserTasks from './components/userTasks'; -// Except for tasks that are always loaded all the other main level +// All the main level // components are loaded in separate webpack chunks. // See https://webpack.js.org/guides/code-splitting-async/ // for docs +// Tasks +const UserTasks = () => import(/* webpackChunkName: "userTasks" */'./components/tasks/user'); + // Inventory const InventoryContainer = () => import(/* webpackChunkName: "inventory" */'./components/inventory/index'); const ItemsPage = () => import(/* webpackChunkName: "inventory" */'./components/inventory/items/index'); diff --git a/website/client/store/actions/tasks.js b/website/client/store/actions/tasks.js index d939810e3e..71ceca0221 100644 --- a/website/client/store/actions/tasks.js +++ b/website/client/store/actions/tasks.js @@ -1,13 +1,54 @@ import { loadAsyncResource } from 'client/libs/asyncResource'; +import compact from 'lodash/compact'; + export function fetchUserTasks (store, forceLoad = false) { return loadAsyncResource({ store, path: 'tasks', url: '/api/v3/tasks/user', deserialize (response) { - return response.data.data; + // Wait for the user to be loaded before deserializing + // because user.tasksOrder is necessary + return store.dispatch('user:fetch').then((userResource) => { + return store.dispatch('tasks:order', [response.data.data, userResource.data.tasksOrder]); + }); }, forceLoad, }); +} + +export function order (store, [rawTasks, tasksOrder]) { + const tasks = { + habits: [], + dailys: [], + todos: [], + rewards: [], + }; + + rawTasks.forEach(task => { + tasks[`${task.type}s`].push(task); + }); + + Object.keys(tasks).forEach((type) => { + let tasksOfType = tasks[type]; + + const orderOfType = tasksOrder[type]; + const orderedTasks = new Array(tasksOfType.length); + const unorderedTasks = []; // what we want to add later + + tasksOfType.forEach((task, index) => { + const taskId = task._id; + const i = orderOfType[index] === taskId ? index : orderOfType.indexOf(taskId); + if (i === -1) { + unorderedTasks.push(task); + } else { + orderedTasks[i] = task; + } + }); + + tasks[type] = compact(orderedTasks).concat(unorderedTasks); + }); + + return tasks; } \ No newline at end of file diff --git a/website/client/store/getters/tasks.js b/website/client/store/getters/tasks.js index 6e641c0708..72c9e7c419 100644 --- a/website/client/store/getters/tasks.js +++ b/website/client/store/getters/tasks.js @@ -1,6 +1,64 @@ +import { shouldDo } from 'common/script/cron'; + // Return all the tags belonging to an user task export function getTagsFor (store) { return (task) => store.state.user.data.tags .filter(tag => task.tags.indexOf(tag.id) !== -1) .map(tag => tag.name); +} + +function getTaskColorByValue (value) { + if (value < -20) { + return 'task-worst'; + } else if (value < -10) { + return 'task-worse'; + } else if (value < -1) { + return 'task-bad'; + } else if (value < 1) { + return 'task-neutral'; + } else if (value < 5) { + return 'task-good'; + } else if (value < 10) { + return 'task-better'; + } else { + return 'task-best'; + } +} + +export function getTaskClasses (store) { + const userPreferences = store.state.user.data.preferences; + + // Purpose is one of 'controls', 'editModal', 'createModal', 'content' + return (task, purpose) => { + const type = task.type; + + switch (purpose) { + case 'createModal': + return 'task-purple'; + case 'editModal': + return type === 'reward' ? 'task-purple' : getTaskColorByValue(task.value); + case 'control': + switch (type) { + case 'daily': + if (task.completed || !shouldDo(new Date(), task, userPreferences)) return 'task-daily-todo-disabled'; + return getTaskColorByValue(task.value); + case 'todo': + if (task.completed) return 'task-daily-todo-disabled'; + return getTaskColorByValue(task.value); + case 'habit': + return { + up: task.up ? getTaskColorByValue(task.value) : 'task-habit-disabled', + down: task.down ? getTaskColorByValue(task.value) : 'task-habit-disabled', + }; + case 'reward': + return 'task-reward'; + } + break; + case 'content': + if (type === 'daily' && (task.completed || !task.isDue) || type === 'todo' && task.completed) { + return 'task-daily-todo-content-disabled'; + } + break; + } + }; } \ No newline at end of file diff --git a/website/common/locales/en/newClient.json b/website/common/locales/en/newClient.json index 7e7d04f71e..81e710db12 100644 --- a/website/common/locales/en/newClient.json +++ b/website/common/locales/en/newClient.json @@ -1,4 +1,5 @@ { + "viewParty": "View Party", "shops": "Shops", "faq": "FAQ", "costumePopoverText": "Select \"Use Costume\" to equip items to your avatar without affecting the stats from your Battle Gear! This means that you can dress up your avatar in whatever outfit you like while still having your best Battle Gear equipped.", @@ -9,6 +10,16 @@ "guildBank": "Guild Bank", "chatPlaceHolder": "Type your message to Guild members here", "today": "Today", + "theseAreYourTasks": "These are your <%= taskType %>", + "habitsDesc": "Habits don't have a rigid schedule. You can check them off multiple times per day.", + "dailysDesc": "Dailies repeat on a regular basis. Choose the schedule that works best for you!", + "todosDesc": "To-Dos need to be completed once. Add checklists to your To-Dos to increase their value.", + "rewardsDesc": "Rewards are a great way to use Habitica and complete your tasks. Try adding a few today!", + "dueIn": "Due <%= dueIn %>", + "complete2": "Complete", + "custom": "Custom", + "wishlist": "Wishlist", + "scheduled": "Scheduled", "like": "Like", "copyAsTodo": "Copy as To-Do", "report": "Report",