Tasks v2 Part 2 (#9236)
* start updating colors for tasks controls * finish updating controls colors * change hoevr behavior * change transition duration * update color with transition * refactor menu wip * wip * upgrade vue deps * fix warnings * fix menu * misc fixes * more fixes * fix badge * fix margins in menu * wip tooltips * tooltips * fix checklist colors * add badges * fix quick add input * add transition to task controls too * add batch add * fix task filtering * finish tasks badges * fix menu * upgrade deps * fix shop items using all the same image * fix animation * disable client tests until we remove phantomjs * revert changes to tasks colors * fix opacity in task modal inputs * remove client unit tests from travis * wip task dropdown * fix z-index for task footer/header * fixes and add files * fixes * bigger clickable area * more space to open task dropdown * droddown position * fix menu position * make sure other dropdowns get closed correctly * fixes * start to fix z-index * draggable = false for task dropdown * fix for dropdown position * implement move to top / bottom * fix push to bottom * typo * fix drag and drop * use standard code * wider click area for dropdown * unify badge look * fix padding * misc fixes * more fixes * make dropdown scrollable * misc fixes * fix padding for notes * use existing code instead of new props
@@ -34,5 +34,4 @@ env:
|
||||
- TEST="test:sanity"
|
||||
- TEST="test:content" COVERAGE=true
|
||||
- TEST="test:common" COVERAGE=true
|
||||
- TEST="client:unit" COVERAGE=true
|
||||
- TEST="apidoc"
|
||||
|
||||
1909
package-lock.json
generated
10
package.json
@@ -116,12 +116,12 @@
|
||||
"validator": "^4.9.0",
|
||||
"vinyl-buffer": "^1.0.0",
|
||||
"vinyl-source-stream": "^1.1.0",
|
||||
"vue": "^2.1.0",
|
||||
"vue-loader": "^11.0.0",
|
||||
"vue": "git://github.com/habitrpg/vue#e8f45fcfc98ed1859669f3a6a8b23ae28bf4d46f",
|
||||
"vue-loader": "^13.3.0",
|
||||
"vue-mugen-scroll": "^0.2.1",
|
||||
"vue-router": "^2.0.0-rc.5",
|
||||
"vue-router": "^3.0.0",
|
||||
"vue-style-loader": "^3.0.0",
|
||||
"vue-template-compiler": "^2.1.10",
|
||||
"vue-template-compiler": "^2.5.2",
|
||||
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker#45e607a7bccf4e3e089761b3b7b33e3f2c5dc21f",
|
||||
"webpack": "^2.2.1",
|
||||
"webpack-merge": "^4.0.0",
|
||||
@@ -136,7 +136,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint --ext .js,.vue .",
|
||||
"test": "npm run lint && gulp test && npm run client:unit && gulp apidoc",
|
||||
"test": "npm run lint && gulp test && gulp apidoc",
|
||||
"test:build": "gulp test:prepare:build",
|
||||
"test:api-v3": "gulp test:api-v3",
|
||||
"test:api-v3:unit": "gulp test:api-v3:unit",
|
||||
|
||||
@@ -71,8 +71,8 @@
|
||||
import axios from 'axios';
|
||||
import { loadProgressBar } from 'axios-progress-bar';
|
||||
|
||||
import AppMenu from './components/appMenu';
|
||||
import AppHeader from './components/appHeader';
|
||||
import AppMenu from './components/header/menu';
|
||||
import AppHeader from './components/header/index';
|
||||
import AppFooter from './components/appFooter';
|
||||
import notificationsDisplay from './components/notifications';
|
||||
import snackbars from './components/snackbars/notifications';
|
||||
|
||||
@@ -20,3 +20,11 @@
|
||||
position: absolute;
|
||||
top: -9px;
|
||||
}
|
||||
|
||||
.badge-purple {
|
||||
position: absolute;
|
||||
color: $white;
|
||||
background: $purple-400;
|
||||
line-height: 1.2;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@
|
||||
&-daily-todo-content-disabled {
|
||||
background: $gray-600;
|
||||
|
||||
* {
|
||||
.task-title, .task-notes {
|
||||
color: $gray-300 !important;
|
||||
}
|
||||
}
|
||||
|
||||
5
website/client/assets/svg/bottom.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="11" viewBox="0 0 10 11">
|
||||
<g fill="none" fill-rule="evenodd" stroke="#686274" stroke-width="2">
|
||||
<path d="M5,0v8 M1,5l4,4l4-4"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 213 B |
@@ -1,3 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="16" viewBox="0 0 14 16">
|
||||
<path fill="#A5A1AC" fill-rule="evenodd" d="M3 14h8V4H3v10zM14 4h-1v10a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V4H0V2h4V1a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v1h4v2zm-6 8h1V6H8v6zm-3 0h1V6H5v6z"/>
|
||||
<path fill-rule="evenodd" d="M3 14h8V4H3v10zM14 4h-1v10a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V4H0V2h4V1a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v1h4v2zm-6 8h1V6H8v6zm-3 0h1V6H5v6z"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 274 B After Width: | Height: | Size: 259 B |
3
website/client/assets/svg/edit.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M2.723 11.859l1.418 1.419-2.219.788.8-2.207zm2.762.686l-2.03-2.03 7.386-7.385 2.03 2.03-7.386 7.385zm8.704-10.731c.56.56.56 1.468 0 2.03l-.285.284-2.03-2.03.286-.284a1.438 1.438 0 0 1 2.027 0h.002zM11.125.782l-.8.8-8.417 8.415a.731.731 0 0 0-.098.122s-.012.024-.02.036a.713.713 0 0 0-.048.1v.012L.044 15.022a.73.73 0 0 0 .934.935l4.755-1.704a.728.728 0 0 0 .102-.05l.034-.018a.731.731 0 0 0 .122-.097l9.227-9.213A2.896 2.896 0 0 0 11.125.782z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 570 B |
3
website/client/assets/svg/menu.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="4" height="16" viewBox="0 0 4 16">
|
||||
<path fill-rule="evenodd" d="M2 4a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 225 B |
5
website/client/assets/svg/top.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="11" viewBox="0 0 10 11">
|
||||
<g fill="none" fill-rule="evenodd" stroke="#686274" stroke-width="2">
|
||||
<path d="M5 3v8M9 6L5 2 1 6"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 212 B |
@@ -1,3 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="24" viewBox="0 0 22 24">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<path fill-rule="evenodd" d="M15 13h-.4c1.9-1.2 3.3-3.3 3.4-5.8.1-3.8-3.1-7.2-6.9-7.2C7.1 0 4 3.1 4 7c0 2.6 1.3 4.8 3.4 6H7c-3.9 0-7 3.1-7 7v1c0 1.7 1.3 3 3 3h16c1.7 0 3-1.3 3-3v-1c0-3.9-3.1-7-7-7zM6 7c0-2.8 2.2-5 5-5s5 2.2 5 5-2.2 5-5 5-5-2.2-5-5zm13 15H3c-.6 0-1-.4-1-1v-1c0-2.8 2.2-5 5-5h8c2.8 0 5 2.2 5 5v1c0 .6-.4 1-1 1z"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 424 B After Width: | Height: | Size: 424 B |
@@ -102,9 +102,9 @@ div
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'client/libs/store';
|
||||
import MemberDetails from './memberDetails';
|
||||
import createPartyModal from './groups/createPartyModal';
|
||||
import membersModal from './groups/membersModal';
|
||||
import MemberDetails from '../memberDetails';
|
||||
import createPartyModal from '../groups/createPartyModal';
|
||||
import membersModal from '../groups/membersModal';
|
||||
import ResizeDirective from 'client/directives/resize.directive';
|
||||
|
||||
export default {
|
||||
@@ -53,42 +53,20 @@ div
|
||||
a.dropdown-item(href="https://trello.com/c/odmhIqyW/440-read-first-table-of-contents", target='_blank') {{ $t('requestAF') }}
|
||||
a.dropdown-item(href="http://habitica.wikia.com/wiki/Contributing_to_Habitica", target='_blank') {{ $t('contributing') }}
|
||||
a.dropdown-item(href="http://habitica.wikia.com/wiki/Habitica_Wiki", target='_blank') {{ $t('wiki') }}
|
||||
.item-with-icon(v-if="userHourglasses > 0")
|
||||
.svg-icon(v-html="icons.hourglasses")
|
||||
span {{ userHourglasses }}
|
||||
.item-with-icon
|
||||
.svg-icon.gem(v-html="icons.gem", @click='showBuyGemsModal("gems")')
|
||||
span {{userGems | roundBigNumber}}
|
||||
.item-with-icon
|
||||
.svg-icon(v-html="icons.gold")
|
||||
span {{Math.floor(user.stats.gp * 100) / 100}}
|
||||
a.item-with-icon(@click="sync")
|
||||
.svg-icon(v-html="icons.sync")
|
||||
notification-menu
|
||||
a.dropdown.item-with-icon.item-user
|
||||
span.message-count.top-count(v-if='user.inbox.newMessages > 0') {{user.inbox.newMessages}}
|
||||
.svg-icon.user(v-html="icons.user")
|
||||
.dropdown-menu.dropdown-menu-right.user-dropdown
|
||||
a.dropdown-item.edit-avatar.dropdown-separated(@click='showAvatar()')
|
||||
h3 {{ user.profile.name }}
|
||||
span.small-text {{ $t('editAvatar') }}
|
||||
a.nav-link.dropdown-item.dropdown-separated(@click.prevent='showInbox()')
|
||||
| {{ $t('messages') }}
|
||||
span.message-count(v-if='user.inbox.newMessages > 0') {{user.inbox.newMessages}}
|
||||
a.dropdown-item(@click='showAvatar("backgrounds", "2017")') {{ $t('backgrounds') }}
|
||||
a.dropdown-item(@click='showProfile("stats")') {{ $t('stats') }}
|
||||
a.dropdown-item(@click='showProfile("achievements")') {{ $t('achievements') }}
|
||||
a.dropdown-item.dropdown-separated(@click='showProfile("profile")') {{ $t('profile') }}
|
||||
router-link.dropdown-item(:to="{name: 'site'}") {{ $t('settings') }}
|
||||
router-link.dropdown-item.dropdown-separated(:to="{name: 'subscription'}") {{ $t('subscription') }}
|
||||
a.nav-link.dropdown-item.dropdown-separated(to="/", @click.prevent='logout()') {{ $t('logout') }}
|
||||
li(v-if='!this.user.purchased.plan.customerId', @click='showBuyGemsModal("subscribe")')
|
||||
.dropdown-item.text-center
|
||||
h3.purple {{ $t('needMoreGems') }}
|
||||
span.small-text {{ $t('needMoreGemsInfo') }}
|
||||
img.float-left.align-self-end(src='~assets/images/gem-rain.png')
|
||||
button.btn.btn-primary.btn-lg.learn-button Learn More
|
||||
img.float-right.align-self-end(src='~assets/images/gold-rain.png')
|
||||
.d-flex.align-items-center
|
||||
.item-with-icon(v-if="userHourglasses > 0")
|
||||
.svg-icon(v-html="icons.hourglasses", v-b-tooltip.hover.bottom="$t('mysticHourglassesTooltip')")
|
||||
span {{ userHourglasses }}
|
||||
.item-with-icon
|
||||
.svg-icon.gem(v-html="icons.gem", @click='showBuyGemsModal("gems")', v-b-tooltip.hover.bottom="$t('gems')")
|
||||
span {{userGems | roundBigNumber}}
|
||||
.item-with-icon.gold
|
||||
.svg-icon(v-html="icons.gold", v-b-tooltip.hover.bottom="$t('gold')")
|
||||
span {{Math.floor(user.stats.gp * 100) / 100}}
|
||||
a.item-with-icon(@click="sync", v-b-tooltip.hover.bottom="$t('sync')")
|
||||
.svg-icon(v-html="icons.sync")
|
||||
notification-menu.item-with-icon
|
||||
user-dropdown.item-with-icon
|
||||
b-nav-toggle(target='nav_collapse')
|
||||
</template>
|
||||
|
||||
@@ -167,38 +145,11 @@ div
|
||||
|
||||
// Make the dropdown menu open on hover
|
||||
.dropdown:hover .dropdown-menu {
|
||||
display: block;
|
||||
margin-top: 0; // remove the gap so it doesn't close
|
||||
display: block;
|
||||
margin-top: 0; // remove the gap so it doesn't close
|
||||
}
|
||||
|
||||
.dropdown + .dropdown {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.dropdown-separated {
|
||||
border-bottom: 1px solid $gray-500;
|
||||
}
|
||||
|
||||
.user-dropdown {
|
||||
width: 14.75em;
|
||||
}
|
||||
|
||||
.learn-button {
|
||||
margin: 0.75em 0.75em 0.75em 1em;
|
||||
}
|
||||
|
||||
.purple {
|
||||
color: $purple-200;
|
||||
}
|
||||
|
||||
.small-text {
|
||||
color: $gray-200;
|
||||
font-style: normal;
|
||||
display: block;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.dropdown-menu:not(.user-dropdown) {
|
||||
.dropdown-menu {
|
||||
background: $purple-200;
|
||||
border-radius: 0px;
|
||||
border: none;
|
||||
@@ -230,81 +181,48 @@ div
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown + .dropdown {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.item-with-icon {
|
||||
color: $white;
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
padding-top: 16px;
|
||||
padding-left: 16px;
|
||||
white-space: nowrap;
|
||||
|
||||
span {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&:hover .svg-icon {
|
||||
&.gold {
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
&:hover /deep/ .svg-icon {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
& /deep/ .svg-icon {
|
||||
color: $header-color;
|
||||
vertical-align: bottom;
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 8px;
|
||||
margin-left: 8px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 12px;
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.item-notifications, .item-user {
|
||||
padding-right: 12.5px;
|
||||
padding-left: 12.5px;
|
||||
color: $header-color;
|
||||
transition: none;
|
||||
|
||||
.svg-icon {
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.item-user .edit-avatar {
|
||||
h3 {
|
||||
color: $gray-10;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
.menu-icon {
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
.gem:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.message-count {
|
||||
background-color: $blue-50;
|
||||
border-radius: 50%;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
float: right;
|
||||
color: $white;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.message-count.top-count {
|
||||
background-color: $red-50;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: .5em;
|
||||
padding: .2em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import bNavToggle from 'bootstrap-vue/lib/components/nav-toggle';
|
||||
import bCollapse from 'bootstrap-vue/lib/components/collapse';
|
||||
|
||||
@@ -313,17 +231,18 @@ import * as Analytics from 'client/libs/analytics';
|
||||
import gemIcon from 'assets/svg/gem.svg';
|
||||
import goldIcon from 'assets/svg/gold.svg';
|
||||
import syncIcon from 'assets/svg/sync.svg';
|
||||
import userIcon from 'assets/svg/user.svg';
|
||||
import svgHourglasses from 'assets/svg/hourglass.svg';
|
||||
import logo from 'assets/svg/logo.svg';
|
||||
import InboxModal from './userMenu/inbox.vue';
|
||||
import notificationMenu from './notificationMenu';
|
||||
import creatorIntro from './creatorIntro';
|
||||
import profile from './userMenu/profile';
|
||||
import markPMSRead from 'common/script/ops/markPMSRead';
|
||||
import InboxModal from '../userMenu/inbox.vue';
|
||||
import notificationMenu from './notificationsDropdown';
|
||||
import creatorIntro from '../creatorIntro';
|
||||
import profile from '../userMenu/profile';
|
||||
import userDropdown from './userDropdown';
|
||||
import bTooltip from 'bootstrap-vue/lib/directives/tooltip';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
userDropdown,
|
||||
InboxModal,
|
||||
notificationMenu,
|
||||
creatorIntro,
|
||||
@@ -331,12 +250,15 @@ export default {
|
||||
bNavToggle,
|
||||
bCollapse,
|
||||
},
|
||||
directives: {
|
||||
bTooltip,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isUserDropdownOpen: false,
|
||||
icons: Object.freeze({
|
||||
gem: gemIcon,
|
||||
gold: goldIcon,
|
||||
user: userIcon,
|
||||
hourglasses: svgHourglasses,
|
||||
sync: syncIcon,
|
||||
logo,
|
||||
@@ -357,31 +279,15 @@ export default {
|
||||
this.getUserGroupPlans();
|
||||
},
|
||||
methods: {
|
||||
toggleUserDropdown () {
|
||||
this.isUserDropdownOpen = !this.isUserDropdownOpen;
|
||||
},
|
||||
sync () {
|
||||
return Promise.all([
|
||||
this.$store.dispatch('user:fetch', {forceLoad: true}),
|
||||
this.$store.dispatch('tasks:fetchUserTasks', {forceLoad: true}),
|
||||
]);
|
||||
},
|
||||
logout () {
|
||||
this.$store.dispatch('auth:logout');
|
||||
},
|
||||
showInbox () {
|
||||
markPMSRead(this.user);
|
||||
axios.post('/api/v3/user/mark-pms-read');
|
||||
this.$root.$emit('show::modal', 'inbox-modal');
|
||||
},
|
||||
showAvatar (startingPage, subpage) {
|
||||
this.$store.state.avatarEditorOptions.editingUser = true;
|
||||
this.$store.state.avatarEditorOptions.startingPage = startingPage;
|
||||
this.$store.state.avatarEditorOptions.subpage = subpage;
|
||||
this.$root.$emit('show::modal', 'avatar-modal');
|
||||
},
|
||||
showProfile (startingPage) {
|
||||
this.$store.state.profileUser = this.user;
|
||||
this.$store.state.profileOptions.startingPage = startingPage;
|
||||
this.$root.$emit('show::modal', 'profile');
|
||||
},
|
||||
async getUserGroupPlans () {
|
||||
this.$store.state.groupPlans = await this.$store.dispatch('guilds:getGroupPlans');
|
||||
},
|
||||
27
website/client/components/header/messageCount.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template lang="pug" functional>
|
||||
span.message-count(:class="{'top-count': props.top === true}") {{props.count}}
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
.message-count {
|
||||
background-color: $blue-50;
|
||||
border-radius: 50%;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
float: right;
|
||||
color: $white;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.message-count.top-count {
|
||||
position: absolute;
|
||||
right: 0.3em;
|
||||
top: -0.8em;
|
||||
padding: 0.2em;
|
||||
background-color: $red-50;
|
||||
}
|
||||
</style>
|
||||
@@ -1,15 +1,17 @@
|
||||
<template lang="pug">
|
||||
div.item-with-icon.item-notifications.dropdown
|
||||
span.message-count.top-count(v-if='notificationsCount > 0') {{ notificationsCount }}
|
||||
.svg-icon.notifications(v-html="icons.notifications")
|
||||
.dropdown-menu.dropdown-menu-right.user-dropdown
|
||||
menu-dropdown.item-notifications(:right="true")
|
||||
div(slot="dropdown-toggle")
|
||||
div
|
||||
message-count(v-if='notificationsCount > 0', :count="notificationsCount", :top="true")
|
||||
.svg-icon.notifications(v-html="icons.notifications")
|
||||
div(slot="dropdown-content")
|
||||
h4.dropdown-item.dropdown-separated(v-if='!hasNoNotifications()') {{ $t('notifications') }}
|
||||
h4.dropdown-item.toolbar-notifs-no-messages(v-if='hasNoNotifications()') {{ $t('noNotifications') }}
|
||||
a.dropdown-item(v-if='user.party.quest && user.party.quest.RSVPNeeded')
|
||||
div {{ $t('invitedTo', {name: quests.quests[user.party.quest.key].text()}) }}
|
||||
div
|
||||
button.btn.btn-primary(@click='questAccept(user.party._id)') Accept
|
||||
button.btn.btn-primary(@click='questReject(user.party._id)') Reject
|
||||
button.btn.btn-primary(@click.stop='questAccept(user.party._id)') Accept
|
||||
button.btn.btn-primary(@click.stop='questReject(user.party._id)') Reject
|
||||
a.dropdown-item(v-if='user.purchased.plan.mysteryItems.length', @click='go("/inventory/items")')
|
||||
span.glyphicon.glyphicon-gift
|
||||
span {{ $t('newSubscriberItem') }}
|
||||
@@ -18,20 +20,19 @@ div.item-with-icon.item-notifications.dropdown
|
||||
span.glyphicon.glyphicon-user
|
||||
span {{ $t('invitedTo', {name: party.name}) }}
|
||||
div
|
||||
button.btn.btn-primary(@click='accept(party, index, "party")') Accept
|
||||
button.btn.btn-primary(@click='reject(party, index, "party")') Reject
|
||||
button.btn.btn-primary(@click.stop='accept(party, index, "party")') Accept
|
||||
button.btn.btn-primary(@click.stop='reject(party, index, "party")') Reject
|
||||
a.dropdown-item(v-if='user.flags.cardReceived', @click='go("/inventory/items")')
|
||||
span.glyphicon.glyphicon-envelope
|
||||
span {{ $t('cardReceived') }}
|
||||
a.dropdown-item(@click='clearCards()', :popover="$t('clear')",
|
||||
popover-placement='right', popover-trigger='mouseenter', popover-append-to-body='true')
|
||||
a.dropdown-item(@click.stop='clearCards()')
|
||||
a.dropdown-item(v-for='(guild, index) in user.invitations.guilds')
|
||||
div
|
||||
span.glyphicon.glyphicon-user
|
||||
span {{ $t('invitedTo', {name: guild.name}) }}
|
||||
div
|
||||
button.btn.btn-primary(@click='accept(guild, index, "guild")') Accept
|
||||
button.btn.btn-primary(@click='reject(guild, index, "guild")') Reject
|
||||
button.btn.btn-primary(@click.stop='accept(guild, index, "guild")') Accept
|
||||
button.btn.btn-primary(@click.stop='reject(guild, index, "guild")') Reject
|
||||
a.dropdown-item(v-if='user.flags.classSelected && !user.preferences.disableClasses && user.stats.points',
|
||||
@click='go("/user/profile")')
|
||||
span.glyphicon.glyphicon-plus-sign
|
||||
@@ -40,130 +41,40 @@ div.item-with-icon.item-notifications.dropdown
|
||||
span(@click='navigateToGroup(message.key)')
|
||||
span.glyphicon.glyphicon-comment
|
||||
span {{message.name}}
|
||||
span.clear-button(@click='clearMessages(message.key)', :popover="$t('clear')",
|
||||
popover-placement='right', popover-trigger='mouseenter', popover-append-to-body='true') Clear
|
||||
span.clear-button(@click.stop='clearMessages(message.key)') Clear
|
||||
a.dropdown-item(v-for='notification in groupNotifications', :key='notification.id')
|
||||
span(:class="groupApprovalNotificationIcon(notification)")
|
||||
span {{notification.data.message}}
|
||||
span.clear-button(@click='viewGroupApprovalNotification(notification)', :popover="$t('clear')",
|
||||
popover-placement='right', popover-trigger='mouseenter', popover-append-to-body='true') Clear
|
||||
span.clear-button(@click.stop='viewGroupApprovalNotification(notification)') Clear
|
||||
</template>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
.message-count {
|
||||
background-color: $blue-50;
|
||||
border-radius: 50%;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
float: right;
|
||||
color: $white;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.message-count.top-count {
|
||||
position: absolute;
|
||||
right: -.5em;
|
||||
top: .5em;
|
||||
padding: .2em;
|
||||
background-color: $red-50;
|
||||
}
|
||||
|
||||
.clear-button {
|
||||
margin-left: .5em;
|
||||
}
|
||||
|
||||
.item-notifications {
|
||||
width: 44px;
|
||||
}
|
||||
|
||||
.item-notifications:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.notifications {
|
||||
color: $header-color;
|
||||
vertical-align: bottom;
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 8px;
|
||||
margin-left: 8px;
|
||||
margin-top: .2em;
|
||||
}
|
||||
|
||||
.item-with-icon:hover {
|
||||
.svg-icon {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.user-dropdown {
|
||||
max-height: 350px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* @TODO: Move to shared css */
|
||||
.dropdown:hover .dropdown-menu {
|
||||
display: block;
|
||||
margin-top: 0; // remove the gap so it doesn't close
|
||||
}
|
||||
|
||||
.dropdown + .dropdown {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.dropdown-separated {
|
||||
border-bottom: 1px solid $gray-500;
|
||||
}
|
||||
|
||||
.dropdown-menu:not(.user-dropdown) {
|
||||
background: $purple-200;
|
||||
border-radius: 0px;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
padding: 0px;
|
||||
|
||||
border-bottom-right-radius: 5px;
|
||||
border-bottom-left-radius: 5px;
|
||||
|
||||
.dropdown-item {
|
||||
font-size: 16px;
|
||||
box-shadow: none;
|
||||
color: $white;
|
||||
border: none;
|
||||
line-height: 1.5;
|
||||
|
||||
&.active {
|
||||
background: $purple-300;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $purple-300;
|
||||
|
||||
&:last-child {
|
||||
border-bottom-right-radius: 5px;
|
||||
border-bottom-left-radius: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.clear-button {
|
||||
margin-left: .5em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import map from 'lodash/map';
|
||||
// import bTooltip from 'bootstrap-vue/lib/directives/tooltip';
|
||||
|
||||
import { mapState } from 'client/libs/store';
|
||||
import * as Analytics from 'client/libs/analytics';
|
||||
import quests from 'common/script/content/quests';
|
||||
import notificationsIcon from 'assets/svg/notifications.svg';
|
||||
import MenuDropdown from '../ui/customMenuDropdown';
|
||||
import MessageCount from './messageCount';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MenuDropdown,
|
||||
MessageCount,
|
||||
},
|
||||
directives: {
|
||||
// bTooltip,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
126
website/client/components/header/userDropdown.vue
Normal file
@@ -0,0 +1,126 @@
|
||||
<template lang="pug">
|
||||
menu-dropdown.item-user(:right="true")
|
||||
div(slot="dropdown-toggle")
|
||||
div(v-b-tooltip.hover.bottom="$t('user')")
|
||||
message-count(v-if='user.inbox.newMessages > 0', :count="user.inbox.newMessages", :top="true")
|
||||
.svg-icon.user(v-html="icons.user")
|
||||
.user-dropdown(slot="dropdown-content")
|
||||
a.dropdown-item.edit-avatar.dropdown-separated(@click='showAvatar()')
|
||||
h3 {{ user.profile.name }}
|
||||
span.small-text {{ $t('editAvatar') }}
|
||||
a.nav-link.dropdown-item.dropdown-separated(@click.prevent='showInbox()')
|
||||
| {{ $t('messages') }}
|
||||
message-count(v-if='user.inbox.newMessages > 0', :count="user.inbox.newMessages")
|
||||
a.dropdown-item(@click='showAvatar("backgrounds", "2017")') {{ $t('backgrounds') }}
|
||||
a.dropdown-item(@click='showProfile("stats")') {{ $t('stats') }}
|
||||
a.dropdown-item(@click='showProfile("achievements")') {{ $t('achievements') }}
|
||||
a.dropdown-item.dropdown-separated(@click='showProfile("profile")') {{ $t('profile') }}
|
||||
router-link.dropdown-item(:to="{name: 'site'}") {{ $t('settings') }}
|
||||
router-link.dropdown-item.dropdown-separated(:to="{name: 'subscription'}") {{ $t('subscription') }}
|
||||
a.nav-link.dropdown-item.dropdown-separated(@click.prevent='logout()') {{ $t('logout') }}
|
||||
li(v-if='!this.user.purchased.plan.customerId', @click='showBuyGemsModal("subscribe")')
|
||||
.dropdown-item.text-center
|
||||
h3.purple {{ $t('needMoreGems') }}
|
||||
span.small-text {{ $t('needMoreGemsInfo') }}
|
||||
img.float-left.align-self-end(src='~assets/images/gem-rain.png')
|
||||
button.btn.btn-primary.btn-lg.learn-button Learn More
|
||||
img.float-right.align-self-end(src='~assets/images/gold-rain.png')
|
||||
</template>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
.edit-avatar {
|
||||
h3 {
|
||||
color: $gray-10;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.user-dropdown {
|
||||
width: 14.75em;
|
||||
}
|
||||
|
||||
.learn-button {
|
||||
margin: 0.75em 0.75em 0.75em 1em;
|
||||
}
|
||||
|
||||
.purple {
|
||||
color: $purple-200;
|
||||
}
|
||||
|
||||
.small-text {
|
||||
color: $gray-200;
|
||||
font-style: normal;
|
||||
display: block;
|
||||
white-space: normal;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'client/libs/store';
|
||||
import * as Analytics from 'client/libs/analytics';
|
||||
import userIcon from 'assets/svg/user.svg';
|
||||
import MenuDropdown from '../ui/customMenuDropdown';
|
||||
import axios from 'axios';
|
||||
import markPMSRead from 'common/script/ops/markPMSRead';
|
||||
import MessageCount from './messageCount';
|
||||
import bTooltip from 'bootstrap-vue/lib/directives/tooltip';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MenuDropdown,
|
||||
MessageCount,
|
||||
},
|
||||
directives: {
|
||||
bTooltip,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
user: userIcon,
|
||||
}),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
},
|
||||
methods: {
|
||||
showAvatar (startingPage, subpage) {
|
||||
this.$store.state.avatarEditorOptions.editingUser = true;
|
||||
this.$store.state.avatarEditorOptions.startingPage = startingPage;
|
||||
this.$store.state.avatarEditorOptions.subpage = subpage;
|
||||
this.$root.$emit('show::modal', 'avatar-modal');
|
||||
},
|
||||
showInbox () {
|
||||
markPMSRead(this.user);
|
||||
axios.post('/api/v3/user/mark-pms-read');
|
||||
this.$root.$emit('show::modal', 'inbox-modal');
|
||||
},
|
||||
showProfile (startingPage) {
|
||||
this.$store.state.profileUser = this.user;
|
||||
this.$store.state.profileOptions.startingPage = startingPage;
|
||||
this.$root.$emit('show::modal', 'profile');
|
||||
},
|
||||
showBuyGemsModal (startingPage) {
|
||||
this.$store.state.gemModalOptions.startingPage = startingPage;
|
||||
|
||||
Analytics.track({
|
||||
hitType: 'event',
|
||||
eventCategory: 'button',
|
||||
eventAction: 'click',
|
||||
eventLabel: 'Gems > User Dropdown',
|
||||
});
|
||||
|
||||
this.$root.$emit('show::modal', 'buy-gems', {alreadyTracked: true});
|
||||
},
|
||||
logout () {
|
||||
this.$store.dispatch('auth:logout');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -79,9 +79,9 @@
|
||||
:showPopover="flatGear[activeItems[group]] && Boolean(flatGear[activeItems[group]].text)",
|
||||
@click="equipItem(flatGear[activeItems[group]])",
|
||||
)
|
||||
template(slot="popoverContent", scope="context")
|
||||
template(slot="popoverContent", slot-scope="context")
|
||||
equipmentAttributesPopover(:item="context.item")
|
||||
template(slot="itemBadge", scope="context")
|
||||
template(slot="itemBadge", slot-scope="context")
|
||||
starBadge(
|
||||
:selected="true",
|
||||
:show="!costume || user.preferences.costume",
|
||||
@@ -105,7 +105,7 @@
|
||||
:type="group.key",
|
||||
:noItemsLabel="$t('noGearItemsOfType', { type: group.label })"
|
||||
)
|
||||
template(slot="item", scope="context")
|
||||
template(slot="item", slot-scope="context")
|
||||
item(
|
||||
:item="context.item",
|
||||
:itemContentClass="'shop_' + context.item.key",
|
||||
@@ -113,13 +113,13 @@
|
||||
:key="context.item.key",
|
||||
@click="openEquipDialog(context.item)"
|
||||
)
|
||||
template(slot="itemBadge", scope="context")
|
||||
template(slot="itemBadge", slot-scope="context")
|
||||
starBadge(
|
||||
:selected="activeItems[context.item.type] === context.item.key",
|
||||
:show="!costume || user.preferences.costume",
|
||||
@click="equipItem(context.item)",
|
||||
)
|
||||
template(slot="popoverContent", scope="context")
|
||||
template(slot="popoverContent", slot-scope="context")
|
||||
equipmentAttributesPopover(:item="context.item")
|
||||
|
||||
equipGearModal(
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
:type="group.key",
|
||||
:noItemsLabel="$t('noGearItemsOfType', { type: $t(group.key) })"
|
||||
)
|
||||
template(slot="item", scope="context")
|
||||
template(slot="item", slot-scope="context")
|
||||
item(
|
||||
:item="context.item",
|
||||
:key="context.item.key",
|
||||
@@ -57,10 +57,10 @@
|
||||
|
||||
@click="onEggClicked($event, context.item)",
|
||||
)
|
||||
template(slot="popoverContent", scope="context")
|
||||
template(slot="popoverContent", slot-scope="context")
|
||||
h4.popover-content-title {{ context.item.text }}
|
||||
.popover-content-text(v-if="currentDraggingPotion == null") {{ context.item.notes }}
|
||||
template(slot="itemBadge", scope="context")
|
||||
template(slot="itemBadge", slot-scope="context")
|
||||
countBadge(
|
||||
:show="true",
|
||||
:count="context.item.quantity"
|
||||
@@ -74,7 +74,7 @@
|
||||
:type="group.key",
|
||||
:noItemsLabel="$t('noGearItemsOfType', { type: $t(group.key) })"
|
||||
)
|
||||
template(slot="item", scope="context")
|
||||
template(slot="item", slot-scope="context")
|
||||
item(
|
||||
:item="context.item",
|
||||
:key="context.item.key",
|
||||
@@ -88,10 +88,10 @@
|
||||
|
||||
@click="onPotionClicked($event, context.item)"
|
||||
)
|
||||
template(slot="popoverContent", scope="context")
|
||||
template(slot="popoverContent", slot-scope="context")
|
||||
h4.popover-content-title {{ context.item.text }}
|
||||
.popover-content-text {{ context.item.notes }}
|
||||
template(slot="itemBadge", scope="context")
|
||||
template(slot="itemBadge", slot-scope="context")
|
||||
countBadge(
|
||||
:show="true",
|
||||
:count="context.item.quantity"
|
||||
@@ -105,7 +105,7 @@
|
||||
:type="group.key",
|
||||
:noItemsLabel="$t('noGearItemsOfType', { type: $t(group.key) })"
|
||||
)
|
||||
template(slot="item", scope="context")
|
||||
template(slot="item", slot-scope="context")
|
||||
item(
|
||||
:item="context.item",
|
||||
:key="context.item.key",
|
||||
@@ -113,7 +113,7 @@
|
||||
:showPopover="currentDraggingPotion == null",
|
||||
@click="itemClicked(group.key, context.item)",
|
||||
)
|
||||
template(slot="popoverContent", scope="context")
|
||||
template(slot="popoverContent", slot-scope="context")
|
||||
div.questPopover(v-if="group.key === 'quests'")
|
||||
h4.popover-content-title {{ context.item.text }}
|
||||
questInfo(:quest="context.item")
|
||||
@@ -121,7 +121,7 @@
|
||||
div(v-else)
|
||||
h4.popover-content-title {{ context.item.text }}
|
||||
.popover-content-text(v-html="context.item.notes")
|
||||
template(slot="itemBadge", scope="context")
|
||||
template(slot="itemBadge", slot-scope="context")
|
||||
countBadge(
|
||||
:show="true",
|
||||
:count="context.item.quantity"
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
div(:class="'Pet_Egg_'+item.eggKey")
|
||||
div(v-else)
|
||||
h4.popover-content-title {{ item.name }}
|
||||
template(slot="itemBadge", scope="context")
|
||||
template(slot="itemBadge", slot-scope="context")
|
||||
starBadge(:selected="item.key === currentPet", :show="item.isOwned()", @click="selectPet(item)")
|
||||
|
||||
.btn.btn-flat.btn-show-more(@click="setShowMore(petGroup.key)", v-if='petGroup.key !== "specialPets"')
|
||||
@@ -144,7 +144,7 @@
|
||||
)
|
||||
span(slot="popoverContent")
|
||||
h4.popover-content-title {{ item.name }}
|
||||
template(slot="itemBadge", scope="context")
|
||||
template(slot="itemBadge", slot-scope="context")
|
||||
starBadge(
|
||||
:selected="item.key === currentMount",
|
||||
:show="item.isOwned()",
|
||||
@@ -187,7 +187,7 @@
|
||||
:itemWidth=94,
|
||||
:itemMargin=24,
|
||||
)
|
||||
template(slot="item", scope="context")
|
||||
template(slot="item", slot-scope="context")
|
||||
foodItem(
|
||||
:item="context.item",
|
||||
:itemCount="userItems.food[context.item.key]",
|
||||
|
||||
@@ -20,17 +20,17 @@
|
||||
.is-buffed(v-if="isBuffed")
|
||||
.svg-icon(v-html="icons.buff")
|
||||
span.small-text.character-level {{ characterLevel }}
|
||||
.progress-container
|
||||
.progress-container(b-tooltip.hover.bottom="$t('health')")
|
||||
.svg-icon(v-html="icons.health")
|
||||
.progress
|
||||
.progress-bar.bg-health(:style="{width: `${percent(member.stats.hp, MAX_HEALTH)}%`}")
|
||||
span.small-text {{member.stats.hp | statFloor}} / {{MAX_HEALTH}}
|
||||
.progress-container
|
||||
.progress-container(b-tooltip.hover.bottom="$t('experience')")
|
||||
.svg-icon(v-html="icons.experience")
|
||||
.progress
|
||||
.progress-bar.bg-experience(:style="{width: `${percent(member.stats.exp, toNextLevel)}%`}")
|
||||
span.small-text {{member.stats.exp | statFloor}} / {{toNextLevel}}
|
||||
.progress-container(v-if="hasClass")
|
||||
.progress-container(v-if="hasClass", b-tooltip.hover.bottom="$t('mana')")
|
||||
.svg-icon(v-html="icons.mana")
|
||||
.progress
|
||||
.progress-bar.bg-mana(:style="{width: `${percent(member.stats.mp, maxMP)}%`}")
|
||||
@@ -186,6 +186,7 @@ import Profile from './userMenu/profile';
|
||||
import { toNextLevel } from '../../common/script/statHelpers';
|
||||
import statsComputed from '../../common/script/libs/statsComputed';
|
||||
import percent from '../../common/script/libs/percent';
|
||||
// import bTooltip from 'bootstrap-vue/lib/directives/tooltip';
|
||||
|
||||
import buffIcon from 'assets/svg/buff.svg';
|
||||
import healthIcon from 'assets/svg/health.svg';
|
||||
@@ -198,6 +199,9 @@ export default {
|
||||
Profile,
|
||||
ClassBadge,
|
||||
},
|
||||
directives: {
|
||||
// bTooltip,
|
||||
},
|
||||
props: {
|
||||
member: {
|
||||
type: Object,
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
:popoverPosition="'top'",
|
||||
@click="featuredItemSelected(item)"
|
||||
)
|
||||
template(slot="itemBadge", scope="ctx")
|
||||
template(slot="itemBadge", slot-scope="ctx")
|
||||
span.badge.badge-pill.badge-item.badge-svg(
|
||||
:class="{'item-selected-badge': ctx.item.pinned, 'hide': !ctx.item.pinned}",
|
||||
@click.prevent.stop="togglePinned(ctx.item)"
|
||||
@@ -101,7 +101,7 @@
|
||||
:type="'gear'",
|
||||
:noItemsLabel="$t('noGearItemsOfClass')"
|
||||
)
|
||||
template(slot="item", scope="ctx")
|
||||
template(slot="item", slot-scope="ctx")
|
||||
shopItem(
|
||||
:key="ctx.item.key",
|
||||
:item="ctx.item",
|
||||
@@ -110,7 +110,7 @@
|
||||
@click="gearSelected(ctx.item)"
|
||||
)
|
||||
|
||||
template(slot="itemBadge", scope="ctx")
|
||||
template(slot="itemBadge", slot-scope="ctx")
|
||||
span.badge.badge-pill.badge-item.badge-svg(
|
||||
:class="{'item-selected-badge': ctx.item.pinned, 'hide': !ctx.item.pinned}",
|
||||
@click.prevent.stop="togglePinned(ctx.item)"
|
||||
@@ -151,13 +151,13 @@
|
||||
strong(v-if='item.key === "gem" && gemsLeft === 0') {{ $t('maxBuyGems') }}
|
||||
h4.popover-content-title {{ item.text }}
|
||||
|
||||
template(slot="itemBadge", scope="ctx")
|
||||
template(slot="itemBadge", slot-scope="ctx")
|
||||
countBadge(
|
||||
v-if="item.showCount != false",
|
||||
:show="userItems[item.purchaseType][item.key] != 0",
|
||||
:count="userItems[item.purchaseType][item.key] || 0"
|
||||
)
|
||||
.gems-left(v-if='item.key === "gem"')
|
||||
.badge.badge-pill.badge-purple.gems-left(v-if='item.key === "gem"')
|
||||
| {{ gemsLeft }}
|
||||
|
||||
span.badge.badge-pill.badge-item.badge-svg(
|
||||
@@ -196,14 +196,14 @@
|
||||
:itemWidth=94,
|
||||
:itemMargin=24,
|
||||
)
|
||||
template(slot="item", scope="ctx")
|
||||
template(slot="item", slot-scope="ctx")
|
||||
item(
|
||||
:item="ctx.item",
|
||||
:itemContentClass="getItemClass(selectedDrawerItemType, ctx.item.key)",
|
||||
popoverPosition="top",
|
||||
@click="selectedItemToSell = ctx.item"
|
||||
)
|
||||
template(slot="itemBadge", scope="ctx")
|
||||
template(slot="itemBadge", slot-scope="ctx")
|
||||
countBadge(
|
||||
:show="true",
|
||||
:count="userItems[drawerTabs[selectedDrawerTab].contentType][ctx.item.key] || 0"
|
||||
@@ -218,13 +218,13 @@
|
||||
:text="selectedItemToSell != null ? getItemName(selectedDrawerItemType, selectedItemToSell) : ''",
|
||||
@change="resetItemToSell($event)"
|
||||
)
|
||||
template(slot="item", scope="ctx")
|
||||
template(slot="item", slot-scope="ctx")
|
||||
item.flat(
|
||||
:item="ctx.item",
|
||||
:itemContentClass="getItemClass(selectedDrawerItemType, ctx.item.key)",
|
||||
:showPopover="false"
|
||||
)
|
||||
template(slot="itemBadge", scope="ctx")
|
||||
template(slot="itemBadge", slot-scope="ctx")
|
||||
countBadge(
|
||||
:show="true",
|
||||
:count="userItems[drawerTabs[selectedDrawerTab].contentType][ctx.item.key] || 0"
|
||||
@@ -356,17 +356,8 @@
|
||||
}
|
||||
|
||||
.market .gems-left {
|
||||
position: absolute;
|
||||
right: -.5em;
|
||||
top: -.5em;
|
||||
color: $white;
|
||||
background: $purple-200;
|
||||
padding: .15em;
|
||||
text-align: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 1px 1px 0 rgba($black, 0.12);
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -54,12 +54,12 @@
|
||||
:popoverPosition="'top'",
|
||||
@click="selectItem(item)"
|
||||
)
|
||||
template(slot="popoverContent", scope="ctx")
|
||||
template(slot="popoverContent", slot-scope="ctx")
|
||||
div.questPopover
|
||||
h4.popover-content-title {{ item.text }}
|
||||
questInfo(:quest="item")
|
||||
|
||||
template(slot="itemBadge", scope="ctx")
|
||||
template(slot="itemBadge", slot-scope="ctx")
|
||||
span.badge.badge-pill.badge-item.badge-svg(
|
||||
:class="{'item-selected-badge': ctx.item.pinned, 'hide': !ctx.item.pinned}",
|
||||
@click.prevent.stop="togglePinned(ctx.item)"
|
||||
@@ -94,7 +94,7 @@
|
||||
:itemMargin=24,
|
||||
:type="'pet_quests'",
|
||||
)
|
||||
template(slot="item", scope="ctx")
|
||||
template(slot="item", slot-scope="ctx")
|
||||
shopItem(
|
||||
:key="ctx.item.key",
|
||||
:item="ctx.item",
|
||||
@@ -104,12 +104,12 @@
|
||||
:emptyItem="false",
|
||||
@click="selectItem(ctx.item)"
|
||||
)
|
||||
span(slot="popoverContent", scope="ctx")
|
||||
span(slot="popoverContent", slot-scope="ctx")
|
||||
div.questPopover
|
||||
h4.popover-content-title {{ ctx.item.text }}
|
||||
questInfo(:quest="ctx.item")
|
||||
|
||||
template(slot="itemBadge", scope="ctx")
|
||||
template(slot="itemBadge", slot-scope="ctx")
|
||||
span.badge.badge-pill.badge-item.badge-svg(
|
||||
:class="{'item-selected-badge': ctx.item.pinned, 'hide': !ctx.item.pinned}",
|
||||
@click.prevent.stop="togglePinned(ctx.item)"
|
||||
@@ -145,7 +145,7 @@
|
||||
.popover-content-text(v-if='item.lvl > user.stats.lvl') {{ `${$t('mustLvlQuest', {level: item.lvl})}` }}
|
||||
questInfo(v-if='!item.locked', :quest="item")
|
||||
|
||||
template(slot="itemBadge", scope="ctx")
|
||||
template(slot="itemBadge", slot-scope="ctx")
|
||||
span.badge.badge-pill.badge-item.badge-svg(
|
||||
:class="{'item-selected-badge': ctx.item.pinned, 'hide': !ctx.item.pinned}",
|
||||
@click.prevent.stop="togglePinned(ctx.item)"
|
||||
@@ -172,7 +172,7 @@
|
||||
h4.popover-content-title {{ item.text }}
|
||||
questInfo(:quest="item")
|
||||
|
||||
template(slot="itemBadge", scope="ctx")
|
||||
template(slot="itemBadge", slot-scope="ctx")
|
||||
span.badge.badge-pill.badge-item.badge-svg(
|
||||
:class="{'item-selected-badge': ctx.item.pinned, 'hide': !ctx.item.pinned}",
|
||||
@click.prevent.stop="togglePinned(ctx.item)"
|
||||
@@ -190,7 +190,7 @@
|
||||
:withPin="true",
|
||||
@change="resetItemToBuy($event)",
|
||||
)
|
||||
template(slot="item", scope="ctx")
|
||||
template(slot="item", slot-scope="ctx")
|
||||
item.flat(
|
||||
:item="ctx.item",
|
||||
:itemContentClass="ctx.item.class",
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
:showEventBadge="false",
|
||||
@click="itemSelected(item)"
|
||||
)
|
||||
template(slot="itemBadge", scope="ctx")
|
||||
template(slot="itemBadge", slot-scope="ctx")
|
||||
span.badge.badge-pill.badge-item.badge-svg(
|
||||
:class="{'item-selected-badge': ctx.item.pinned, 'hide': !ctx.item.pinned}",
|
||||
@click.prevent.stop="togglePinned(ctx.item)"
|
||||
|
||||
@@ -18,8 +18,7 @@ div
|
||||
slot(name="itemImage", :item="item")
|
||||
|
||||
div.price
|
||||
span.svg-icon.inline.icon-16(v-html="icons[currencyClass]")
|
||||
|
||||
span.svg-icon.inline.icon-16(v-html="icons[currencyClass]", v-once)
|
||||
span.price-label(:class="currencyClass", v-once) {{ getPrice() }}
|
||||
b-popover(
|
||||
:target="itemId",
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
:itemMargin=24,
|
||||
:type="category.identifier",
|
||||
)
|
||||
template(slot="item", scope="ctx")
|
||||
template(slot="item", slot-scope="ctx")
|
||||
shopItem(
|
||||
:key="ctx.item.key",
|
||||
:item="ctx.item",
|
||||
@@ -72,11 +72,11 @@
|
||||
:emptyItem="false",
|
||||
@click="selectItemToBuy(ctx.item)"
|
||||
)
|
||||
span(slot="popoverContent", scope="ctx")
|
||||
span(slot="popoverContent", slot-scope="ctx")
|
||||
div
|
||||
h4.popover-content-title {{ ctx.item.text }}
|
||||
|
||||
template(slot="itemBadge", scope="ctx")
|
||||
template(slot="itemBadge", slot-scope="ctx")
|
||||
span.badge.badge-pill.badge-item.badge-svg(
|
||||
v-if="ctx.item.pinType !== 'IGNORE'",
|
||||
:class="{'item-selected-badge': ctx.item.pinned, 'hide': !ctx.item.pinned}",
|
||||
|
||||
@@ -16,9 +16,13 @@ div
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.task-unclaimed a {
|
||||
float: right;
|
||||
}
|
||||
.claim-bottom-message {
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
.task-unclaimed a {
|
||||
float: right;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -4,11 +4,15 @@
|
||||
| {{ message }}
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.approval {
|
||||
background: #24cc8f;
|
||||
color: #fff;
|
||||
}
|
||||
<style lang="scss" scoped>
|
||||
.claim-bottom-message {
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
.approval {
|
||||
background: #24cc8f;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
@change="resetItemToBuy($event)"
|
||||
v-if='type === "reward"')
|
||||
.d-flex
|
||||
h2.tasks-column-title(v-once) {{ $t(types[type].label) }}
|
||||
h2.tasks-column-title
|
||||
| {{ $t(types[type].label) }}
|
||||
.badge.badge-pill.badge-purple.column-badge(v-if="badgeCount > 0") {{ badgeCount }}
|
||||
.filters.d-flex.justify-content-end
|
||||
.filter.small-text(
|
||||
v-for="filter in types[type].filters",
|
||||
@@ -15,18 +17,35 @@
|
||||
@click="activateFilter(type, filter)",
|
||||
) {{ $t(filter.label) }}
|
||||
.tasks-list(ref="tasksWrapper")
|
||||
input.quick-add(
|
||||
textarea.quick-add(
|
||||
:rows="quickAddRows",
|
||||
v-if="isUser", :placeholder="quickAddPlaceholder",
|
||||
v-model="quickAddText", @keyup.enter="quickAdd",
|
||||
v-model="quickAddText", @keypress.enter="quickAdd",
|
||||
ref="quickAdd",
|
||||
@focus="quickAddFocused = true", @blur="quickAddFocused = false",
|
||||
)
|
||||
transition(name="quick-add-tip-slide")
|
||||
.quick-add-tip.small-text(v-show="quickAddFocused", v-html="$t('addMultipleTip')")
|
||||
.column-background(
|
||||
v-if="isUser === true",
|
||||
:class="{'initial-description': initialColumnDescription}",
|
||||
ref="columnBackground",
|
||||
)
|
||||
.svg-icon(v-html="icons[type]", :class="`icon-${type}`", v-once)
|
||||
h3(v-once) {{$t('theseAreYourTasks', {taskType: $t(types[type].label)})}}
|
||||
.small-text {{$t(`${type}sDesc`)}}
|
||||
.sortable-tasks(
|
||||
ref="tasksList",
|
||||
v-sortable='activeFilters[type].label !== "scheduled"',
|
||||
@onsort='sorted',
|
||||
data-sortableId
|
||||
)
|
||||
.sortable-tasks(ref="tasksList", v-sortable='activeFilters[type].label !== "scheduled"', @onsort='sorted', data-sortableId)
|
||||
task(
|
||||
v-for="task in taskList",
|
||||
:key="task.id", :task="task",
|
||||
v-if="filterTask(task)",
|
||||
:isUser="isUser",
|
||||
@editTask="editTask",
|
||||
@moveTo="moveTo",
|
||||
:group='group',
|
||||
)
|
||||
template(v-if="hasRewardsList")
|
||||
@@ -39,15 +58,6 @@
|
||||
@click="openBuyDialog(reward)",
|
||||
:popoverPosition="'left'"
|
||||
)
|
||||
|
||||
.column-background(
|
||||
v-if="isUser === true",
|
||||
:class="{'initial-description': initialColumnDescription}",
|
||||
ref="columnBackground",
|
||||
)
|
||||
.svg-icon(v-html="icons[type]", :class="`icon-${type}`", v-once)
|
||||
h3(v-once) {{$t('theseAreYourTasks', {taskType: $t(types[type].label)})}}
|
||||
.small-text {{$t(`${type}sDesc`)}}
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -57,11 +67,6 @@
|
||||
min-height: 556px;
|
||||
}
|
||||
|
||||
.task-wrapper {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.sortable-tasks + .reward-items {
|
||||
margin-top: 16px;
|
||||
}
|
||||
@@ -87,9 +92,10 @@
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
padding: 12px 16px;
|
||||
font-weight: bold;
|
||||
border-color: transparent;
|
||||
transition: background 0.15s ease-in;
|
||||
resize: none;
|
||||
margin-bottom: 0px;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba($black, 0.1);
|
||||
@@ -101,20 +107,42 @@
|
||||
border-color: $purple-500;
|
||||
color: $gray-50;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-gradient {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
height: 42px;
|
||||
background-image: linear-gradient(to bottom, rgba($gray-10, 0), rgba($gray-10, 0.24));
|
||||
width: 100%;
|
||||
z-index: 99;
|
||||
.quick-add-tip {
|
||||
font-style: normal;
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
|
||||
overflow-y: hidden;
|
||||
max-height: 65px; // approximate max height
|
||||
}
|
||||
|
||||
.quick-add-tip-slide-enter-active {
|
||||
transition: all 0.5s cubic-bezier(0, 1, 0.5, 1);
|
||||
}
|
||||
|
||||
.quick-add-tip-slide-leave-active {
|
||||
transition: all 0.5s cubic-bezier(0, 1, 0.5, 1);
|
||||
}
|
||||
|
||||
.quick-add-tip-slide-enter, .quick-add-tip-slide-leave-to {
|
||||
max-height: 0;
|
||||
padding: 0px 16px;
|
||||
}
|
||||
|
||||
.tasks-column-title {
|
||||
margin-bottom: 8px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.column-badge {
|
||||
top: -5px;
|
||||
right: -24px;
|
||||
}
|
||||
|
||||
.filters {
|
||||
@@ -142,7 +170,6 @@
|
||||
.column-background {
|
||||
position: absolute;
|
||||
bottom: 32px;
|
||||
z-index: 1;
|
||||
|
||||
&.initial-description {
|
||||
top: 30%;
|
||||
@@ -230,6 +257,7 @@ export default {
|
||||
},
|
||||
props: ['type', 'isUser', 'searchText', 'selectedTags', 'taskListOverride', 'group'], // @TODO: maybe we should store the group on state?
|
||||
data () {
|
||||
// @TODO refactor this so that filter functions aren't in data
|
||||
const types = Object.freeze({
|
||||
habit: {
|
||||
label: 'habits',
|
||||
@@ -285,6 +313,8 @@ export default {
|
||||
|
||||
forceRefresh: new Date(),
|
||||
quickAddText: '',
|
||||
quickAddFocused: false,
|
||||
quickAddRows: 1,
|
||||
|
||||
selectedItemToBuy: {},
|
||||
};
|
||||
@@ -315,13 +345,14 @@ export default {
|
||||
taskList = sortBy(taskList, filter.sort);
|
||||
}
|
||||
|
||||
return taskList;
|
||||
return taskList.filter(t => {
|
||||
return this.filterTask(t);
|
||||
});
|
||||
},
|
||||
inAppRewards () {
|
||||
let watchRefresh = this.forceRefresh; // eslint-disable-line
|
||||
let rewards = inAppRewards(this.user);
|
||||
|
||||
|
||||
// Add season rewards if user is affected
|
||||
// @TODO: Add buff coniditional
|
||||
const seasonalSkills = {
|
||||
@@ -365,6 +396,26 @@ export default {
|
||||
const type = this.$t(this.type);
|
||||
return this.$t('addATask', {type});
|
||||
},
|
||||
badgeCount () {
|
||||
// 0 means the badge will not be shown
|
||||
// It is shown for the all and due views of dailies
|
||||
// and for the active and scheduled views of todos.
|
||||
if (this.type === 'todo') {
|
||||
if (this.activeFilters.todo.label !== 'complete2') return this.taskList.length;
|
||||
} else if (this.type === 'daily') {
|
||||
const activeFilter = this.activeFilters.daily.label;
|
||||
|
||||
if (activeFilter === 'due') {
|
||||
return this.taskList.length;
|
||||
} else if (activeFilter === 'all') {
|
||||
return this.taskList.reduce((count, t) => {
|
||||
return !t.completed && shouldDo(new Date(), t, this.userPreferences) ? count + 1 : count;
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
taskList: {
|
||||
@@ -392,13 +443,12 @@ export default {
|
||||
createTask: 'tasks:create',
|
||||
}),
|
||||
async sorted (data) {
|
||||
const filteredList = this.taskList.filter(this.activeFilters[this.type].filter);
|
||||
const sorting = this.taskList;
|
||||
const filteredList = this.taskList;
|
||||
const taskIdToMove = filteredList[data.oldIndex]._id;
|
||||
|
||||
// Server
|
||||
const taskIdToReplace = filteredList[data.newIndex];
|
||||
const newIndexOnServer = this.taskList.findIndex(taskId => taskId === taskIdToReplace);
|
||||
const newIndexOnServer = this.tasks[`${this.type}s`].findIndex(taskId => taskId === taskIdToReplace);
|
||||
let newOrder = await this.$store.dispatch('tasks:move', {
|
||||
taskId: taskIdToMove,
|
||||
position: newIndexOnServer,
|
||||
@@ -406,16 +456,47 @@ export default {
|
||||
this.user.tasksOrder[`${this.type}s`] = newOrder;
|
||||
|
||||
// Client
|
||||
if (sorting) {
|
||||
const deleted = sorting.splice(data.oldIndex, 1);
|
||||
sorting.splice(data.newIndex, 0, deleted[0]);
|
||||
}
|
||||
const deleted = this.tasks[`${this.type}s`].splice(data.oldIndex, 1);
|
||||
this.tasks[`${this.type}s`].splice(data.newIndex, 0, deleted[0]);
|
||||
},
|
||||
quickAdd () {
|
||||
const task = taskDefaults({type: this.type, text: this.quickAddText});
|
||||
task.tags = this.selectedTags;
|
||||
async moveTo (task, where) { // where is 'top' or 'bottom'
|
||||
const taskIdToMove = task._id;
|
||||
const list = this.tasks[`${this.type}s`];
|
||||
|
||||
const oldPosition = list.findIndex(t => t._id === taskIdToMove);
|
||||
const moved = list.splice(oldPosition, 1);
|
||||
const newPosition = where === 'top' ? 0 : list.length;
|
||||
list.splice(newPosition, 0, moved[0]);
|
||||
|
||||
let newOrder = await this.$store.dispatch('tasks:move', {
|
||||
taskId: taskIdToMove,
|
||||
position: newPosition,
|
||||
});
|
||||
this.user.tasksOrder[`${this.type}s`] = newOrder;
|
||||
},
|
||||
quickAdd (ev) {
|
||||
// Add a new line if Shift+Enter Pressed
|
||||
if (ev.shiftKey) {
|
||||
this.quickAddRows++;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Do not add new line is added if only Enter is pressed
|
||||
ev.preventDefault();
|
||||
const text = this.quickAddText;
|
||||
if (!text) return false;
|
||||
|
||||
const tasks = text.split('\n').reverse().filter(taskText => {
|
||||
return taskText ? true : false;
|
||||
}).map(taskText => {
|
||||
const task = taskDefaults({type: this.type, text: taskText});
|
||||
task.tags = this.selectedTags;
|
||||
return task;
|
||||
});
|
||||
|
||||
this.quickAddText = null;
|
||||
this.createTask(task);
|
||||
this.quickAddRows = 1;
|
||||
this.createTask(tasks);
|
||||
},
|
||||
editTask (task) {
|
||||
this.$emit('editTask', task);
|
||||
|
||||
@@ -13,15 +13,45 @@
|
||||
.svg-icon.check(v-html="icons.check", :class="{'display-check-icon': task.completed}")
|
||||
// Task title, description and icons
|
||||
.task-content(:class="contentClass")
|
||||
.task-clickable-area(@click="edit($event, task)")
|
||||
h3.task-title(:class="{ 'has-notes': task.notes }", v-markdown="task.text")
|
||||
.task-notes.small-text(v-markdown="task.notes")
|
||||
.task-clickable-area(@click="edit($event, task)", :class="{'task-clickable-area-user': isUser}")
|
||||
.d-flex.justify-content-between
|
||||
h3.task-title(:class="{ 'has-notes': task.notes }", v-markdown="task.text")
|
||||
menu-dropdown.task-dropdown(
|
||||
v-if="isUser && !isRunningYesterdailies",
|
||||
:right="task.type === 'reward'",
|
||||
ref="taskDropdown"
|
||||
)
|
||||
div(slot="dropdown-toggle", draggable=false)
|
||||
.svg-icon.dropdown-icon(v-html="icons.menu")
|
||||
div(slot="dropdown-content", draggable=false)
|
||||
.dropdown-item.edit-task-item(ref="editTaskItem")
|
||||
span.dropdown-icon-item
|
||||
span.svg-icon.inline.edit-icon(v-html="icons.edit")
|
||||
span.text {{ $t('edit') }}
|
||||
.dropdown-item(@click="moveToTop")
|
||||
span.dropdown-icon-item
|
||||
span.svg-icon.inline.push-to-top(v-html="icons.top")
|
||||
span.text {{ $t('taskToTop') }}
|
||||
.dropdown-item(@click="moveToBottom")
|
||||
span.dropdown-icon-item
|
||||
span.svg-icon.inline.push-to-bottom(v-html="icons.bottom")
|
||||
span.text {{ $t('taskToBottom') }}
|
||||
.dropdown-item(@click="destroy", v-if="canDelete(task)")
|
||||
span.dropdown-icon-item.delete-task-item
|
||||
span.svg-icon.inline.delete(v-html="icons.delete")
|
||||
span.text {{ $t('delete') }}
|
||||
|
||||
.task-notes.small-text(
|
||||
v-markdown="task.notes",
|
||||
:class="{'has-checklist': task.notes && hasChecklist}",
|
||||
)
|
||||
.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}",
|
||||
v-b-tooltip.hover.bottom="$t(`${task.collapseChecklist ? 'expand': 'collapse'}Checklist`)",
|
||||
)
|
||||
.svg-icon(v-html="icons.checklist")
|
||||
span {{ checklistProgress }}
|
||||
@@ -70,7 +100,7 @@
|
||||
approval-footer(:task='task', v-if='this.task.group.id', :group='group')
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
.task {
|
||||
@@ -78,14 +108,13 @@
|
||||
box-shadow: 0 2px 2px 0 rgba($black, 0.16), 0 1px 4px 0 rgba($black, 0.12);
|
||||
background: transparent;
|
||||
border-radius: 2px;
|
||||
z-index: 9;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 1px 8px 0 rgba($black, 0.12), 0 4px 4px 0 rgba($black, 0.16);
|
||||
|
||||
.left-control, .right-control, .task-content {
|
||||
border-color: $purple-500;
|
||||
border-color: $purple-400;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,32 +128,112 @@
|
||||
color: $gray-10;
|
||||
font-weight: normal;
|
||||
margin-bottom: 0px;
|
||||
line-height: 1.43;
|
||||
font-size: 14px;
|
||||
|
||||
&.has-notes {
|
||||
padding-bottom: 0px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.task-clickable-area {
|
||||
padding: 7px 8px;
|
||||
padding-bottom: 0px;
|
||||
|
||||
&-user {
|
||||
padding-right: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.task-title + .task-dropdown /deep/ .dropdown-menu {
|
||||
margin-top: 2px !important;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
color: $gray-100 !important;
|
||||
}
|
||||
|
||||
.task /deep/ .habitica-menu-dropdown .habitica-menu-dropdown-toggle {
|
||||
opacity: 0;
|
||||
padding: 0 8px;
|
||||
transition: opacity 0.15s ease-in;
|
||||
}
|
||||
|
||||
.task:hover /deep/ .habitica-menu-dropdown .habitica-menu-dropdown-toggle {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.task-clickable-area /deep/ .habitica-menu-dropdown.open .habitica-menu-dropdown-toggle {
|
||||
opacity: 1;
|
||||
|
||||
.svg-icon {
|
||||
color: $purple-400 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.task-clickable-area /deep/ .habitica-menu-dropdown .habitica-menu-dropdown-toggle:hover .svg-icon {
|
||||
color: $purple-400 !important;
|
||||
}
|
||||
|
||||
.task-dropdown {
|
||||
max-height: 16px;
|
||||
}
|
||||
|
||||
.task-dropdown /deep/ .dropdown-menu {
|
||||
.dropdown-item {
|
||||
cursor: pointer !important;
|
||||
transition: none;
|
||||
|
||||
* {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $purple-300;
|
||||
|
||||
.svg-icon.push-to-top, .svg-icon.push-to-bottom {
|
||||
* {
|
||||
stroke: $purple-300;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.task-notes {
|
||||
color: $gray-100;
|
||||
font-style: normal;
|
||||
padding-right: 6px;
|
||||
|
||||
&.has-checklist {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.task-content {
|
||||
padding: 8px;
|
||||
padding-top: 0px;
|
||||
padding-bottom: 7px;
|
||||
flex-grow: 1;
|
||||
cursor: pointer;
|
||||
background: $white;
|
||||
border: 1px solid transparent;
|
||||
transition-duration: 0.15;
|
||||
|
||||
&.no-right-border {
|
||||
border-right: none !important;
|
||||
}
|
||||
|
||||
&.reward-content {
|
||||
border-top-left-radius: 2px;
|
||||
border-bottom-left-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.checklist {
|
||||
margin-bottom: 2px;
|
||||
margin-top: 8px;
|
||||
margin-top: -3px;
|
||||
}
|
||||
|
||||
.collapse-checklist {
|
||||
@@ -154,6 +263,7 @@
|
||||
margin-bottom: 10px;
|
||||
min-height: 0px;
|
||||
width: 100%;
|
||||
margin-left: 8px;
|
||||
|
||||
&-done {
|
||||
color: $gray-300;
|
||||
@@ -170,6 +280,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.icons, .checklist {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.icons {
|
||||
margin-top: 4px;
|
||||
color: $gray-300;
|
||||
@@ -197,6 +311,34 @@
|
||||
height: 7.1px;
|
||||
}
|
||||
|
||||
.delete-task-item {
|
||||
color: $red-10;
|
||||
}
|
||||
|
||||
.edit-task-item span.text {
|
||||
margin-left: -3px;
|
||||
}
|
||||
|
||||
.svg-icon.edit-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.svg-icon.push-to-top, .svg-icon.push-to-bottom {
|
||||
width: 10px;
|
||||
height: 11px;
|
||||
margin-left: 3px;
|
||||
|
||||
svg {
|
||||
stroke: $purple-300;
|
||||
}
|
||||
}
|
||||
|
||||
.svg-icon.delete {
|
||||
width: 14px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.tags.svg-icon, .calendar.svg-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
@@ -235,6 +377,12 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.left-control, .right-control, .task-control {
|
||||
transition-duration: 0.15s;
|
||||
transition-property: border-color, background, color;
|
||||
transition-timing-function: ease-in;
|
||||
}
|
||||
|
||||
.left-control {
|
||||
border-top-left-radius: 2px;
|
||||
border-bottom-left-radius: 2px;
|
||||
@@ -244,6 +392,8 @@
|
||||
|
||||
& + .task-content {
|
||||
border-left: none;
|
||||
border-top-right-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,6 +477,7 @@ import axios from 'axios';
|
||||
import scoreTask from 'common/script/ops/scoreTask';
|
||||
import Vue from 'vue';
|
||||
import * as Analytics from 'client/libs/analytics';
|
||||
import bTooltip from 'bootstrap-vue/lib/directives/tooltip';
|
||||
|
||||
import positiveIcon from 'assets/svg/positive.svg';
|
||||
import negativeIcon from 'assets/svg/negative.svg';
|
||||
@@ -336,12 +487,18 @@ 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 editIcon from 'assets/svg/edit.svg';
|
||||
import topIcon from 'assets/svg/top.svg';
|
||||
import bottomIcon from 'assets/svg/bottom.svg';
|
||||
import deleteIcon from 'assets/svg/delete.svg';
|
||||
import checklistIcon from 'assets/svg/checklist.svg';
|
||||
import menuIcon from 'assets/svg/menu.svg';
|
||||
import bPopover from 'bootstrap-vue/lib/components/popover';
|
||||
import markdownDirective from 'client/directives/markdown';
|
||||
import notifications from 'client/mixins/notifications';
|
||||
import approvalHeader from './approvalHeader';
|
||||
import approvalFooter from './approvalFooter';
|
||||
import MenuDropdown from '../ui/customMenuDropdown';
|
||||
|
||||
export default {
|
||||
mixins: [notifications],
|
||||
@@ -349,9 +506,11 @@ export default {
|
||||
bPopover,
|
||||
approvalFooter,
|
||||
approvalHeader,
|
||||
MenuDropdown,
|
||||
},
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
bTooltip,
|
||||
},
|
||||
props: ['task', 'isUser', 'group', 'dueDate'], // @TODO: maybe we should store the group on state?
|
||||
data () {
|
||||
@@ -366,6 +525,11 @@ export default {
|
||||
tags: tagsIcon,
|
||||
check: checkIcon,
|
||||
checklist: checklistIcon,
|
||||
delete: deleteIcon,
|
||||
edit: editIcon,
|
||||
top: topIcon,
|
||||
bottom: bottomIcon,
|
||||
menu: menuIcon,
|
||||
}),
|
||||
};
|
||||
},
|
||||
@@ -373,15 +537,19 @@ export default {
|
||||
...mapState({
|
||||
user: 'user.data',
|
||||
castingSpell: 'spellOptions.castingSpell',
|
||||
isRunningYesterdailies: 'isRunningYesterdailies',
|
||||
}),
|
||||
...mapGetters({
|
||||
getTagsFor: 'tasks:getTagsFor',
|
||||
getTaskClasses: 'tasks:getTaskClasses',
|
||||
canDelete: 'tasks:canDelete',
|
||||
}),
|
||||
hasChecklist () {
|
||||
return this.task.checklist && this.task.checklist.length > 0;
|
||||
},
|
||||
canViewchecklist () {
|
||||
let hasChecklist = this.task.checklist && this.task.checklist.length > 0;
|
||||
let userIsTaskUser = this.task.userId ? this.task.userId === this.user._id : true;
|
||||
return hasChecklist && userIsTaskUser;
|
||||
return this.hasChecklist && userIsTaskUser;
|
||||
},
|
||||
checklistProgress () {
|
||||
const totalItems = this.task.checklist.length;
|
||||
@@ -405,11 +573,19 @@ export default {
|
||||
return this.getTaskClasses(this.task, 'control', this.dueDate);
|
||||
},
|
||||
contentClass () {
|
||||
const type = this.task.type;
|
||||
|
||||
const classes = [];
|
||||
classes.push(this.getTaskClasses(this.task, 'content', this.dueDate));
|
||||
if (this.task.type === 'reward' || this.task.type === 'habit') {
|
||||
|
||||
if (type === 'reward' || type === 'habit') {
|
||||
classes.push('no-right-border');
|
||||
}
|
||||
|
||||
if (type === 'reward') {
|
||||
classes.push('reward-content');
|
||||
}
|
||||
|
||||
return classes;
|
||||
},
|
||||
showStreak () {
|
||||
@@ -432,6 +608,7 @@ export default {
|
||||
...mapActions({
|
||||
scoreChecklistItem: 'tasks:scoreChecklistItem',
|
||||
collapseChecklist: 'tasks:collapseChecklist',
|
||||
destroyTask: 'tasks:destroy',
|
||||
}),
|
||||
toggleChecklistItem (item) {
|
||||
if (this.castingSpell) return;
|
||||
@@ -439,15 +616,33 @@ export default {
|
||||
this.scoreChecklistItem({taskId: this.task._id, itemId: item.id});
|
||||
},
|
||||
edit (e, task) {
|
||||
if (this.isRunningYesterdailies) return;
|
||||
|
||||
// Prevent clicking on a link from opening the edit modal
|
||||
const target = e.target || e.srcElement;
|
||||
|
||||
if (target.tagName === 'A') { // Link
|
||||
return;
|
||||
} else if (!this.$store.state.spellOptions.castingSpell) {
|
||||
if (target.tagName === 'A') return; // clicked on a link
|
||||
|
||||
const isDropdown = this.$refs.taskDropdown.$el.contains(target);
|
||||
const isEditAction = this.$refs.editTaskItem.contains(target);
|
||||
|
||||
if (isDropdown && !isEditAction) return;
|
||||
|
||||
if (!this.$store.state.spellOptions.castingSpell) {
|
||||
this.$emit('editTask', task);
|
||||
}
|
||||
},
|
||||
moveToTop () {
|
||||
this.$emit('moveTo', this.task, 'top');
|
||||
},
|
||||
moveToBottom () {
|
||||
this.$emit('moveTo', this.task, 'bottom');
|
||||
},
|
||||
destroy () {
|
||||
if (!confirm(this.$t('sureDelete'))) return;
|
||||
this.destroyTask(this.task);
|
||||
this.$emit('taskDestroyed', this.task);
|
||||
},
|
||||
castEnd (e, task) {
|
||||
this.$root.$emit('castEnd', task, 'task', e);
|
||||
},
|
||||
|
||||
@@ -9,7 +9,11 @@
|
||||
button.btn.btn-secondary(type="submit", v-once) {{ $t('save') }}
|
||||
.form-group
|
||||
label(v-once) {{ `${$t('text')}*` }}
|
||||
input.form-control.title-input(type='text', :class="[`${cssClass}-modal-input`]", required, v-model="task.text", autofocus, spellcheck='true')
|
||||
input.form-control.title-input(
|
||||
type="text", :class="[`${cssClass}-modal-input`]",
|
||||
required, v-model="task.text",
|
||||
autofocus, spellcheck="true",
|
||||
)
|
||||
.form-group
|
||||
label(v-once) {{ $t('notes') }}
|
||||
textarea.form-control(:class="[`${cssClass}-modal-input`]", v-model="task.notes", rows="3")
|
||||
@@ -181,9 +185,12 @@
|
||||
input, textarea {
|
||||
border: none;
|
||||
background-color: rgba(0, 0, 0, 0.16);
|
||||
opacity: 0.64;
|
||||
color: $white !important;
|
||||
|
||||
&:focus {
|
||||
color: $white !important;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,7 +228,7 @@
|
||||
input {
|
||||
background: $white;
|
||||
border: 1px solid $gray-500;
|
||||
color: $gray-200;
|
||||
color: $gray-200 !important;
|
||||
|
||||
&:focus {
|
||||
color: $gray-50 !important;
|
||||
@@ -580,6 +587,7 @@ export default {
|
||||
...mapGetters({
|
||||
getTaskClasses: 'tasks:getTaskClasses',
|
||||
getTagsFor: 'tasks:getTagsFor',
|
||||
canDeleteTask: 'tasks:canDelete',
|
||||
}),
|
||||
...mapState({
|
||||
user: 'user.data',
|
||||
@@ -593,9 +601,7 @@ export default {
|
||||
return !isUserChallenge && (this.challengeId || this.task.challenge && this.task.challenge.id);
|
||||
},
|
||||
canDelete () {
|
||||
let isUserChallenge = Boolean(this.task.userId);
|
||||
let activeChallenge = isUserChallenge && this.task.challenge && this.task.challenge.id && !this.task.challenge.broken;
|
||||
return this.purpose !== 'create' && !activeChallenge;
|
||||
return this.purpose !== 'create' && this.canDeleteTask(this.task);
|
||||
},
|
||||
title () {
|
||||
const type = this.$t(this.task.type);
|
||||
@@ -740,7 +746,7 @@ export default {
|
||||
this.$root.$emit('hide::modal', 'task-modal');
|
||||
},
|
||||
destroy () {
|
||||
if (!confirm('Are you sure you want to delete this task?')) return;
|
||||
if (!confirm(this.$t('sureDelete'))) return;
|
||||
this.destroyTask(this.task);
|
||||
this.$emit('taskDestroyed', this.task);
|
||||
this.$root.$emit('hide::modal', 'task-modal');
|
||||
|
||||
75
website/client/components/ui/customMenuDropdown.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<!--
|
||||
A simplified dropdown component that doesn't rely on buttons as toggles like bootstrap-vue
|
||||
-->
|
||||
|
||||
<template lang="pug">
|
||||
.habitica-menu-dropdown.item-with-icon.dropdown(@click="toggleDropdown()", :class="{open: isDropdownOpen}")
|
||||
.habitica-menu-dropdown-toggle
|
||||
slot(name="dropdown-toggle")
|
||||
.dropdown-menu(:class="{'dropdown-menu-right': right}")
|
||||
slot(name="dropdown-content")
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
.habitica-menu-dropdown.open {
|
||||
.habitica-menu-dropdown-toggle .svg-icon {
|
||||
color: $white !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang='scss' scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
.dropdown {
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
cursor: auto;
|
||||
box-shadow: 0 2px 2px 0 rgba($black, 0.16), 0 1px 4px 0 rgba($black, 0.12);
|
||||
max-height: calc(100vh - 100px);
|
||||
overflow: auto;
|
||||
|
||||
/deep/ .dropdown-separated {
|
||||
border-bottom: 1px solid $gray-500;
|
||||
}
|
||||
}
|
||||
|
||||
&.open {
|
||||
.dropdown-menu {
|
||||
display: block;
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['right'],
|
||||
data () {
|
||||
return {
|
||||
isDropdownOpen: false,
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
document.documentElement.addEventListener('click', this._clickOutListener);
|
||||
},
|
||||
destroyed () {
|
||||
document.removeEventListener('click', this._clickOutListener);
|
||||
},
|
||||
methods: {
|
||||
_clickOutListener (e) {
|
||||
if (!this.$el.contains(e.target) && this.isDropdownOpen) {
|
||||
this.toggleDropdown();
|
||||
}
|
||||
},
|
||||
toggleDropdown () {
|
||||
this.isDropdownOpen = !this.isDropdownOpen;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -12,11 +12,11 @@
|
||||
p.call-to-action.text-center {{ $t('checkOffYesterDailies') }}
|
||||
.tasks-list
|
||||
task(
|
||||
v-for='task in tasksByType["daily"]',
|
||||
:key='task.id',
|
||||
:task='task',
|
||||
:isUser='true',
|
||||
:dueDate='dueDate',
|
||||
v-for="task in tasksByType.daily",
|
||||
:key="task.id",
|
||||
:task="task",
|
||||
:isUser="true",
|
||||
:dueDate="dueDate",
|
||||
)
|
||||
.start-day.text-center
|
||||
button.btn.btn-primary(@click='close()') {{ $t('yesterDailiesCallToAction') }}
|
||||
|
||||
@@ -14,6 +14,7 @@ let sortableReferences = {};
|
||||
|
||||
function createSortable (el, vNode) {
|
||||
let sortableRef = Sortable.create(el, {
|
||||
filter: '.task-dropdown', // do not make the tasks dropdown draggable or it won't work
|
||||
onSort: (evt) => {
|
||||
emit(vNode, 'onsort', {
|
||||
oldIndex: evt.oldIndex,
|
||||
|
||||
@@ -92,18 +92,29 @@ function sanitizeChecklist (task) {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Supply an array to create multiple tasks
|
||||
export async function create (store, createdTask) {
|
||||
const type = `${createdTask.type}s`;
|
||||
const list = store.state.tasks.data[type];
|
||||
// Treat all create actions as if we are adding multiple tasks
|
||||
const payload = Array.isArray(createdTask) ? createdTask : [createdTask];
|
||||
|
||||
sanitizeChecklist(createdTask);
|
||||
payload.forEach(t => {
|
||||
const type = `${t.type}s`;
|
||||
const list = store.state.tasks.data[type];
|
||||
|
||||
list.unshift(createdTask);
|
||||
store.state.user.data.tasksOrder[type].unshift(createdTask._id);
|
||||
sanitizeChecklist(t);
|
||||
|
||||
const response = await axios.post('/api/v3/tasks/user', createdTask);
|
||||
list.unshift(t);
|
||||
store.state.user.data.tasksOrder[type].unshift(t._id);
|
||||
});
|
||||
|
||||
Object.assign(list[0], response.data.data);
|
||||
const response = await axios.post('/api/v3/tasks/user', payload);
|
||||
const data = Array.isArray(response.data.data) ? response.data.data : [response.data.data];
|
||||
|
||||
data.forEach(taskRes => {
|
||||
const taskData = store.state.tasks.data[`${taskRes.type}s`].find(t => t._id === taskRes._id);
|
||||
Object.assign(taskData, taskRes);
|
||||
});
|
||||
}
|
||||
|
||||
export async function save (store, editedTask) {
|
||||
|
||||
@@ -27,6 +27,14 @@ function getTaskColorByValue (value) {
|
||||
}
|
||||
}
|
||||
|
||||
export function canDelete () {
|
||||
return (task) => {
|
||||
let isUserChallenge = Boolean(task.userId);
|
||||
let activeChallenge = isUserChallenge && task.challenge && task.challenge.id && !task.challenge.broken;
|
||||
return !activeChallenge;
|
||||
};
|
||||
}
|
||||
|
||||
export function getTaskClasses (store) {
|
||||
const userPreferences = store.state.user.data.preferences;
|
||||
|
||||
|
||||
@@ -182,6 +182,7 @@
|
||||
"consecutiveMonths": "Consecutive Months:",
|
||||
"gemCapExtra": "Gem Cap Extra:",
|
||||
"mysticHourglasses": "Mystic Hourglasses:",
|
||||
"mysticHourglassesTooltip": "Mystic Hourglasses",
|
||||
"paypal": "PayPal",
|
||||
"amazonPayments": "Amazon Payments",
|
||||
"timezone": "Time Zone",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"clearCompleted": "Delete Completed",
|
||||
"lotOfToDos": "Your most recent 30 completed To-Dos are shown here. You can see older completed To-Dos from Data > Data Display Tool or Data > Export Data > User Data.",
|
||||
"deleteToDosExplanation": "If you click the button below, all of your completed To-Dos and archived To-Dos will be permanently deleted, except for To-Dos from active challenges and Group Plans. Export them first if you want to keep a record of them.",
|
||||
"addmultiple": "Add Multiple",
|
||||
"addMultipleTip": "<strong>Tip:</strong> To add multiple Tasks, separate each one using a line break (Shift + Enter) and then press \"Enter.\"",
|
||||
"addsingle": "Add Single",
|
||||
"addATask": "Add a <%= type %>",
|
||||
"editATask": "Edit a <%= type %>",
|
||||
@@ -25,7 +25,8 @@
|
||||
"checklist": "Checklist",
|
||||
"checklistText": "Break a task into smaller pieces! Checklists increase the Experience and Gold gained from a To-Do, and reduce the damage caused by a Daily.",
|
||||
"newChecklistItem": "New checklist item",
|
||||
"expandCollapse": "Expand/Collapse",
|
||||
"expandChecklist": "Expand Checklist",
|
||||
"collapseChecklist": "Collapse Checklist",
|
||||
"text": "Title",
|
||||
"extraNotes": "Extra Notes",
|
||||
"notes": "Notes",
|
||||
@@ -114,10 +115,11 @@
|
||||
"fortifyText": "Fortify will return all your tasks, except challenge tasks, to a neutral (yellow) state, as if you'd just added them, and top your Health off to full. This is great if all your red tasks are making the game too hard, or all your blue tasks are making the game too easy. If starting fresh sounds much more motivating, spend the Gems and catch a reprieve!",
|
||||
"confirmFortify": "Are you sure?",
|
||||
"fortifyComplete": "Fortify complete!",
|
||||
"sureDelete": "Are you sure you want to delete the <%= taskType %> with the text \"<%= taskText %>\"?",
|
||||
"sureDelete": "Are you sure you want to delete this task?",
|
||||
"sureDeleteCompletedTodos": "Are you sure you want to delete your completed todos?",
|
||||
"streakCoins": "Streak Bonus!",
|
||||
"pushTaskToTop": "Push task to top. Hold ctrl or cmd to push to bottom.",
|
||||
"taskToTop": "To top",
|
||||
"taskToBottom": "To bottom",
|
||||
"emptyTask": "Enter the task's title first.",
|
||||
"dailiesRestingInInn": "You're Resting in the Inn! Your Dailies will NOT hurt you tonight, but they WILL still refresh every day. If you're in a quest, you won't deal damage/collect items until you check out of the Inn, but you can still be injured by a Boss if your Party mates skip their own Dailies.",
|
||||
"habitHelp1": "Good Habits are things that you do often. They award Gold and Experience every time you click the <%= plusIcon %>.",
|
||||
|
||||