Client Tasks v1 (#8823)

* remove unused elements from tasks page

* remove components

* client: tasks: wip

* tasks: order, start styling them

* more tasks works

* habits controls

* more work

* tasks icons

* split columns in their own component

* implement tags for tasks

* wip

* add columns description
This commit is contained in:
Matteo Pagliazzi
2017-06-26 23:55:14 +02:00
committed by GitHub
parent 7c5bd526b1
commit 6fb4c1b576
30 changed files with 834 additions and 265 deletions

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -21,4 +21,6 @@
@import './item';
@import './stats';
@import './icon';
@import './task';
@import './categories';
@import './dragdrop';

View File

@@ -3,7 +3,7 @@ html {
}
html, body {
height: 100%;
height: calc(100% - 56px); // 56px is the menu
background: $gray-700;
}

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14">
<path fill-rule="evenodd" d="M2 12h10V6H2v6zM12 2V0h-2v2H4V0H2v2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2z"/>
</svg>

After

Width:  |  Height:  |  Size: 227 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="12" viewBox="0 0 14 12">
<path fill-rule="evenodd" d="M10 6.306L2.582 7.542A.5.5 0 0 1 2 7.05V2.591a.5.5 0 0 1 .582-.493L10 3.334v2.972zm2.329-4.612l-.024-.004c-.007-.002-.012-.007-.02-.009-.017-.005-.035.001-.052-.003L2.329.028A2 2 0 0 0 0 2v5.64a2 2 0 0 0 2.329 1.972l7.056-1.176-.525 2.1a1.175 1.175 0 0 0 2.28.57l.772-3.09.417-.07A2 2 0 0 0 14 5.971V3.667a2 2 0 0 0-1.671-1.973z"/>
</svg>

After

Width:  |  Height:  |  Size: 456 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="10" viewBox="0 0 13 10">
<path fill-rule="evenodd" d="M4.662 9.832c-.312 0-.61-.123-.831-.344L0 5.657l1.662-1.662 2.934 2.934L10.534 0l1.785 1.529-6.764 7.893a1.182 1.182 0 0 1-.848.409l-.045.001"/>
</svg>

After

Width:  |  Height:  |  Size: 268 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="20" viewBox="0 0 24 20">
<path fill="#C3C0C7" fill-rule="evenodd" d="M13 16h2v-2h-2v2zm-4 0h2v-2H9v2zm-4 0h2v-2H5v2zm12-4h2v-2h-2v2zm-4 0h2v-2h-2v2zm-4 0h2v-2H9v2zm13-4H2v8a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8zm2-2v10a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V6a4 4 0 0 1 4-4h1V0h2v2h10V0h2v2h1a4 4 0 0 1 4 4zM5 12h2v-2H5v2z"/>
</svg>

After

Width:  |  Height:  |  Size: 382 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="20" viewBox="0 0 30 20">
<path fill="#C3C0C7" fill-rule="evenodd" d="M11 11H9v2H7v-2H5V9h2V7h2v2h2v2zm8 0h6V9h-6v2zm9 5c0 1.103-.897 2-2 2H16V2h10c1.103 0 2 .897 2 2v12zM4 18c-1.103 0-2-.897-2-2V4c0-1.103.897-2 2-2h10v16H4zM26 0H4a4 4 0 0 0-4 4v12a4 4 0 0 0 4 4h22a4 4 0 0 0 4-4V4a4 4 0 0 0-4-4z"/>
</svg>

After

Width:  |  Height:  |  Size: 369 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="2" viewBox="0 0 10 2">
<path fill-rule="evenodd" d="M0 0h10v2H0z"/>
</svg>

After

Width:  |  Height:  |  Size: 135 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10">
<path fill-rule="evenodd" d="M6 4V0H4v4H0v2h4v4h2V6h4V4H6z"/>
</svg>

After

Width:  |  Height:  |  Size: 157 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="26" height="20" viewBox="0 0 26 20">
<path fill="#C3C0C7" fill-rule="evenodd" d="M24 10h-8V8h4a2 2 0 0 0 2-2V2c1.103 0 2 .897 2 2v6zm0 6a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-4h8v1a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2v-1h8v4zM2 4c0-1.103.897-2 2-2v4a2 2 0 0 0 2 2h4v2H2V4zm10 9h2V8h-2v5zm8-11v4H6V2h14zm2-2H4a4 4 0 0 0-4 4v12a4 4 0 0 0 4 4h18a4 4 0 0 0 4-4V4a4 4 0 0 0-4-4z"/>
</svg>

After

Width:  |  Height:  |  Size: 421 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="8" viewBox="0 0 12 8">
<path fill-rule="evenodd" d="M11.376 3.15L6.777.086A.5.5 0 0 0 6 .5v6.132a.5.5 0 0 0 .777.416l4.599-3.066a.5.5 0 0 0 0-.832M.777.085L6 3.567.777 7.049A.5.5 0 0 1 0 6.633V.5A.5.5 0 0 1 .777.085"/>
</svg>

After

Width:  |  Height:  |  Size: 289 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14">
<path fill-rule="evenodd" d="M10 3a1 1 0 1 1 0 2 1 1 0 0 1 0-2zM2.004 6.994L7 2h5l-.004 5.006L7 12l.004-.004-5-5.002zM0 7c0 .55.22 1.05.59 1.41l5 5a1.996 1.996 0 0 0 2.83 0l4.99-4.99c.37-.37.59-.87.59-1.42V2c0-1.11-.89-2-2-2H7c-.55 0-1.05.22-1.41.58l-5 5C.23 5.94 0 6.44 0 7z"/>
</svg>

After

Width:  |  Height:  |  Size: 373 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
<path fill="#C3C0C7" fill-rule="evenodd" d="M8.343 14.916c-.312 0-.61-.123-.831-.344l-3.831-3.831 1.662-1.662 2.934 2.934 5.938-6.929L16 6.613l-6.764 7.893a1.182 1.182 0 0 1-.848.409l-.045.001zM18 16c0 1.103-.897 2-2 2H4c-1.102 0-2-.897-2-2V4c0-1.103.898-2 2-2h12c1.103 0 2 .897 2 2v12zM16 0H4a4 4 0 0 0-4 4v12a4 4 0 0 0 4 4h12a4 4 0 0 0 4-4V4a4 4 0 0 0-4-4z"/>
</svg>

After

Width:  |  Height:  |  Size: 457 B

View File

@@ -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;

View File

@@ -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;

View File

@@ -1,33 +0,0 @@
<template lang="pug">
li
ul
li
strong {{task.text}}
li(v-if="task.type === 'habit'") up: {{task.up}}, down: {{task.down}}
li value: {{task.value}}
template(v-if="task.type === 'daily' || task.type === 'todo'")
li completed: {{task.completed}}
li
span checklist
ul
li(v-for="checklist in task.checklist") {{checklist.text}}
template(v-if="task.type === 'daily'")
li streak: {{task.streak}}
li repeat: {{task.repeat}}
li(v-if="task.type === 'todo'") due date: {{task.date}}
li attribute {{task.attribute}}
li difficulty {{task.priority}}
li tags {{getTagsFor(task)}}
</template>
<script>
import { mapState, mapGetters } from 'client/libs/store';
export default {
props: ['task'],
computed: {
...mapState({user: 'user.data'}),
...mapGetters({getTagsFor: 'tasks:getTagsFor'}),
},
};
</script>

View File

@@ -0,0 +1,194 @@
<template lang="pug">
.tasks-column
.d-flex
h2.tasks-column-title(v-once) {{ $t(types[type].label) }}
.filters.d-flex.justify-content-end
.filter.small-text(
v-for="filter in types[type].filters",
:class="{active: activeFilter.label === filter.label}",
@click="activeFilter = filter",
) {{ $t(filter.label) }}
.tasks-list
task(v-for="task in tasks[`${type}s`]", :key="task.id", :task="task", v-if="activeFilter.filter(task)")
.bottom-gradient
.column-background(v-if="isUser === true", :class="{'initial-description': tasks[`${type}s`].length === 0}")
.svg-icon(v-html="icons[type]", :class="`icon-${type}`", v-once)
h3(v-once) {{$t('theseAreYourTasks', {taskType: `${type}s`})}}
.small-text {{$t(`${type}sDesc`)}}
</template>
<style lang="scss" scoped>
@import '~client/assets/scss/colors.scss';
.tasks-column {
flex-grow: 1;
}
.tasks-list {
border-radius: 4px;
background: $gray-600;
padding: 8px;
// not sure why but this is necessary or the last task will overflow the container
padding-bottom: 0.1px;
position: relative;
height: calc(100% - 64px);
}
.bottom-gradient {
position: absolute;
bottom: 0px;
left: -0px;
height: 42px;
background-image: linear-gradient(to bottom, rgba(52, 49, 58, 0), #34313a);
width: 100%;
}
.tasks-column-title {
margin-bottom: 8px;
}
.filters {
flex-grow: 1;
}
.filter {
font-weight: bold;
color: $gray-100;
font-style: normal;
padding: 8px;
cursor: pointer;
&:hover {
color: $purple-200;
}
&.active {
color: $purple-200;
border-bottom: 2px solid $purple-200;
padding-bottom: 6px;
}
}
.column-background {
position: absolute;
bottom: 32px;
z-index: 7;
&.initial-description {
top: 30%;
}
.svg-icon {
margin: 0 auto;
margin-bottom: 12px;
}
h3, .small-text {
color: $gray-300;
text-align: center;
}
h3 {
font-weight: normal;
margin-bottom: 4px;
}
.small-text {
font-style: normal;
padding-left: 24px;
padding-right: 24px;
}
}
.icon-habit {
width: 30px;
height: 20px;
}
.icon-daily {
width: 30px;
height: 20px;
}
.icon-todo {
width: 20px;
height: 20px;
}
.icon-reward {
width: 26px;
height: 20px;
}
</style>
<script>
import Task from './task';
import { mapState } from 'client/libs/store';
import { shouldDo } from 'common/script/cron';
import habitIcon from 'assets/svg/habit.svg';
import dailyIcon from 'assets/svg/daily.svg';
import todoIcon from 'assets/svg/todo.svg';
import rewardIcon from 'assets/svg/reward.svg';
export default {
components: {
Task,
},
props: ['type', 'isUser'],
data () {
const types = Object.freeze({
habit: {
label: 'habits',
filters: [
{label: 'all', filter: () => true, default: true},
{label: 'yellowred', filter: t => t.value < 1}, // weak
{label: 'greenblue', filter: t => t.value >= 1}, // strong
],
},
daily: {
label: 'dailies',
filters: [
{label: 'all', filter: () => true, default: true},
{label: 'due', filter: t => !t.completed && shouldDo(new Date(), t, this.userPreferences)},
{label: 'notDue', filter: t => t.completed || !shouldDo(new Date(), t, this.userPreferences)},
],
},
todo: {
label: 'todos',
filters: [
{label: 'remaining', filter: t => !t.completed, default: true}, // active
{label: 'scheduled', filter: t => !t.completed && t.date},
{label: 'complete2', filter: t => t.completed},
],
},
reward: {
label: 'rewards',
filters: [
{label: 'all', filter: () => true, default: true},
{label: 'custom', filter: () => true}, // all rewards made by the user
{label: 'wishlist', filter: () => false}, // not user tasks
],
},
});
const icons = Object.freeze({
habit: habitIcon,
daily: dailyIcon,
todo: todoIcon,
reward: rewardIcon,
});
return {
types,
activeFilter: types[this.type].filters.find(f => f.default === true),
icons,
};
},
computed: {
...mapState({
tasks: 'tasks.data',
userPreferences: 'user.data.preferences',
}),
},
};
</script>

View File

@@ -0,0 +1,293 @@
<template lang="pug">
.task.d-flex
// Habits left side control
.left-control.d-flex.align-items-center.justify-content-center(v-if="task.type === 'habit'", :class="controlClass.up")
.task-control.habit-control(:class="controlClass.up + '-control'")
.svg-icon.positive(v-html="icons.positive")
// Dailies and todos left side control
.left-control.d-flex.align-items-center.justify-content-center(v-if="task.type === 'daily' || task.type === 'todo'", :class="controlClass")
.task-control.daily-todo-control(:class="controlClass + '-control'")
.svg-icon.check(v-html="icons.check", v-if="task.completed")
// Task title, description and icons
.task-content(:class="contentClass")
h3.task-title(:class="{ 'has-notes': task.notes }") {{task.text}}
.task-notes.small-text {{task.notes}}
.icons.small-text.d-flex.align-items-center
.d-flex.align-items-center(v-if="task.type === 'todo' && task.date", :class="{'due-overdue': isDueOverdue}")
.svg-icon.calendar(v-html="icons.calendar")
span {{dueIn}}
.icons-right.d-flex.justify-content-end
.d-flex.align-items-center(v-if="showStreak")
.svg-icon.streak(v-html="icons.streak")
span(v-if="task.type === 'daily'") {{task.streak}}
span(v-if="task.type === 'habit'")
span.m-0(v-if="task.up") +{{task.counterUp}}
span.m-0(v-if="task.up && task.down") &nbsp;|&nbsp;
span.m-0(v-if="task.down") -{{task.counterDown}}
.d-flex.align-items-center(v-if="task.challenge && task.challenge.id")
.svg-icon.challenge(v-html="icons.challenge")
b-popover.tags-popover.no-span-margin(
:triggers="['hover']",
:placement="'bottom'",
:popover-style="{'max-width': '1000px'}",
)
.d-flex.align-items-center(slot="content")
.tags-popover-title(v-once) {{ `${$t('tags')}:` }}
.tag-label(v-for="tag in getTagsFor(task)") {{tag}}
.d-flex.align-items-center(v-if="task.tags && task.tags.length > 0")
.svg-icon.tags(v-html="icons.tags")
// Habits right side control
.right-control.d-flex.align-items-center.justify-content-center(v-if="task.type === 'habit'", :class="controlClass.down")
.task-control.habit-control(:class="controlClass.down + '-control'")
.svg-icon.negative(v-html="icons.negative")
// Rewards right side control
.right-control.d-flex.align-items-center.justify-content-center.reward-control(v-if="task.type === 'reward'", :class="controlClass")
.svg-icon(v-html="icons.gold")
.small-text {{task.value}}
</template>
<style lang="scss">
@import '~client/assets/scss/colors.scss';
.task {
margin-bottom: 8px;
box-shadow: 0 2px 2px 0 rgba($black, 0.16), 0 1px 4px 0 rgba($black, 0.12);
background: $white;
border-radius: 2px;
z-index: 9;
position: relative;
}
.task-title {
margin-bottom: 8px;
color: $gray-10;
font-weight: normal;
&.has-notes {
margin-bottom: 0px;
}
}
.task-notes {
color: $gray-100;
font-style: normal;
margin-bottom: 4px;
}
.task-content {
padding: 8px;
flex-grow: 1;
}
.icons {
color: $gray-300;
font-style: normal;
&-right {
flex-grow: 1;
}
}
.icons-right .svg-icon {
margin-left: 8px;
}
.icons span {
margin-left: 4px;
}
.no-span-margin span {
margin-left: 0px !important;
}
.svg-icon.streak {
width: 11.6px;
height: 7.1px;
}
.tags.svg-icon, .calendar.svg-icon {
width: 14px;
height: 14px;
}
.tags:hover {
color: $purple-500;
}
.due-overdue {
color: $red-50;
}
.calendar.svg-icon {
margin-right: 2px;
margin-top: -2px;
}
.challenge.svg-icon {
width: 14px;
height: 12px;
}
.check.svg-icon {
width: 12.3px;
height: 9.8px;
margin: 8px;
}
.left-control, .right-control {
width: 40px;
flex-shrink: 0;
}
.left-control {
border-top-left-radius: 2px;
border-bottom-left-radius: 2px;
}
.right-control {
border-top-right-radius: 2px;
border-bottom-right-radius: 2px;
}
.task-control {
width: 28px;
height: 28px;
}
.habit-control {
border-radius: 100px;
color: $white;
.svg-icon {
width: 10px;
margin: 0 auto;
}
.positive {
margin-top: 9px;
}
.negative {
margin-top: 13px;
}
}
.daily-todo-control {
border-radius: 2px;
}
.reward-control {
flex-direction: column;
padding-top: 16px;
padding-bottom: 12px;
.svg-icon {
width: 24px;
height: 24px;
}
.small-text {
margin-top: 4px;
color: $yellow-10;
}
}
</style>
<style lang="scss"> // not working as scoped css
@import '~client/assets/scss/colors.scss';
.tags-popover {
// TODO fix padding, see https://github.com/bootstrap-vue/bootstrap-vue/issues/559#issuecomment-311150335
white-space: nowrap;
}
.tags-popover-title {
margin-right: 4px;
display: block;
float: left;
}
.tag-label {
display: block;
float: left;
margin-left: 4px;
border-radius: 100px;
background-color: $gray-50;
padding: 4px 10px;
color: $gray-300;
white-space: nowrap;
}
</style>
<script>
import { mapState, mapGetters } from 'client/libs/store';
import moment from 'moment';
import positiveIcon from 'assets/svg/positive.svg';
import negativeIcon from 'assets/svg/negative.svg';
import goldIcon from 'assets/svg/gold.svg';
import streakIcon from 'assets/svg/streak.svg';
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 bPopover from 'bootstrap-vue/lib/components/popover';
export default {
components: {
bPopover,
},
props: ['task'],
data () {
return {
icons: Object.freeze({
positive: positiveIcon,
negative: negativeIcon,
gold: goldIcon,
streak: streakIcon,
calendar: calendarIcon,
challenge: challengeIcon,
tags: tagsIcon,
check: checkIcon,
}),
};
},
computed: {
...mapState({user: 'user.data'}),
...mapGetters({
getTagsFor: 'tasks:getTagsFor',
getTaskClasses: 'tasks:getTaskClasses',
}),
leftControl () {
const task = this.task;
if (task.type === 'reward') return false;
return true;
},
rightControl () {
const task = this.task;
if (task.type === 'reward') return true;
if (task.type === 'habit') return true;
return false;
},
controlClass () {
return this.getTaskClasses(this.task, 'control');
},
contentClass () {
return this.getTaskClasses(this.task, 'content');
},
showStreak () {
if (this.task.streak !== undefined) return true;
if (this.task.type === 'habit' && (this.task.up || this.task.down)) return true;
return false;
},
isDueOverdue () {
return moment().diff(this.task.date, 'days') >= 0;
},
dueIn () {
const dueIn = moment().to(this.task.date);
return this.$t('dueIn', {dueIn});
},
},
};
</script>

View File

@@ -0,0 +1,56 @@
<template lang="pug">
.row.user-tasks-page
.col-12
.row.tasks-navigation
.col-4.offset-4
input.form-control.input-search(type="text", :placeholder="$t('search')")
.col-1.offset-3
button.btn.btn-success(v-once)
.svg-icon.positive(v-html="icons.positive")
| {{ $t('create') }}
.row.tasks-columns
task-column.col-3(v-for="column in columns", :type="column", :key="column", :isUser="true")
</template>
<style lang="scss" scoped>
@import '~client/assets/scss/colors.scss';
.user-tasks-page {
padding-top: 31px;
}
.tasks-navigation {
margin-bottom: 40px;
}
.positive {
display: inline-block;
width: 10px;
color: $green-500;
margin-right: 8px;
padding-top: 6px;
}
.tasks-columns {
height: 100%;
}
</style>
<script>
import Column from './column';
import positiveIcon from 'assets/svg/positive.svg';
export default {
components: {
TaskColumn: Column,
},
data () {
return {
columns: ['habit', 'daily', 'todo', 'reward'],
icons: Object.freeze({
positive: positiveIcon,
}),
};
},
};
</script>

View File

@@ -1,163 +0,0 @@
<template lang="pug">
.row
.col-12
.row
.col-3.p-4
h3 Input
input.form-control(type="text", placeholder="Placeholder")
.col-3.p-4
h3 Input Disabled
input.form-control(type="text", placeholder="Placeholder", disabled)
.col-3.p-4
h3 Input With Icon
input.form-control.input-search(type="text", placeholder="Placeholder")
.col-3.p-4
h3 Input With Icon Disabled
input.form-control.input-search(type="text", placeholder="Placeholder", disabled)
.col-3.p-4
h3 Input Valid
input.form-control.input-valid(type="text", placeholder="Placeholder")
.col-3.p-4
h3 Input Invalid
input.form-control.input-invalid(type="text", placeholder="Placeholder")
.row
.col-6.p-4
h3 Textarea
textarea.form-control(rows="5", cols="50")
.col-6.p-4
h3 Textarea Disabled
textarea.form-control(disabled, rows="10", cols="50")
.row
.col-2.p-4
toggleSwitch(label="Toggle Switch")
.row
.col-3.p-4
h3 Checkbox
label.custom-control.custom-checkbox
input.custom-control-input(type='checkbox')
span.custom-control-indicator
span.custom-control-description Check this custom checkbox
.col-3.p-4
h3 Checkbox Disabled Checked
label.custom-control.custom-checkbox
input.custom-control-input(type='checkbox', disabled, checked)
span.custom-control-indicator
span.custom-control-description Check this custom checkbox
.col-3.p-4
h3 Checkbox Disabled Not Checked
label.custom-control.custom-checkbox
input.custom-control-input(type='checkbox', disabled)
span.custom-control-indicator
span.custom-control-description Check this custom checkbox
.col-6.p-4
h3 Radio Button
form
label.custom-control.custom-radio
input#radio1.custom-control-input(name='radio', type='radio')
span.custom-control-indicator
span.custom-control-description Toggle this custom radio
label.custom-control.custom-radio
input#radio2.custom-control-input(name='radio', type='radio')
span.custom-control-indicator
span.custom-control-description Toggle this custom radio
.col-3.p-4
h3 Radio Button Disabled Checked
form
label.custom-control.custom-radio
input#radio3.custom-control-input(name='radio', type='radio', disabled, checked)
span.custom-control-indicator
span.custom-control-description Toggle this custom radio
.col-3.p-4
h3 Radio Button Disabled Not Checked
form
label.custom-control.custom-radio
input#radio3.custom-control-input(name='radio', type='radio', disabled)
span.custom-control-indicator
span.custom-control-description Toggle this custom radio
.row
.col-3.p-4
h3 Main Button
button.btn.btn-primary Button
.col-3.p-4
h3 Secondary Button
button.btn.btn-secondary Button
.col-3.p-4
h3 Green Button
button.btn.btn-success Button
.col-3.p-4
h3 Blue Button
button.btn.btn-info Button
.col-3.p-4
h3 Red Button
button.btn.btn-danger Button
.row
.col-3.p-4
h3 Main Button Disabled
button.btn.btn-primary(disabled=true) Button
.col-3.p-4
h3 Secondary Button Disabled
button.btn.btn-secondary(disabled=true) Button
.col-3.p-4
h3 Green Button Disabled
button.btn.btn-success(disabled=true) Button
.col-3.p-4
h3 Blue Button Disabled
button.btn.btn-info(disabled=true) Button
.col-3.p-4
h3 Red Button Disabled
button.btn.btn-danger(disabled=true) Button
.row
.col-6.p-4
h3 Dropdown Menu
b-dropdown(text="Menu", right=false)
b-dropdown-item(href="#") Menu item 1
b-dropdown-item(href="#") Menu item 2
b-dropdown-item(href="#") Menu item 3
b-dropdown-item(href="#") Menu item 4
.col-6.p-4
h3 Dropdown Menu Disabled
b-dropdown(text="Menu", disabled)
b-dropdown-item(href="#") Menu item 1
b-dropdown-item(href="#") Menu item 2
b-dropdown-item(href="#") Menu item 3
b-dropdown-item(href="#") Menu item 4
.row
.col-6.p-4
h1 Heading 1
h2 Heading 2
h3 Heading 3
h4 Heading 4
.col-6.p-4
p Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vehicula, purus sit amet sodales pharetra, ipsum ipsum mollis orci, id pharetra velit diam et dui. Sed placerat ipsum eget pharetra rutrum. Ut vitae rutrum lacus, eu imperdiet velit. Pellentesque eu velit cursus, scelerisque dui quis, dapibus magna. Vestibulum molestie sed sapien et ultricies. Nam porta ipsum leo, non congue magna vestibulum a. Etiam dictum felis sit amet augue varius tincidunt. Sed eget urna auctor, convallis felis in, pretium justo. Curabitur aliquet, ligula id tincidunt ullamcorper, orci lorem pharetra neque, in ornare arcu magna accumsan arcu. Maecenas dignissim lorem sed eros accumsan scelerisque.
p.small-text Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vehicula, purus sit amet sodales pharetra, ipsum ipsum mollis orci, id pharetra velit diam et dui.
.row
.col(v-for="taskType in tasksTypes")
h3 {{taskType}}s
ul
task(v-for="task in tasks", v-if="task.type === taskType", :key="task.id", :task="task")
</template>
<script>
import Task from './task';
import { mapState } from 'client/libs/store';
import bDropdown from 'bootstrap-vue/lib/components/dropdown';
import bDropdownItem from 'bootstrap-vue/lib/components/dropdown-item';
import toggleSwitch from 'client/components/ui/toggleSwitch';
export default {
components: {
Task,
bDropdown,
bDropdownItem,
toggleSwitch,
},
data () {
return {
tasksTypes: ['habit', 'daily', 'todo', 'reward'],
};
},
computed: {
...mapState({tasks: 'tasks.data'}),
},
};
</script>

View File

@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Habitica</title>
<!-- TODO load google fonts separately as @import is slow, find alternative -->
<link href="https://fonts.googleapis.com/css?family=Roboto+Condensed:700|Roboto:400,400i,700,700i" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Roboto+Condensed:400,400i,700,700i|Roboto:400,400i,700,700i" rel="stylesheet">
</head>
<body>
<!-- #loading-screen needs to be rendered before vue, will be deleted once app is loaded -->

View File

@@ -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}".`));

View File

@@ -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');
}

View File

@@ -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');

View File

@@ -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;
}

View File

@@ -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;
}
};
}

View File

@@ -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",