Merge branch 'develop' into negue/modal-notifications

This commit is contained in:
Matteo Pagliazzi
2018-10-13 21:18:16 +02:00
882 changed files with 46962 additions and 35748 deletions
@@ -70,9 +70,9 @@ export default {
facebook,
}),
tweet,
achievementLink: `${BASE_URL}/social/achievement`,
twitterLink: `https://twitter.com/intent/tweet?text=${tweet}&via=habitica&url=${BASE_URL}/social/achievement&count=none`,
facebookLink: `https://www.facebook.com/sharer/sharer.php?text=${tweet}&u=${BASE_URL}/social/achievement`,
achievementLink: `${BASE_URL}`,
twitterLink: `https://twitter.com/intent/tweet?text=${tweet}&via=habitica&url=${BASE_URL}&count=none`,
facebookLink: `https://www.facebook.com/sharer/sharer.php?text=${tweet}&u=${BASE_URL}`,
};
},
};
@@ -164,30 +164,30 @@ export default {
classGear (heroClass) {
if (heroClass === 'rogue') {
return {
armor: 'armor_rogue_5',
head: 'head_rogue_5',
shield: 'shield_rogue_6',
weapon: 'weapon_rogue_6',
armor: 'armor_special_fall2018Rogue',
head: 'head_special_fall2018Rogue',
shield: 'shield_special_fall2018Rogue',
weapon: 'weapon_special_fall2018Rogue',
};
} else if (heroClass === 'wizard') {
return {
armor: 'armor_wizard_5',
head: 'head_wizard_5',
weapon: 'weapon_wizard_6',
armor: 'armor_special_fall2018Mage',
head: 'head_special_fall2018Mage',
weapon: 'weapon_special_fall2018Mage',
};
} else if (heroClass === 'healer') {
return {
armor: 'armor_healer_5',
head: 'head_healer_5',
shield: 'shield_healer_5',
weapon: 'weapon_healer_6',
armor: 'armor_special_fall2018Healer',
head: 'head_special_fall2018Healer',
shield: 'shield_special_fall2018Healer',
weapon: 'weapon_special_fall2018Healer',
};
} else {
return {
armor: 'armor_warrior_5',
head: 'head_warrior_5',
shield: 'shield_warrior_5',
weapon: 'weapon_warrior_6',
armor: 'armor_special_fall2018Warrior',
head: 'head_special_fall2018Warrior',
shield: 'shield_special_fall2018Warrior',
weapon: 'weapon_special_fall2018Warrior',
};
}
},
+54 -3
View File
@@ -11,20 +11,20 @@
span {{registering ? $t('signUpWithSocial', {social: 'Google'}) : $t('loginWithSocial', {social: 'Google'})}}
.form-group(v-if='registering')
label(for='usernameInput', v-once) {{$t('username')}}
input#usernameInput.form-control(type='text', :placeholder='$t("usernamePlaceholder")', v-model='username')
input#usernameInput.form-control(type='text', :placeholder='$t("usernamePlaceholder")', v-model='username', :class='{"input-valid": usernameValid, "input-invalid": usernameInvalid}')
.form-group(v-if='!registering')
label(for='usernameInput', v-once) {{$t('emailOrUsername')}}
input#usernameInput.form-control(type='text', :placeholder='$t("emailOrUsername")', v-model='username')
.form-group(v-if='registering')
label(for='emailInput', v-once) {{$t('email')}}
input#emailInput.form-control(type='email', :placeholder='$t("emailPlaceholder")', v-model='email')
input#emailInput.form-control(type='email', :placeholder='$t("emailPlaceholder")', v-model='email', :class='{"input-invalid": emailInvalid, "input-valid": emailValid}')
.form-group
label(for='passwordInput', v-once) {{$t('password')}}
a.float-right.forgot-password(v-once, v-if='!registering', @click='forgotPassword = true') {{$t('forgotPassword')}}
input#passwordInput.form-control(type='password', :placeholder='$t(registering ? "passwordPlaceholder" : "password")', v-model='password')
.form-group(v-if='registering')
label(for='confirmPasswordInput', v-once) {{$t('confirmPassword')}}
input#confirmPasswordInput.form-control(type='password', :placeholder='$t("confirmPasswordPlaceholder")', v-model='passwordConfirm')
input#confirmPasswordInput.form-control(type='password', :placeholder='$t("confirmPasswordPlaceholder")', v-model='passwordConfirm', :class='{"input-invalid": passwordConfirmInvalid, "input-valid": passwordConfirmValid}')
small.form-text(v-once, v-html="$t('termsAndAgreement')")
.text-center
.btn.btn-info(@click='register()', v-if='registering', v-once) {{$t('joinHabitica')}}
@@ -49,6 +49,8 @@
.social-button {
width: 100%;
height: 100%;
white-space: inherit;
text-align: center;
.text {
@@ -69,12 +71,17 @@
small.form-text {
text-align: center;
}
.input-valid {
color: #fff;
}
}
</style>
<script>
import hello from 'hellojs';
import { setUpAxios } from 'client/libs/auth';
import debounce from 'lodash/debounce';
import facebookSquareIcon from 'assets/svg/facebook-square.svg';
import googleIcon from 'assets/svg/google.svg';
@@ -88,6 +95,7 @@ export default {
email: '',
password: '',
passwordConfirm: '',
usernameIssues: [],
};
data.icons = Object.freeze({
@@ -104,7 +112,50 @@ export default {
google: process.env.GOOGLE_CLIENT_ID, // eslint-disable-line
});
},
computed: {
emailValid () {
if (this.email.length <= 3) return false;
return this.validateEmail(this.email);
},
emailInvalid () {
return !this.emailValid;
},
usernameValid () {
if (this.username.length <= 3) return false;
return this.usernameIssues.length === 0;
},
usernameInvalid () {
return !this.usernameValid;
},
passwordConfirmValid () {
if (this.passwordConfirm.length <= 3) return false;
return this.passwordConfirm === this.password;
},
passwordConfirmInvalid () {
return !this.passwordConfirmValid;
},
},
watch: {
username () {
this.validateUsername(this.username);
},
},
methods: {
// eslint-disable-next-line func-names
validateUsername: debounce(function (username) {
if (username.length <= 3) {
return;
}
this.$store.dispatch('auth:verifyUsername', {
username: this.username,
}).then(res => {
if (res.issues !== undefined) {
this.usernameIssues = res.issues;
} else {
this.usernameIssues = [];
}
});
}, 500),
// @TODO: Abstract hello in to action or lib
async socialAuth (network) {
try {
@@ -21,23 +21,24 @@
.col-12.col-md-6
.btn.btn-secondary.social-button(@click='socialAuth("google")')
.svg-icon.social-icon(v-html="icons.googleIcon")
span {{registering ? $t('signUpWithSocial', {social: 'Google'}) : $t('loginWithSocial', {social: 'Google'})}}
.text {{registering ? $t('signUpWithSocial', {social: 'Google'}) : $t('loginWithSocial', {social: 'Google'})}}
.form-group(v-if='registering')
label(for='usernameInput', v-once) {{$t('username')}}
input#usernameInput.form-control(type='text', :placeholder='$t("usernamePlaceholder")', v-model='username')
input#usernameInput.form-control(type='text', :placeholder='$t("usernamePlaceholder")', v-model='username', :class='{"input-valid": usernameValid, "input-invalid": usernameInvalid}')
.input-error(v-for="issue in usernameIssues") {{ issue }}
.form-group(v-if='!registering')
label(for='usernameInput', v-once) {{$t('emailOrUsername')}}
input#usernameInput.form-control(type='text', :placeholder='$t("emailOrUsername")', v-model='username')
.form-group(v-if='registering')
label(for='emailInput', v-once) {{$t('email')}}
input#emailInput.form-control(type='email', :placeholder='$t("emailPlaceholder")', v-model='email')
input#emailInput.form-control(type='email', :placeholder='$t("emailPlaceholder")', v-model='email', :class='{"input-invalid": emailInvalid, "input-valid": emailValid}')
.form-group
label(for='passwordInput', v-once) {{$t('password')}}
a.float-right.forgot-password(v-once, v-if='!registering', @click='forgotPassword = true') {{$t('forgotPassword')}}
input#passwordInput.form-control(type='password', :placeholder='$t(registering ? "passwordPlaceholder" : "password")', v-model='password')
.form-group(v-if='registering')
label(for='confirmPasswordInput', v-once) {{$t('confirmPassword')}}
input#confirmPasswordInput.form-control(type='password', :placeholder='$t("confirmPasswordPlaceholder")', v-model='passwordConfirm')
input#confirmPasswordInput.form-control(type='password', :placeholder='$t("confirmPasswordPlaceholder")', v-model='passwordConfirm', :class='{"input-invalid": passwordConfirmInvalid, "input-valid": passwordConfirmValid}')
small.form-text(v-once, v-html="$t('termsAndAgreement')")
.text-center
.btn.btn-info(@click='register()', v-if='registering', v-once) {{$t('joinHabitica')}}
@@ -200,6 +201,10 @@
color: $white;
}
#usernameInput.input-invalid {
margin-bottom: 0.5em;
}
.form-text {
font-size: 14px;
color: $white;
@@ -207,6 +212,8 @@
.social-button {
width: 100%;
height: 100%;
white-space: inherit;
text-align: center;
.text {
@@ -275,11 +282,19 @@
.forgot-password {
color: #bda8ff !important;
}
.input-error {
color: #fff;
font-size: 90%;
width: 100%;
text-align: center;
}
</style>
<script>
import axios from 'axios';
import hello from 'hellojs';
import debounce from 'lodash/debounce';
import gryphon from 'assets/svg/gryphon.svg';
import habiticaIcon from 'assets/svg/habitica-logo.svg';
@@ -298,6 +313,7 @@ export default {
hasError: null,
code: null,
},
usernameIssues: [],
};
data.icons = Object.freeze({
@@ -322,6 +338,30 @@ export default {
}
return false;
},
emailValid () {
if (this.email.length <= 3) return false;
return this.validateEmail(this.email);
},
emailInvalid () {
if (this.email.length <= 3) return false;
return !this.emailValid;
},
usernameValid () {
if (this.username.length <= 3) return false;
return this.usernameIssues.length === 0;
},
usernameInvalid () {
if (this.username.length <= 3) return false;
return !this.usernameValid;
},
passwordConfirmValid () {
if (this.passwordConfirm.length <= 3) return false;
return this.passwordConfirm === this.password;
},
passwordConfirmInvalid () {
if (this.passwordConfirm.length <= 3) return false;
return !this.passwordConfirmValid;
},
},
mounted () {
hello.init({
@@ -355,8 +395,26 @@ export default {
},
immediate: true,
},
username () {
this.validateUsername(this.username);
},
},
methods: {
// eslint-disable-next-line func-names
validateUsername: debounce(function (username) {
if (username.length <= 3 || !this.registering) {
return;
}
this.$store.dispatch('auth:verifyUsername', {
username: this.username,
}).then(res => {
if (res.issues !== undefined) {
this.usernameIssues = res.issues;
} else {
this.usernameIssues = [];
}
});
}, 500),
async register () {
// @TODO do not use alert
if (!this.email) {
@@ -2,7 +2,7 @@
.row
challenge-modal(v-on:updatedChallenge='updatedChallenge')
leave-challenge-modal(:challengeId='challenge._id')
close-challenge-modal(:members='members', :challengeId='challenge._id')
close-challenge-modal(:members='members', :challengeId='challenge._id', :prize='challenge.prize')
challenge-member-progress-modal(:challengeId='challenge._id')
.col-12.col-md-8.standard-page
.row
@@ -30,7 +30,7 @@
div.category-wrap(@click.prevent="toggleCategorySelect")
span.category-select(v-if='workingChallenge.categories.length === 0') {{$t('none')}}
.category-label(v-for='category in workingChallenge.categories') {{$t(categoriesHashByKey[category])}}
.category-box(v-if="showCategorySelect")
.category-box(v-if="showCategorySelect && creating")
.form-check(
v-for="group in categoryOptions",
:key="group.key",
@@ -74,7 +74,7 @@ div
import memberSearchDropdown from 'client/components/members/memberSearchDropdown';
export default {
props: ['challengeId', 'members'],
props: ['challengeId', 'members', 'prize'],
components: {
memberSearchDropdown,
},
@@ -102,7 +102,10 @@ export default {
},
async deleteChallenge () {
if (!confirm('Are you sure you want to delete this challenge?')) return;
this.challenge = await this.$store.dispatch('challenges:deleteChallenge', {challengeId: this.challengeId});
this.challenge = await this.$store.dispatch('challenges:deleteChallenge', {
challengeId: this.challengeId,
prize: this.prize,
});
this.$router.push('/challenges/myChallenges');
},
},
@@ -14,11 +14,17 @@
button.btn.btn-secondary.create-challenge-button.float-right(@click='createChallenge()')
.svg-icon.positive-icon(v-html="icons.positiveIcon")
span(v-once) {{$t('createChallenge')}}
.row
.no-challenges.text-center.col-md-6.offset-3(v-if='!loading && filteredChallenges.length === 0')
h2(v-once) {{$t('noChallengeMatchFilters')}}
.row
.col-12.col-md-6(v-for='challenge in filteredChallenges')
challenge-item(:challenge='challenge')
.row
.col-12.text-center
.col-12.text-center(v-if='!loading && filteredChallenges.length > 0')
button.btn.btn-secondary(@click='loadMore()') {{ $t('loadMore') }}
</template>
@@ -41,6 +47,15 @@
margin-right: .5em;
}
}
.no-challenges {
color: $gray-200;
margin-top: 10em;
h2 {
color: $gray-200;
}
}
</style>
<script>
@@ -7,6 +7,7 @@
.row.header-row
.col-md-8.text-left
h1(v-once) {{$t('myChallenges')}}
h2(v-if='loading && challenges.length === 0') {{ $t('loading') }}
.col-md-4
// @TODO: implement sorting span.dropdown-label {{ $t('sortBy') }}
b-dropdown(:text="$t('sort')", right=true)
@@ -16,12 +17,16 @@
span(v-once) {{$t('createChallenge')}}
.row
.no-challenges.text-center.col-md-6.offset-3(v-if='filteredChallenges.length === 0')
.no-challenges.text-center.col-md-6.offset-3(v-if='!loading && challenges.length === 0')
.svg-icon(v-html="icons.challengeIcon")
h2(v-once) {{$t('noChallengeTitle')}}
p(v-once) {{$t('challengeDescription1')}}
p(v-once) {{$t('challengeDescription2')}}
.row
.no-challenges.text-center.col-md-6.offset-3(v-if='!loading && challenges.length > 0 && filteredChallenges.length === 0')
h2(v-once) {{$t('noChallengeMatchFilters')}}
.row
.col-12.col-md-6(v-for='challenge in filteredChallenges')
challenge-item(:challenge='challenge')
@@ -48,14 +53,15 @@
}
.no-challenges {
color: $gray-300;
color: $gray-200;
margin-top: 10em;
h2 {
color: $gray-300;
color: $gray-200;
}
.svg-icon {
color: #C3C0C7;
width: 88.7px;
margin: 1em auto;
}
@@ -84,6 +90,7 @@ export default {
challengeIcon,
positiveIcon,
}),
loading: false,
challenges: [],
sort: 'none',
sortOptions: [
@@ -113,7 +120,7 @@ export default {
};
},
mounted () {
this.loadchallanges();
this.loadChallenges();
},
computed: {
filteredChallenges () {
@@ -138,10 +145,12 @@ export default {
this.$store.state.challengeOptions.workingChallenge = {};
this.$root.$emit('bv::show::modal', 'challenge-modal');
},
async loadchallanges () {
async loadChallenges () {
this.loading = true;
this.challenges = await this.$store.dispatch('challenges:getUserChallenges', {
member: true,
});
this.loading = false;
},
challengeCreated (challenge) {
this.challenges.push(challenge);
@@ -14,7 +14,7 @@ div.autocomplete-selection(v-if='searchResults.length > 0', :style='autocomplete
import groupBy from 'lodash/groupBy';
export default {
props: ['selections', 'text', 'coords', 'chat'],
props: ['selections', 'text', 'coords', 'chat', 'textbox'],
data () {
return {
currentSearch: '',
@@ -25,9 +25,15 @@ export default {
},
computed: {
autocompleteStyle () {
function heightToUse (textBox, topCoords) {
let textBoxHeight = textBox['user-entry'].clientHeight;
return topCoords < textBoxHeight ? topCoords + 30 : textBoxHeight + 10;
}
return {
top: `${this.coords.TOP + 30}px`,
top: `${heightToUse(this.textbox, this.coords.TOP)}px`,
left: `${this.coords.LEFT + 30}px`,
marginLeft: '-28px',
marginTop: '28px',
position: 'absolute',
minWidth: '100px',
minHeight: '100px',
@@ -42,6 +48,7 @@ export default {
return option.toLowerCase().indexOf(currentSearch.toLowerCase()) !== -1;
});
},
},
mounted () {
this.grabUserNames();
+26 -20
View File
@@ -7,30 +7,31 @@ div
h3.leader(
:class='userLevelStyle(msg)',
@click="showMemberModal(msg.uuid)",
v-b-tooltip.hover.top="('contributor' in msg) ? msg.contributor.text : ''",
v-b-tooltip.hover.top="tierTitle",
)
| {{msg.user}}
.svg-icon(v-html="tierIcon", v-if='showShowTierStyle')
p.time(v-b-tooltip="", :title="msg.timestamp | date") {{msg.timestamp | timeAgo}}
.text(v-markdown='msg.text')
hr
.action(@click='like()', v-if='!inbox && msg.likes', :class='{active: msg.likes[user._id]}')
.svg-icon(v-html="icons.like")
span(v-if='!msg.likes[user._id]') {{ $t('like') }}
span(v-if='msg.likes[user._id]') {{ $t('liked') }}
span.action(v-if='!inbox', @click='copyAsTodo(msg)')
.svg-icon(v-html="icons.copy")
| {{$t('copyAsTodo')}}
span.action(v-if='!inbox && user.flags.communityGuidelinesAccepted && msg.uuid !== "system"', @click='report(msg)')
.svg-icon(v-html="icons.report")
| {{$t('report')}}
// @TODO make flagging/reporting work in the inbox. NOTE: it must work even if the communityGuidelines are not accepted and it MUST work for messages that you have SENT as well as received. -- Alys
span.action(v-if='msg.uuid === user._id || inbox || user.contributor.admin', @click='remove()')
.svg-icon(v-html="icons.delete")
| {{$t('delete')}}
span.action.float-right.liked(v-if='likeCount > 0')
.svg-icon(v-html="icons.liked")
| + {{ likeCount }}
div(v-if='msg.id')
.action(@click='like()', v-if='!inbox && msg.likes', :class='{active: msg.likes[user._id]}')
.svg-icon(v-html="icons.like")
span(v-if='!msg.likes[user._id]') {{ $t('like') }}
span(v-if='msg.likes[user._id]') {{ $t('liked') }}
span.action(v-if='!inbox', @click='copyAsTodo(msg)')
.svg-icon(v-html="icons.copy")
| {{$t('copyAsTodo')}}
span.action(v-if='!inbox && user.flags.communityGuidelinesAccepted && msg.uuid !== "system"', @click='report(msg)')
.svg-icon(v-html="icons.report")
| {{$t('report')}}
// @TODO make flagging/reporting work in the inbox. NOTE: it must work even if the communityGuidelines are not accepted and it MUST work for messages that you have SENT as well as received. -- Alys
span.action(v-if='msg.uuid === user._id || inbox || user.contributor.admin', @click='remove()')
.svg-icon(v-html="icons.delete")
| {{$t('delete')}}
span.action.float-right.liked(v-if='likeCount > 0')
.svg-icon(v-html="icons.liked")
| + {{ likeCount }}
</template>
<style lang="scss" scoped>
@@ -118,6 +119,8 @@ import markdownDirective from 'client/directives/markdown';
import { mapState } from 'client/libs/store';
import styleHelper from 'client/mixins/styleHelper';
import achievementsLib from '../../../common/script/libs/achievements';
import deleteIcon from 'assets/svg/delete.svg';
import copyIcon from 'assets/svg/copy.svg';
import likeIcon from 'assets/svg/like.svg';
@@ -220,6 +223,10 @@ export default {
}
return this.icons[`tier${message.contributor.level}`];
},
tierTitle () {
const message = this.msg;
return achievementsLib.getContribText(message.contributor, message.backer) || '';
},
},
methods: {
async like () {
@@ -254,8 +261,7 @@ export default {
this.$emit('message-removed', message);
if (this.inbox) {
axios.delete(`/api/v4/user/messages/${message.id}`);
this.$delete(this.user.inbox.messages, message.id);
await axios.delete(`/api/v4/inbox/messages/${message.id}`);
return;
}
@@ -230,6 +230,11 @@ export default {
this.chat.splice(chatIndex, 1, message);
},
messageRemoved (message) {
if (this.inbox) {
this.$emit('message-removed', message);
return;
}
const chatIndex = findIndex(this.chat, chatMessage => {
return chatMessage.id === message.id;
});
@@ -96,16 +96,10 @@ export default {
};
},
created () {
this.$root.$on('habitica::report-chat', data => {
if (!data.message || !data.groupId) return;
this.abuseObject = data.message;
this.groupId = data.groupId;
this.reportComment = '';
this.$root.$emit('bv::show::modal', 'report-flag');
});
this.$root.$on('habitica::report-chat', this.handleReport);
},
destroyed () {
this.$root.$off('habitica::report-chat');
this.$root.$off('habitica::report-chat', this.handleReport);
},
methods: {
close () {
@@ -129,6 +123,13 @@ export default {
});
this.close();
},
handleReport (data) {
if (!data.message || !data.groupId) return;
this.abuseObject = data.message;
this.groupId = data.groupId;
this.reportComment = '';
this.$root.$emit('bv::show::modal', 'report-flag');
},
},
};
</script>
+115 -54
View File
@@ -193,8 +193,10 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
.col-3.text-center.sub-menu-item(@click='changeSubPage("flower")', :class='{active: activeSubPage === "flower"}')
strong(v-once) {{$t('accent')}}
.row.sub-menu(v-if='editing')
.col-4.offset-2.text-center.sub-menu-item(@click='changeSubPage("ears")' :class='{active: activeSubPage === "ears"}')
.col-4.text-center.sub-menu-item(@click='changeSubPage("ears")' :class='{active: activeSubPage === "ears"}')
strong(v-once) {{$t('animalEars')}}
.col-4.text-center.sub-menu-item(@click='changeSubPage("tails")' :class='{active: activeSubPage === "tails"}')
strong(v-once) {{$t('animalTails')}}
.col-4.text-center.sub-menu-item(@click='changeSubPage("headband")' :class='{active: activeSubPage === "headband"}')
strong(v-once) {{$t('headband')}}
#glasses.row(v-if='activeSubPage === "glasses"')
@@ -203,17 +205,36 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
.sprite.customize-option(:class="`eyewear_special_${option.key}`", @click='option.click')
#animal-ears.row(v-if='activeSubPage === "ears"')
.section.col-12.customize-options
.option(v-for='option in animalEars',
.option(v-for='option in animalItems("headAccessory")',
:class='{active: option.active, locked: option.locked}')
.sprite.customize-option(:class="`headAccessory_special_${option.key}`", @click='option.click')
.gem-lock(v-if='option.locked')
.gem-lock(v-if='option.gemLocked')
.svg-icon.gem(v-html='icons.gem')
span 2
.col-12.text-center(v-if='!animalEarsOwned')
.gold-lock(v-if='option.goldLocked')
.svg-icon.gold(v-html='icons.gold')
span 20
.col-12.text-center(v-if='!animalItemsOwned("headAccessory")')
.gem-lock
.svg-icon.gem(v-html='icons.gem')
span 5
button.btn.btn-secondary.purchase-all(@click='unlock(animalEarsUnlockString)') {{ $t('purchaseAll') }}
button.btn.btn-secondary.purchase-all(@click='unlock(animalItemsUnlockString("headAccessory"))') {{ $t('purchaseAll') }}
#animal-tails.row(v-if='activeSubPage === "tails"')
.section.col-12.customize-options
.option(v-for='option in animalItems("back")',
:class='{active: option.active, locked: option.locked}')
.sprite.customize-option(:class="`icon_back_special_${option.key}`", @click='option.click')
.gem-lock(v-if='option.gemLocked')
.svg-icon.gem(v-html='icons.gem')
span 2
.gold-lock(v-if='option.goldLocked')
.svg-icon.gold(v-html='icons.gold')
span 20
.col-12.text-center(v-if='!animalItemsOwned("back")')
.gem-lock
.svg-icon.gem(v-html='icons.gem')
span 5
button.btn.btn-secondary.purchase-all(@click='unlock(animalItemsUnlockString("back"))') {{ $t('purchaseAll') }}
#headband.row(v-if='activeSubPage === "headband"')
.col-12.customize-options
.option(v-for='option in headbands', :class='{active: option.active}')
@@ -570,20 +591,21 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
}
}
.text-center .gem-lock {
display: inline-block;
margin-right: 1em;
margin-bottom: 1.6em;
vertical-align: bottom;
.text-center {
.gem-lock, .gold-lock {
display: inline-block;
margin-right: 1em;
margin-bottom: 1.6em;
vertical-align: bottom;
}
}
.gem-lock {
.gem-lock, .gold-lock {
.svg-icon {
width: 16px;
}
span {
color: #24cc8f;
font-weight: bold;
margin-left: .5em;
}
@@ -594,6 +616,14 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
}
}
.gem-lock span {
color: $green-10
}
.gold-lock span {
color: $yellow-10
}
.option.active {
border-color: $purple-200;
}
@@ -710,7 +740,7 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
color: #24cc8f;
}
.gem {
.gem, .coin {
width: 16px;
}
@@ -725,13 +755,13 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
font-size: 14px;
}
.gem {
.gem, .coin {
width: 20px;
}
}
}
.gem {
.gem, .coin {
margin: 0 .5em;
display: inline-block;
vertical-align: bottom;
@@ -837,6 +867,7 @@ import { mapState } from 'client/libs/store';
import avatar from './avatar';
import { getBackgroundShopSets } from '../../common/script/libs/shops';
import unlock from '../../common/script/ops/unlock';
import buy from '../../common/script/ops/buy/buy';
import guide from 'client/mixins/guide';
import notifications from 'client/mixins/notifications';
import appearance from 'common/script/content/appearance';
@@ -850,6 +881,7 @@ import skinIcon from 'assets/svg/skin.svg';
import hairIcon from 'assets/svg/hair.svg';
import backgroundsIcon from 'assets/svg/backgrounds.svg';
import gem from 'assets/svg/gem.svg';
import gold from 'assets/svg/gold.svg';
import pin from 'assets/svg/pin.svg';
import isPinned from 'common/script/libs/isPinned';
@@ -1013,7 +1045,10 @@ export default {
baseHair4Keys: [15, 16, 17, 18, 19, 20],
baseHair5Keys: [1, 2],
baseHair6Keys: [1, 2, 3],
animalEarsKeys: ['bearEars', 'cactusEars', 'foxEars', 'lionEars', 'pandaEars', 'pigEars', 'tigerEars', 'wolfEars'],
animalItemKeys: {
back: ['bearTail', 'cactusTail', 'foxTail', 'lionTail', 'pandaTail', 'pigTail', 'tigerTail', 'wolfTail'],
headAccessory: ['bearEars', 'cactusEars', 'foxEars', 'lionEars', 'pandaEars', 'pigEars', 'tigerEars', 'wolfEars'],
},
chairKeys: ['black', 'blue', 'green', 'pink', 'red', 'yellow', 'handleless_black', 'handleless_blue', 'handleless_green', 'handleless_pink', 'handleless_red', 'handleless_yellow'],
icons: Object.freeze({
logoPurple,
@@ -1024,6 +1059,7 @@ export default {
backgroundsIcon,
gem,
pin,
gold,
}),
modalPage: 1,
activeTopPage: 'body',
@@ -1075,44 +1111,6 @@ export default {
});
return options;
},
animalEarsUnlockString () {
let animalItemKeys = this.animalEarsKeys.map(key => {
return `items.gear.owned.headAccessory_special_${key}`;
});
return animalItemKeys.join(',');
},
animalEarsOwned () {
// @TODO: For some resonse when I use $set on the user purchases object, this is not recomputed. Hack for now
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
let own = true;
this.animalEarsKeys.forEach(key => {
if (!this.user.items.gear.owned[`headAccessory_special_${key}`]) own = false;
});
return own;
},
animalEars () {
// @TODO: For some resonse when I use $set on the user purchases object, this is not recomputed. Hack for now
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
let keys = this.animalEarsKeys;
let options = keys.map(key => {
let newKey = `headAccessory_special_${key}`;
let userPurchased = this.user.items.gear.owned[newKey];
let locked = !userPurchased;
let option = {};
option.key = key;
option.active = this.user.preferences.costume ? this.user.items.gear.costume.headAccessory === newKey : this.user.items.gear.equipped.headAccessory === newKey;
option.locked = locked;
option.click = () => {
let type = this.user.preferences.costume ? 'costume' : 'equipped';
return locked ? this.unlock(`items.gear.owned.${newKey}`) : this.equip(newKey, type);
};
return option;
});
return options;
},
specialShirts () {
// @TODO: For some resonse when I use $set on the user purchases object, this is not recomputed. Hack for now
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
@@ -1525,6 +1523,24 @@ export default {
alert(e.message);
}
},
async buy (item) {
const options = {
currency: 'gold',
key: item,
type: 'marketGear',
quantity: 1,
pinType: 'marketGear',
};
await axios.post(`/api/v4/user/buy/${item}`, options);
try {
buy(this.user, {
params: options,
});
this.backgroundUpdate = new Date();
} catch (e) {
alert(e.message);
}
},
setKeys (type, _set) {
return map(_set, (v, k) => {
if (type === 'background') k = v.key;
@@ -1550,6 +1566,51 @@ export default {
backgroundPurchased () {
this.backgroundUpdate = new Date();
},
animalItemsUnlockString (category) {
const keys = this.animalItemKeys[category].map(key => {
return `items.gear.owned.${category}_special_${key}`;
});
return keys.join(',');
},
animalItemsOwned (category) {
// @TODO: For some resonse when I use $set on the user purchases object, this is not recomputed. Hack for now
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
let own = true;
this.animalItemKeys[category].forEach(key => {
if (this.user.items.gear.owned[`${category}_special_${key}`] === undefined) own = false;
});
return own;
},
animalItems (category) {
// @TODO: For some resonse when I use $set on the user purchases object, this is not recomputed. Hack for now
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
let keys = this.animalItemKeys[category];
let options = keys.map(key => {
let newKey = `${category}_special_${key}`;
let userPurchased = this.user.items.gear.owned[newKey];
let option = {};
option.key = key;
option.active = this.user.preferences.costume ? this.user.items.gear.costume[category] === newKey : this.user.items.gear.equipped[category] === newKey;
option.gemLocked = userPurchased === undefined;
option.goldLocked = userPurchased === false;
option.locked = option.gemLocked || option.goldLocked;
option.click = () => {
if (option.gemLocked) {
return this.unlock(`items.gear.owned.${newKey}`);
} else if (option.goldLocked) {
return this.buy(newKey);
} else {
let type = this.user.preferences.costume ? 'costume' : 'equipped';
return this.equip(newKey, type);
}
};
return option;
});
return options;
},
},
};
</script>
+4 -1
View File
@@ -6,6 +6,7 @@
.row
textarea(:placeholder='placeholder',
v-model='newMessage',
ref='user-entry',
:class='{"user-entry": newMessage}',
@keydown='updateCarretPosition',
@keyup.ctrl.enter='sendMessageShortcut()',
@@ -16,6 +17,7 @@
autocomplete(
:text='newMessage',
v-on:select="selectedAutocomplete",
:textbox='textbox',
:coords='coords',
:chat='group.chat')
@@ -62,6 +64,7 @@
TOP: 0,
LEFT: 0,
},
textbox: this.$refs,
};
},
computed: {
@@ -187,7 +190,7 @@
position: relative;
textarea {
height: 150px;
min-height: 150px;
width: 100%;
background-color: $white;
border: solid 1px $gray-400;
@@ -14,6 +14,11 @@
button.btn.btn-secondary.create-group-button.float-right(@click='createGroup()')
.svg-icon.positive-icon(v-html="icons.positiveIcon")
span(v-once) {{$t('createGuild2')}}
.row
.no-guilds.text-center.col-md-6.offset-md-3(v-if='!loading && filteredGuilds.length === 0')
h2(v-once) {{$t('noGuildsMatchFilters')}}
.row
.col-md-12
public-guild-item(v-for="guild in filteredGuilds", :key='guild._id', :guild="guild", :display-leave='true')
@@ -39,6 +44,15 @@
display: inline-block;
margin-right: .5em;
}
.no-guilds {
color: $gray-200;
margin-top: 10em;
h2 {
color: $gray-200;
}
}
</style>
<script>
+1 -6
View File
@@ -62,7 +62,7 @@
p(v-markdown='group.description')
sidebar-section(
:title="$t('challenges')",
:tooltip="isParty ? $t('challengeDetails') : $t('privateDescription')"
:tooltip="$t('challengeDetails')"
)
group-challenges(:groupId='searchId')
div.text-center
@@ -478,11 +478,6 @@ export default {
return n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupId;
});
},
deleteAllMessages () {
if (confirm(this.$t('confirmDeleteAllMessages'))) {
// User.clearPMs();
}
},
checkForAchievements () {
// Checks if user's party has reached 2 players for the first time.
if (!this.user.achievements.partyUp && this.group.memberCount >= 2) {
@@ -33,29 +33,29 @@ div
.col-1.actions
b-dropdown(right=true)
.svg-icon.inline.dots(slot='button-content', v-html="icons.dots")
b-dropdown-item(@click='removeMember(member, index)', v-if='isLeader')
span.dropdown-icon-item
.svg-icon.inline(v-html="icons.removeIcon", v-if='isLeader')
span.text {{$t('removeMember')}}
b-dropdown-item(@click='sendMessage(member)')
span.dropdown-icon-item
.svg-icon.inline(v-html="icons.messageIcon")
span.text {{$t('sendMessage')}}
b-dropdown-item(@click='promoteToLeader(member)', v-if='shouldShowPromoteToLeader')
b-dropdown-item(@click='promoteToLeader(member)', v-if='shouldShowLeaderFunctions(member._id)')
span.dropdown-icon-item
.svg-icon.inline(v-html="icons.starIcon")
span.text {{$t('promoteToLeader')}}
b-dropdown-item(@click='addManager(member._id)', v-if='isLeader && groupIsSubscribed')
b-dropdown-item(@click='addManager(member._id)', v-if='shouldShowAddManager(member._id)')
span.dropdown-icon-item
.svg-icon.inline(v-html="icons.starIcon")
span.text {{$t('addManager')}}
b-dropdown-item(@click='removeManager(member._id)', v-if='isLeader && groupIsSubscribed')
b-dropdown-item(@click='removeManager(member._id)', v-if='shouldShowRemoveManager(member._id)')
span.dropdown-icon-item
.svg-icon.inline(v-html="icons.removeIcon")
span.text {{$t('removeManager2')}}
b-dropdown-item(@click='viewProgress(member)', v-if='challengeId')
span.dropdown-icon-item
span.text {{ $t('viewProgress') }}
b-dropdown-item(@click='removeMember(member, index)', v-if='shouldShowLeaderFunctions(member._id)')
span.dropdown-icon-item
.svg-icon.inline(v-html="icons.removeIcon")
span.text {{$t('removeMember')}}
.row(v-if='isLoadMoreAvailable')
.col-12.text-center
button.btn.btn-secondary(@click='loadMoreMembers()') {{ $t('loadMore') }}
@@ -295,9 +295,6 @@ export default {
},
computed: {
...mapState({user: 'user.data'}),
shouldShowPromoteToLeader () {
return !this.challengeId && (this.isLeader || this.isAdmin);
},
isLeader () {
if (!this.group || !this.group.leader) return false;
return this.user._id === this.group.leader || this.user._id === this.group.leader._id;
@@ -338,6 +335,9 @@ export default {
// @TOOD: We might not need this since groupId is computed now
this.getMembers();
},
challengeId () {
this.getMembers();
},
group () {
this.getMembers();
},
@@ -377,9 +377,7 @@ export default {
this.invites = invites;
}
if (this.$store.state.memberModalOptions.viewingMembers.length > 0) {
this.members = this.$store.state.memberModalOptions.viewingMembers;
}
this.members = this.$store.state.memberModalOptions.viewingMembers;
},
async clickMember (uid, forceShow) {
let user = this.$store.state.user.data;
@@ -497,6 +495,17 @@ export default {
progressMemberId: member._id,
});
},
shouldShowAddManager (memberId) {
if (memberId === this.group.leader || memberId === this.group.leader._id) return false;
return !(this.group.managers && this.group.managers[memberId]);
},
shouldShowRemoveManager (memberId) {
if (!this.isLeader && !this.isAdmin) return false;
return this.group.managers && this.group.managers[memberId];
},
shouldShowLeaderFunctions (memberId) {
return !this.challengeId && (this.isLeader || this.isAdmin) && this.user._id !== memberId;
},
},
};
</script>
+22 -30
View File
@@ -2,18 +2,11 @@
.row
sidebar(v-on:search="updateSearch", v-on:filter="updateFilters")
.no-guilds.standard-page(v-if='filteredGuilds.length === 0')
.no-guilds-wrapper
.svg-icon(v-html='icons.greyBadge')
h2 {{$t('noGuildsTitle')}}
p {{$t('noGuildsParagraph1')}}
p {{$t('noGuildsParagraph2')}}
span(v-if='loading') {{ $t('loading') }}
.standard-page(v-if='filteredGuilds.length > 0')
.standard-page
.row
.col-md-8
h1.page-header.float-left(v-once) {{ $t('myGuilds') }}
.col-md-8.text-left
h1.page-header(v-once) {{ $t('myGuilds') }}
h2(v-if='loading && guilds.length === 0') {{ $t('loading') }}
.col-4
button.btn.btn-secondary.create-group-button.float-right(@click='createGroup()')
.svg-icon.positive-icon(v-html="icons.positiveIcon")
@@ -22,6 +15,18 @@
span.dropdown-label {{ $t('sortBy') }}
b-dropdown(:text="$t('sort')", right=true)
b-dropdown-item(v-for='sortOption in sortOptions', :key="sortOption.value", @click='sort(sortOption.value)') {{sortOption.text}}
.row
.no-guilds.text-center.col-md-6.offset-md-3(v-if='!loading && guilds.length === 0')
.svg-icon(v-html='icons.greyBadge')
h2(v-once) {{$t('noGuildsTitle')}}
p(v-once) {{$t('noGuildsParagraph1')}}
p(v-once) {{$t('noGuildsParagraph2')}}
.row
.no-guilds.text-center.col-md-6.offset-md-3(v-if='!loading && guilds.length > 0 && filteredGuilds.length === 0')
h2(v-once) {{$t('noGuildsMatchFilters')}}
.row
.col-md-12
public-guild-item(v-for="guild in filteredGuilds", :key='guild._id', :guild="guild", :display-gem-bank='true')
@@ -41,29 +46,16 @@
}
.no-guilds {
text-align: center;
color: $gray-200;
margin-top: 15em;
margin-top: 10em;
p {
font-size: 14px;
line-height: 1.43;
h2 {
color: $gray-200;
}
.no-guilds-wrapper {
width: 400px;
margin: 0 auto;
.svg-icon {
width: 60px;
margin: 0 auto;
}
}
}
@media only screen and (max-width: 768px) {
.no-guilds-wrapper {
width: 100% !important;
.svg-icon {
width: 88.7px;
margin: 1em auto;
}
}
</style>
@@ -139,8 +139,6 @@ export default {
this.emitFilters();
},
searchTerm: throttle(function searchTerm (newSearch) {
if (newSearch.length <= 1) return; // @TODO: eh, should we limit based on length?
this.$emit('search', {
searchTerm: newSearch,
});
+14 -29
View File
@@ -35,26 +35,11 @@ div
span.small-text(v-html="$t('inviteFriendsParty')")
br
button.btn.btn-primary(@click='createOrInviteParty()') {{ user.party._id ? $t('inviteFriends') : $t('startAParty') }}
a.useMobileApp(v-if="isAndroidMobile()", v-once, href="https://play.google.com/store/apps/details?id=com.habitrpg.android.habitica") {{ $t('useMobileApps') }}
a.useMobileApp(v-if="isIOSMobile()", v-once, href="https://itunes.apple.com/us/app/habitica-gamified-task-manager/id994882113?mt=8") {{ $t('useMobileApps') }}
</template>
<style lang="scss" scoped>
@import '~client/assets/scss/colors.scss';
.useMobileApp {
background: red;
color: white;
z-index: 10;
width: 100%;
margin: 10px 5px 0 0;
height: 64px;
text-align: center;
display: flex;
align-items: center;
}
#app-header {
padding-left: 24px;
padding-top: 9px;
@@ -155,12 +140,6 @@ export default {
...mapActions({
getPartyMembers: 'party:getMembers',
}),
isAndroidMobile () {
return navigator.userAgent.match(/Android/i);
},
isIOSMobile () {
return navigator.userAgent.match(/iPhone|iPad|iPod/i);
},
expandMember (memberId) {
if (this.expandedMember === memberId) {
this.expandedMember = null;
@@ -175,11 +154,13 @@ export default {
this.$root.$emit('bv::show::modal', 'create-party-modal');
}
},
showPartyMembers () {
async showPartyMembers () {
const party = await this.$store.dispatch('party:getParty');
this.$root.$emit('habitica:show-member-modal', {
groupId: this.user.party._id,
groupId: party.data._id,
viewingMembers: this.partyMembers,
group: this.user.party,
group: party.data,
});
},
setPartyMembersWidth ($event) {
@@ -192,12 +173,16 @@ export default {
if (this.user.party && this.user.party._id) {
this.$store.state.memberModalOptions.groupId = this.user.party._id;
this.getPartyMembers();
this.$root.$on('inviteModal::inviteToGroup', (group) => {
this.inviteModalGroup = group;
this.$root.$emit('bv::show::modal', 'invite-modal');
});
}
},
mounted () {
this.$root.$on('inviteModal::inviteToGroup', (group) => {
this.inviteModalGroup = group;
this.$root.$emit('bv::show::modal', 'invite-modal');
});
},
destroyed () {
this.$root.off('inviteModal::inviteToGroup');
},
};
</script>
+3 -2
View File
@@ -396,12 +396,13 @@ export default {
toggleUserDropdown () {
this.isUserDropdownOpen = !this.isUserDropdownOpen;
},
sync () {
async sync () {
this.$root.$emit('habitica::resync-requested');
return Promise.all([
await Promise.all([
this.$store.dispatch('user:fetch', {forceLoad: true}),
this.$store.dispatch('tasks:fetchUserTasks', {forceLoad: true}),
]);
this.$root.$emit('habitica::resync-completed');
},
async getUserGroupPlans () {
this.$store.state.groupPlans = await this.$store.dispatch('guilds:getGroupPlans');
@@ -61,4 +61,4 @@ export default {
},
};
</script>
</script>
@@ -0,0 +1,74 @@
<template lang="pug">
base-notification(
:can-remove="false",
:has-icon="false",
:read-after-click="false",
:notification="{}",
@click="action",
)
div.text-center(slot="content")
div.username-notification-title {{ $t('setUsernameNotificationTitle') }}
div {{ $t('setUsernameNotificationBody') }}
div.current-username-container.mx-auto
label.font-weight-bold {{ $t('currentUsername') + " " }}
label @
label {{ user.auth.local.username }}
.notifications-buttons
.btn.btn-small.btn-secondary(@click.stop="changeUsername()") {{ $t('goToSettings') }}
</template>
<style lang='scss'>
@import '../../../assets/scss/colors.scss';
.username-notification-title {
font-size: 16px;
margin-bottom: 8px;
font-weight: bold;
color: $purple-300;
}
.current-username-container {
border-radius: 2px;
background-color: #f9f9f9;
border: solid 1px #e1e0e3;
padding: 8px 16px 8px 16px;
display: inline-block;
margin-top: 16px;
margin-bottom: 16px;
label {
display: inline;
}
.notification-buttons {
display: inline-block;
}
}
</style>
<script>
import BaseNotification from './base';
import { mapState } from 'client/libs/store';
import axios from 'axios';
export default {
props: ['notification'],
components: {
BaseNotification,
},
computed: {
...mapState({user: 'user.data'}),
},
methods: {
action () {
this.$router.push({ name: 'site' });
},
async confirmUsername () {
await axios.put('/api/v4/user/auth/update-username', {username: this.user.auth.local.username});
},
changeUsername () {
this.$router.push({ name: 'site' });
},
},
};
</script>
@@ -93,6 +93,7 @@ import CARD_RECEIVED from './notifications/cardReceived';
import NEW_INBOX_MESSAGE from './notifications/newInboxMessage';
import NEW_CHAT_MESSAGE from './notifications/newChatMessage';
import WORLD_BOSS from './notifications/worldBoss';
import VERIFY_USERNAME from './notifications/verifyUsername';
export default {
components: {
@@ -105,6 +106,7 @@ export default {
UNALLOCATED_STATS_POINTS, NEW_MYSTERY_ITEMS, CARD_RECEIVED,
NEW_INBOX_MESSAGE, NEW_CHAT_MESSAGE,
WorldBoss: WORLD_BOSS,
VERIFY_USERNAME,
},
data () {
return {
@@ -127,6 +129,7 @@ export default {
'QUEST_INVITATION', 'GROUP_TASK_APPROVAL', 'GROUP_TASK_APPROVED',
'NEW_MYSTERY_ITEMS', 'CARD_RECEIVED',
'NEW_INBOX_MESSAGE', 'NEW_CHAT_MESSAGE', 'UNALLOCATED_STATS_POINTS',
'VERIFY_USERNAME',
],
};
},
@@ -179,6 +182,16 @@ export default {
});
}
if (this.user.flags.verifiedUsername !== true) {
notifications.push({
type: 'VERIFY_USERNAME',
data: {
username: this.user.auth.local.username,
},
id: 'custom-change-username',
});
}
const orderMap = this.notificationsOrder;
// Push the notifications stored in user.notifications
@@ -405,6 +405,8 @@ export default {
this.currentDraggingEgg = egg;
this.eggClickMode = true;
// Wait for the div.eggInfo.mouse node to be added to the DOM before
// changing its position.
this.$nextTick(() => {
this.mouseMoved(lastMouseMoveEvent);
});
@@ -427,6 +429,8 @@ export default {
this.currentDraggingPotion = potion;
this.potionClickMode = true;
// Wait for the div.hatchingPotionInfo.mouse node to be added to the
// DOM before changing its position.
this.$nextTick(() => {
this.mouseMoved(lastMouseMoveEvent);
});
@@ -471,6 +475,11 @@ export default {
},
mouseMoved ($event) {
// Keep track of the last mouse position even in click mode so that we
// know where to position the dragged potion/egg info on item click.
lastMouseMoveEvent = $event;
// Update the potion/egg popover if we are already dragging it.
if (this.potionClickMode) {
// dragging potioninfo is 180px wide (90 would be centered)
this.$refs.clickPotionInfo.style.left = `${$event.x - 60}px`;
@@ -479,8 +488,6 @@ export default {
// dragging eggInfo is 180px wide (90 would be centered)
this.$refs.clickEggInfo.style.left = `${$event.x - 60}px`;
this.$refs.clickEggInfo.style.top = `${$event.y + 10}px`;
} else {
lastMouseMoveEvent = $event;
}
},
},
@@ -17,7 +17,7 @@ div
triggers="hover",
placement="top",
)
h4.popover-content-title {{ item.text() }}
h4.popover-content-title {{ itemName || item.text() }}
div.popover-content-text(v-html="item.notes()")
</template>
@@ -45,6 +45,9 @@ export default {
itemContentClass: {
type: String,
},
itemName: {
type: String,
},
active: {
type: Boolean,
},
@@ -145,47 +145,17 @@
.btn.btn-flat.btn-show-more(@click="setShowMore(mountGroup.key)", v-if='mountGroup.key !== "specialMounts"')
| {{ $_openedItemRows_isToggled(mountGroup.key) ? $t('showLess') : $t('showMore') }}
drawer(:title="$t('quickInventory')",
:errorMessage="(!hasDrawerTabItems(selectedDrawerTab)) ? ((selectedDrawerTab === 0) ? $t('noFoodAvailable') : $t('noSaddlesAvailable')) : null")
div(slot="drawer-header")
.drawer-tab-container
.drawer-tab.text-right
a.drawer-tab-text(
@click="selectedDrawerTab = 0",
:class="{'drawer-tab-text-active': selectedDrawerTab === 0}",
) {{ drawerTabs[0].label }}
.clearfix
.drawer-tab.float-left
a.drawer-tab-text(
@click="selectedDrawerTab = 1",
:class="{ 'drawer-tab-text-active': selectedDrawerTab === 1 }",
) {{ drawerTabs[1].label }}
#petLikeToEatStable.drawer-help-text(v-once)
| {{ $t('petLikeToEat') + ' ' }}
span.svg-icon.inline.icon-16(v-html="icons.information")
b-popover(
target="petLikeToEatStable"
placement="top"
)
.popover-content-text(v-html="$t('petLikeToEatText')", v-once)
drawer-slider(
:items="drawerTabs[selectedDrawerTab].items",
:scrollButtonsVisible="hasDrawerTabItems(selectedDrawerTab)",
slot="drawer-slider",
:itemWidth=94,
:itemMargin=24,
:itemType="selectedDrawerTab"
)
template(slot="item", slot-scope="context")
foodItem(
:item="context.item",
:itemCount="userItems.food[context.item.key]",
:active="currentDraggingFood == context.item",
@itemDragEnd="onDragEnd()",
@itemDragStart="onDragStart($event, context.item)",
@itemClick="onFoodClicked($event, context.item)"
)
inventoryDrawer
template(slot="item", slot-scope="ctx")
foodItem(
:item="ctx.item",
:itemCount="ctx.itemCount",
:itemContentClass="ctx.itemClass",
:active="currentDraggingFood === ctx.item",
@itemDragEnd="onDragEnd()",
@itemDragStart="onDragStart($event, ctx.item)",
@itemClick="onFoodClicked($event, ctx.item)"
)
hatchedPetDialog(:hideText="true")
div.foodInfo(ref="dragginFoodInfo")
div(v-if="currentDraggingFood != null")
@@ -284,9 +254,6 @@
}
}
.drawer-slider .items {
height: 114px;
}
.modal-backdrop.fade.show {
background-color: $purple-50;
@@ -386,6 +353,7 @@
import StarBadge from 'client/components/ui/starBadge';
import CountBadge from 'client/components/ui/countBadge';
import DrawerSlider from 'client/components/ui/drawerSlider';
import InventoryDrawer from 'client/components/shared/inventoryDrawer';
import ResizeDirective from 'client/directives/resize.directive';
import DragDropDirective from 'client/directives/dragdrop.directive';
@@ -399,6 +367,8 @@
import openedItemRowsMixin from 'client/mixins/openedItemRows';
import petMixin from 'client/mixins/petMixin';
import { CONSTANTS, setLocalSetting, getLocalSetting } from 'client/libs/userlocalManager';
// TODO Normalize special pets and mounts
// import Store from 'client/store';
// import deepFreeze from 'client/libs/deepFreeze';
@@ -423,6 +393,7 @@
MountRaisedModal,
WelcomeModal,
HatchingModal,
InventoryDrawer,
},
directives: {
resize: ResizeDirective,
@@ -430,13 +401,15 @@
mousePosition: MouseMoveDirective,
},
data () {
const stableSortState = getLocalSetting(CONSTANTS.keyConstants.STABLE_SORT_STATE) || 'standard';
return {
viewOptions: {},
hideMissing: false,
searchText: null,
searchTextThrottled: '',
// sort has the translation-keys as values
selectedSortBy: 'standard',
selectedSortBy: stableSortState,
sortByItems: [
'standard',
'AZ',
@@ -461,6 +434,11 @@
let search = this.searchText.toLowerCase();
this.searchTextThrottled = search;
}, 250),
selectedSortBy: {
handler () {
setLocalSetting(CONSTANTS.keyConstants.STABLE_SORT_STATE, this.selectedSortBy);
},
},
},
computed: {
...mapState({
@@ -849,11 +827,12 @@
}
},
mouseMoved ($event) {
// Keep track of the last mouse position even in click mode so that we
// know where to position the dragged food icon on click.
lastMouseMoveEvent = $event;
if (this.foodClickMode) {
this.$refs.clickFoodInfo.style.left = `${$event.x - 70}px`;
this.$refs.clickFoodInfo.style.top = `${$event.y}px`;
} else {
lastMouseMoveEvent = $event;
}
},
},
+41 -19
View File
@@ -221,10 +221,8 @@ export default {
...mapState({
user: 'user.data',
userHp: 'user.data.stats.hp',
userExp: 'user.data.stats.exp',
userGp: 'user.data.stats.gp',
userMp: 'user.data.stats.mp',
userLvl: 'user.data.stats.lvl',
userNotifications: 'user.data.notifications',
userAchievements: 'user.data.achievements', // @TODO: does this watch deeply?
armoireEmpty: 'user.data.flags.armoireEmpty',
@@ -233,9 +231,15 @@ export default {
userClassSelect () {
return !this.user.flags.classSelected && this.user.stats.lvl >= 10;
},
userHasClass () {
return this.$store.getters['members:hasClass'](this.user);
},
invitedToQuest () {
return this.user.party.quest.RSVPNeeded && !this.user.party.quest.completed;
},
userExpAndLvl () {
return [this.user.stats.exp, this.user.stats.lvl];
},
},
watch: {
userHp (after, before) {
@@ -253,16 +257,6 @@ export default {
if (after < 0) this.playSound('Minus_Habit');
},
userExp (after, before) {
if (after === before) return;
if (this.user.stats.lvl === 0) return;
let exp = after - before;
if (exp < -50) { // recalculate exp if user level up
exp = toNextLevel(this.user.stats.lvl - 1) - before + after;
}
this.exp(exp);
},
userGp (after, before) {
if (after === before) return;
if (this.user.stats.lvl === 0) return;
@@ -283,15 +277,11 @@ export default {
},
userMp (after, before) {
if (after === before) return;
if (!this.$store.getters['members:hasClass'](this.user)) return;
if (!this.userHasClass) return;
let mana = after - before;
const mana = after - before;
this.mp(mana);
},
userLvl (after, before) {
if (after <= before || this.$store.state.isRunningYesterdailies) return;
this.showLevelUpNotifications(after);
},
userClassSelect (after) {
if (this.user.needsCron) return;
if (!after) return;
@@ -315,6 +305,9 @@ export default {
if (after !== true) return;
this.$root.$emit('bv::show::modal', 'quest-invitation');
},
userExpAndLvl (after, before) {
this.displayUserExpAndLvlNotifications(after[0], before[0], after[1], before[1]);
},
},
mounted () {
Promise.all([
@@ -385,6 +378,35 @@ export default {
debounceCheckUserAchievements: debounce(function debounceCheck () {
this.checkUserAchievements();
}, 700),
displayUserExpAndLvlNotifications (afterExp, beforeExp, afterLvl, beforeLvl) {
if (afterExp === beforeExp && afterLvl === beforeLvl) return;
// XP evaluation
if (afterExp !== beforeExp) {
if (this.user.stats.lvl === 0) return;
const lvlUps = afterLvl - beforeLvl;
let exp = afterExp - beforeExp;
if (lvlUps > 0) {
let level = Math.trunc(beforeLvl);
exp += toNextLevel(level);
// loop if more than 1 lvl up
for (let i = 1; i < lvlUps; i += 1) {
level += 1;
exp += toNextLevel(level);
}
}
this.exp(exp);
}
// Lvl evaluation
if (afterLvl !== beforeLvl) {
if (afterLvl <= beforeLvl || this.$store.state.isRunningYesterdailies) return;
this.showLevelUpNotifications(afterLvl);
}
},
checkUserAchievements () {
if (this.user.needsCron) return;
@@ -560,7 +582,7 @@ export default {
case 'CRON':
if (notification.data) {
if (notification.data.hp) this.hp(notification.data.hp, 'hp');
if (notification.data.mp) this.mp(notification.data.mp);
if (notification.data.mp && this.userHasClass) this.mp(notification.data.mp);
}
break;
case 'SCORED_TASK':
@@ -41,7 +41,11 @@ b-modal#send-gems(:title="title", :hide-footer="true", size='lg', @hide='onHide(
//include ../formatting-help
.modal-footer
button.btn.btn-primary(v-if='fromBal', @click='sendGift()') {{ $t("send") }}
button.btn.btn-primary(
v-if="fromBal",
@click="sendGift()",
:disabled="sendingInProgress"
) {{ $t("send") }}
template(v-else)
button.btn.btn-primary(@click='showStripe({gift, uuid: userReceivingGems._id})') {{ $t('card') }}
button.btn.btn-warning(@click='openPaypalGift({gift: gift, giftedTo: userReceivingGems._id})') PayPal
@@ -103,6 +107,7 @@ export default {
assistanceEmailObject: {
hrefTechAssistanceEmail: `<a href="mailto:${TECH_ASSISTANCE_EMAIL}">${TECH_ASSISTANCE_EMAIL}</a>`,
},
sendingInProgress: false,
};
},
computed: {
@@ -130,6 +135,7 @@ export default {
methods: {
// @TODO move to payments mixin or action (problem is that we need notifications)
async sendGift () {
this.sendingInProgress = true;
await this.$store.dispatch('members:transferGems', {
message: this.gift.message,
toUserId: this.userReceivingGems._id,
@@ -139,7 +145,9 @@ export default {
this.close();
},
onHide () {
this.gift.gems.amount = 0;
this.gift.message = '';
this.sendingInProgress = false;
},
close () {
this.$root.$emit('bv::hide::modal', 'send-gems');
@@ -40,7 +40,7 @@ export default {
...mapState({user: 'user.data', credentials: 'credentials'}),
getCodesUrl () {
if (!this.user) return '';
return `/api/v4/coupons?_id=${this.user._id}&apiToken=${this.credentials.API_TOKEN}`;
return '/api/v4/coupons';
},
},
methods: {
+126 -36
View File
@@ -4,7 +4,7 @@
reset-modal
delete-modal
h1.col-12 {{ $t('settings') }}
.col-6
.col-sm-6
.form-horizontal
h5 {{ $t('language') }}
select.form-control(:value='user.preferences.language',
@@ -105,7 +105,7 @@
p(v-html="$t('timezoneUTC', {utc: timezoneOffsetToUtc})")
p(v-html="$t('timezoneInfo')")
.col-6
.col-sm-6
h2 {{ $t('registration') }}
.panel-body
div
@@ -115,13 +115,9 @@
button.btn.btn-primary.mb-2(disabled='disabled', v-if='!hasBackupAuthOption(network.key) && user.auth[network.key].id') {{ $t('registeredWithSocial', {network: network.name}) }}
button.btn.btn-danger(@click='deleteSocialAuth(network)', v-if='hasBackupAuthOption(network.key) && user.auth[network.key].id') {{ $t('detachSocial', {network: network.name}) }}
hr
div(v-if='!user.auth.local.username')
div(v-if='!user.auth.local.email')
p {{ $t('addLocalAuth') }}
p {{ $t('usernameLimitations') }}
.form(name='localAuth', novalidate)
//-.alert.alert-danger(ng-messages='changeUsername.$error && changeUsername.submitted') {{ $t('fillAll') }}
.form-group
input.form-control(type='text', :placeholder="$t('username')", v-model='localAuth.username', required)
.form-group
input.form-control(type='text', :placeholder="$t('email')", v-model='localAuth.email', required)
.form-group
@@ -130,37 +126,36 @@
input.form-control(type='password', :placeholder="$t('confirmPass')", v-model='localAuth.confirmPassword', required)
button.btn.btn-primary(type='submit', @click='addLocalAuth()') {{ $t('submit') }}
.usersettings(v-if='user.auth.local.username')
p {{ $t('username') }}
|: {{user.auth.local.username}}
p
small.muted
| {{ $t('loginNameDescription') }}
p {{ $t('email') }}
|: {{user.auth.local.email}}
hr
.usersettings
h5 {{ $t('changeDisplayName') }}
.form(name='changeDisplayName', novalidate)
.form-group
input#changeDisplayname.form-control(type='text', :placeholder="$t('newDisplayName')", v-model='temporaryDisplayName')
button.btn.btn-primary(type='submit', @click='changeDisplayName(temporaryDisplayName)') {{ $t('submit') }}
h5 {{ $t('changeUsername') }}
.form(v-if='user.auth.local', name='changeUsername', novalidate)
//-.alert.alert-danger(ng-messages='changeUsername.$error && changeUsername.submitted') {{ $t('fillAll') }}
.form(name='changeUsername', novalidate)
.iconalert.iconalert-success(v-if='verifiedUsername') {{ $t('usernameVerifiedConfirmation', {'username': user.auth.local.username}) }}
.iconalert.iconalert-warning(v-else)
div.align-middle
span {{ $t('usernameNotVerified') }}
.form-group
input.form-control(type='text', :placeholder="$t('newUsername')", v-model='usernameUpdates.username')
input#changeUsername.form-control(@blur='restoreEmptyUsername()',type='text', :placeholder="$t('newUsername')", v-model='usernameUpdates.username', :class='{"is-invalid input-invalid": usernameInvalid}')
.input-error(v-for="issue in usernameIssues") {{ issue }}
small.form-text.text-muted {{ $t('changeUsernameDisclaimer') }}
button.btn.btn-primary(type='submit', @click='changeUser("username", usernameUpdates)', :disabled='usernameCannotSubmit') {{ $t('saveAndConfirm') }}
h5(v-if='user.auth.local.email') {{ $t('changeEmail') }}
.form(v-if='user.auth.local.email', name='changeEmail', novalidate)
.form-group
input.form-control(type='password', :placeholder="$t('password')", v-model='usernameUpdates.password')
button.btn.btn-primary(type='submit', @click='changeUser("username", usernameUpdates)') {{ $t('submit') }}
h5 {{ $t('changeEmail') }}
.form(v-if='user.auth.local', name='changeEmail', novalidate)
.form-group
input.form-control(type='text', :placeholder="$t('newEmail')", v-model='emailUpdates.newEmail')
input#changeEmail.form-control(type='text', :placeholder="$t('newEmail')", v-model='emailUpdates.newEmail')
.form-group
input.form-control(type='password', :placeholder="$t('password')", v-model='emailUpdates.password')
button.btn.btn-primary(type='submit', @click='changeUser("email", emailUpdates)') {{ $t('submit') }}
h5 {{ $t('changePass') }}
.form(v-if='user.auth.local', name='changePassword', novalidate)
h5(v-if='user.auth.local.email') {{ $t('changePass') }}
.form(v-if='user.auth.local.email', name='changePassword', novalidate)
.form-group
input.form-control(type='password', :placeholder="$t('oldPass')", v-model='passwordUpdates.password')
input#changePassword.form-control(type='password', :placeholder="$t('oldPass')", v-model='passwordUpdates.password')
.form-group
input.form-control(type='password', :placeholder="$t('newPass')", v-model='passwordUpdates.newPassword')
.form-group
@@ -177,10 +172,33 @@
popover-trigger='mouseenter', v-b-popover.hover.auto="$t('deleteAccPop')") {{ $t('deleteAccount') }}
</template>
<style scoped>
<style lang="scss" scoped>
@import '~client/assets/scss/colors.scss';
input {
color: $gray-50;
}
.usersettings h5 {
margin-top: 1em;
}
.iconalert > div > span {
line-height: 25px;
}
.iconalert > div:after {
clear: both;
content: '';
display: table;
}
.input-error {
color: $red-50;
font-size: 90%;
width: 100%;
margin-top: 5px;
}
</style>
<script>
@@ -188,7 +206,7 @@ import hello from 'hellojs';
import moment from 'moment';
import axios from 'axios';
import { mapState } from 'client/libs/store';
import debounce from 'lodash/debounce';
import restoreModal from './restoreModal';
import resetModal from './resetModal';
import deleteModal from './deleteModal';
@@ -224,7 +242,8 @@ export default {
availableFormats: ['MM/dd/yyyy', 'dd/MM/yyyy', 'yyyy/MM/dd'],
dayStartOptions,
newDayStart: 0,
usernameUpdates: {},
temporaryDisplayName: '',
usernameUpdates: {username: ''},
emailUpdates: {},
passwordUpdates: {},
localAuth: {
@@ -233,6 +252,7 @@ export default {
password: '',
confirmPassword: '',
},
usernameIssues: [],
};
},
mounted () {
@@ -240,12 +260,26 @@ export default {
// @TODO: We may need to request the party here
this.party = this.$store.state.party;
this.newDayStart = this.user.preferences.dayStart;
this.usernameUpdates.username = this.user.auth.local.username || null;
this.temporaryDisplayName = this.user.profile.name;
this.emailUpdates.newEmail = this.user.auth.local.email || null;
this.localAuth.username = this.user.auth.local.username || null;
hello.init({
facebook: process.env.FACEBOOK_KEY, // eslint-disable-line no-process-env
google: process.env.GOOGLE_CLIENT_ID, // eslint-disable-line no-process-env
}, {
redirect_uri: '', // eslint-disable-line
});
const focusID = this.$route.query.focus;
if (focusID !== undefined && focusID !== null) {
this.$nextTick(() => {
const element = document.getElementById(focusID);
if (element !== undefined && element !== null) {
element.focus();
}
});
}
},
computed: {
...mapState({
@@ -275,8 +309,47 @@ export default {
hasClass () {
return this.$store.getters['members:hasClass'](this.user);
},
verifiedUsername () {
return this.user.flags.verifiedUsername;
},
usernameValid () {
if (this.usernameUpdates.username.length <= 1) return false;
return this.usernameIssues.length === 0;
},
usernameInvalid () {
if (this.usernameUpdates.username.length <= 1) return false;
return !this.usernameValid;
},
usernameCannotSubmit () {
if (this.usernameUpdates.username.length <= 1) return true;
return !this.usernameValid;
},
},
watch: {
usernameUpdates: {
handler () {
this.validateUsername(this.usernameUpdates.username);
},
deep: true,
},
},
methods: {
// eslint-disable-next-line func-names
validateUsername: debounce(function (username) {
if (username.length <= 1 || username === this.user.auth.local.username) {
this.usernameIssues = [];
return;
}
this.$store.dispatch('auth:verifyUsername', {
username,
}).then(res => {
if (res.issues !== undefined) {
this.usernameIssues = res.issues;
} else {
this.usernameIssues = [];
}
});
}, 500),
set (preferenceType, subtype) {
let settings = {};
if (!subtype) {
@@ -349,8 +422,19 @@ export default {
},
async changeUser (attribute, updates) {
await axios.put(`/api/v4/user/auth/update-${attribute}`, updates);
alert(this.$t(`${attribute}Success`));
this.user[attribute] = updates[attribute];
if (attribute === 'username') {
this.user.auth.local.username = updates[attribute];
this.localAuth.username = this.user.auth.local.username;
this.user.flags.verifiedUsername = true;
} else if (attribute === 'email') {
this.user.auth.local.email = updates[attribute];
}
},
async changeDisplayName (newName) {
await axios.put('/api/v4/user/', {'profile.name': newName});
alert(this.$t('displayNameSuccess'));
this.user.profile.name = newName;
this.temporaryDisplayName = newName;
},
openRestoreModal () {
this.$root.$emit('bv::show::modal', 'restore');
@@ -383,8 +467,14 @@ export default {
alert(e.message);
}
},
addLocalAuth () {
axios.post('/api/v4/user/auth/local/register', this.localAuth, 'addedLocalAuth');
async addLocalAuth () {
await axios.post('/api/v4/user/auth/local/register', this.localAuth);
alert(this.$t('addedLocalAuth'));
},
restoreEmptyUsername () {
if (this.usernameUpdates.username.length < 1) {
this.usernameUpdates.username = this.user.auth.local.username;
}
},
},
};
@@ -0,0 +1,169 @@
<template lang="pug">
drawer.inventoryDrawer(
:title="$t('quickInventory')"
:errorMessage="inventoryDrawerErrorMessage(selectedDrawerItemType)"
)
div(slot="drawer-header")
drawer-header-tabs(
:tabs="filteredTabs",
@changedPosition="tabSelected($event)"
)
div(slot="right-item")
#petLikeToEatMarket.drawer-help-text(v-once)
| {{ $t('petLikeToEat') + ' ' }}
span.svg-icon.inline.icon-16(v-html="icons.information")
b-popover(
target="petLikeToEatMarket",
:placement="'top'",
)
.popover-content-text(v-html="$t('petLikeToEatText')", v-once)
drawer-slider(
v-if="hasOwnedItemsForType(selectedDrawerItemType)"
:items="ownedItems(selectedDrawerItemType) || []",
slot="drawer-slider",
:itemWidth=94,
:itemMargin=24,
:itemType="selectedDrawerTab"
)
template(slot="item", slot-scope="ctx")
slot(
name="item",
:item="ctx.item",
:itemClass="getItemClass(selectedDrawerContentType, ctx.item.key)",
:itemCount="userItems[selectedDrawerContentType][ctx.item.key] || 0",
:itemName="getItemName(selectedDrawerItemType, ctx.item)",
:itemType="selectedDrawerItemType"
)
</template>
<script>
import {mapState} from 'client/libs/store';
import inventoryUtils from 'client/mixins/inventoryUtils';
import svgInformation from 'assets/svg/information.svg';
import _filter from 'lodash/filter';
import CountBadge from 'client/components/ui/countBadge';
import Item from 'client/components/inventory/item';
import Drawer from 'client/components/ui/drawer';
import DrawerSlider from 'client/components/ui/drawerSlider';
import DrawerHeaderTabs from 'client/components/ui/drawerHeaderTabs';
export default {
mixins: [inventoryUtils],
components: {
Item,
CountBadge,
Drawer,
DrawerSlider,
DrawerHeaderTabs,
},
props: {
defaultSelectedTab: {
type: Number,
default: 0,
},
showEggs: Boolean,
showPotions: Boolean,
},
data () {
return {
drawerTabs: [
{
key: 'eggs',
label: this.$t('eggs'),
show: () => this.showEggs,
},
{
key: 'food',
label: this.$t('foodTitle'),
show: () => true,
},
{
key: 'hatchingPotions',
label: this.$t('hatchingPotions'),
show: () => this.showPotions,
},
{
key: 'special',
contentType: 'food',
label: this.$t('special'),
show: () => true,
},
],
selectedDrawerTab: this.defaultSelectedTab,
icons: Object.freeze({
information: svgInformation,
}),
};
},
computed: {
...mapState({
content: 'content',
userItems: 'user.data.items',
}),
selectedDrawerItemType () {
return this.filteredTabs[this.selectedDrawerTab].key;
},
selectedDrawerContentType () {
return this.filteredTabs[this.selectedDrawerTab].contentType ||
this.selectedDrawerItemType;
},
filteredTabs () {
return this.drawerTabs.filter(t => t.show());
},
},
methods: {
ownedItems (type) {
let mappedItems = _filter(this.content[type], i => {
return this.userItems[type][i.key] > 0;
});
switch (type) {
case 'food':
return _filter(mappedItems, f => {
return f.key !== 'Saddle';
});
case 'special':
if (this.userItems.food.Saddle) {
return _filter(this.content.food, f => {
return f.key === 'Saddle';
});
} else {
return [];
}
default:
return mappedItems;
}
},
tabSelected ($event) {
this.selectedDrawerTab = $event;
},
hasOwnedItemsForType (type) {
return this.ownedItems(type).length > 0;
},
inventoryDrawerErrorMessage (type) {
if (!this.hasOwnedItemsForType(type)) {
switch (type) {
case 'food': return this.$t('noFoodAvailable');
case 'special': return this.$t('noSaddlesAvailable');
default:
// @TODO: Change any places using similar locales from `pets.json` and use these new locales from 'inventory.json'
return this.$t('noItemsAvailableForType', {type: this.$t(`${type}ItemType`)});
}
}
},
},
};
</script>
<style lang="scss">
.inventoryDrawer {
.drawer-slider {
height: 126px;
}
}
</style>
@@ -0,0 +1,126 @@
<template lang="pug">
div.featuredItems
.background(:class="{broken: broken}")
.background(:class="{cracked: broken, broken: broken}")
div.npc
div.featured-label
span.rectangle
span.text {{npcName}}
span.rectangle
div.content
div.featured-label.with-border
span.rectangle
span.text {{ featuredText }}
span.rectangle
div.items.margin-center
shopItem(
v-for="item in featuredItems",
:key="item.key",
:item="item",
:price="item.value",
:itemContentClass="'shop_'+item.key",
:emptyItem="false",
:popoverPosition="'top'",
@click="featuredItemSelected(item)"
)
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)"
)
span.svg-icon.inline.icon-12.color(v-html="icons.pin")
</template>
<script>
import ShopItem from './shopItem';
import pinUtils from 'client/mixins/pinUtils';
import svgPin from 'assets/svg/pin.svg';
export default {
mixins: [pinUtils],
props: {
broken: Boolean,
npcName: String,
featuredText: String,
featuredItems: Array,
},
components: {
ShopItem,
},
data () {
return {
icons: Object.freeze({
pin: svgPin,
}),
};
},
methods: {
featuredItemSelected (item) {
this.$emit('featuredItemSelected', item);
},
},
};
</script>
<style lang="scss" scoped>
.featuredItems {
height: 216px;
.background {
width: 100%;
height: 216px;
position: absolute;
top: 0;
left: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.content {
display: flex;
flex-direction: column;
z-index: 1; // Always cover background.
}
.npc {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 216px;
}
.background.broken {
background: url('~assets/images/npc/broken/market_broken_background.png');
background-repeat: repeat-x;
}
.background.cracked {
background: url('~assets/images/npc/broken/market_broken_layer.png');
background-repeat: repeat-x;
}
.broken .npc {
background: url('~assets/images/npc/broken/market_broken_npc.png');
background-repeat: no-repeat;
}
}
.featured-label {
margin: 24px auto;
}
@media only screen and (max-width: 768px) {
.featuredItems .content {
display: none !important;
}
}
</style>
+1 -7
View File
@@ -11,17 +11,11 @@
<script>
import SecondaryMenu from 'client/components/secondaryMenu';
import notifications from 'client/mixins/notifications';
export default {
mixins: [notifications],
components: {
SecondaryMenu,
},
methods: {
showUnpinNotification (item) {
this.text(this.$t('unpinnedItem', {item: item.text}));
},
},
methods: {},
};
</script>
@@ -0,0 +1,52 @@
<template lang="pug">
div
countBadge(
v-if="item.showCount !== false",
:show="true",
:count="count"
)
.badge.badge-pill.badge-purple.gems-left(v-if='item.key === "gem"')
| {{ gemsLeft }}
span.badge.badge-pill.badge-item.badge-svg(
:class="{'item-selected-badge': item.pinned, 'hide': !item.pinned}",
@click.prevent.stop="togglePinned(item)"
)
span.svg-icon.inline.icon-12.color(v-html="icons.pin")
</template>
<script>
import { mapState } from 'client/libs/store';
import CountBadge from 'client/components/ui/countBadge';
import svgPin from 'assets/svg/pin.svg';
import planGemLimits from 'common/script/libs/planGemLimits';
import pinUtils from '../../../mixins/pinUtils';
export default {
mixins: [pinUtils],
props: ['item'],
components: {
CountBadge,
},
data () {
return {
icons: Object.freeze({
pin: svgPin,
}),
};
},
computed: {
...mapState({
user: 'user.data',
userItems: 'user.data.items',
}),
count () {
return this.userItems[this.item.purchaseType][this.item.key];
},
gemsLeft () {
if (!this.user.purchased.plan) return 0;
return planGemLimits.convCap + this.user.purchased.plan.consecutive.gemCapExtra - this.user.purchased.plan.gemsBought;
},
},
};
</script>
@@ -0,0 +1,94 @@
<template lang="pug">
div.items
shopItem(v-for="item in sortedMarketItems",
:key="item.key",
:item="item",
:emptyItem="false",
:popoverPosition="'top'",
@click="itemSelected(item)")
span(slot="popoverContent")
strong(v-if='item.key === "gem" && gemsLeft === 0') {{ $t('maxBuyGems') }}
h4.popover-content-title {{ item.text }}
template(slot="itemBadge", slot-scope="ctx")
category-item(:item='ctx.item')
</template>
<script>
import { mapState } from 'client/libs/store';
import pinUtils from 'client/mixins/pinUtils';
import planGemLimits from 'common/script/libs/planGemLimits';
import ShopItem from '../shopItem';
import CategoryItem from './categoryItem';
import _filter from 'lodash/filter';
import _sortBy from 'lodash/sortBy';
import _map from 'lodash/map';
export default {
mixins: [pinUtils],
props: ['hideLocked', 'hidePinned', 'searchBy', 'sortBy', 'category'],
components: {
CategoryItem,
ShopItem,
},
computed: {
...mapState({
content: 'content',
user: 'user.data',
userItems: 'user.data.items',
userStats: 'user.data.stats',
}),
gemsLeft () {
if (!this.user.purchased.plan) return 0;
return planGemLimits.convCap + this.user.purchased.plan.consecutive.gemCapExtra - this.user.purchased.plan.gemsBought;
},
sortedMarketItems () {
let result = _map(this.category.items, (e) => {
return {
...e,
pinned: this.isPinned(e),
};
});
result = _filter(result, (item) => {
if (this.hidePinned && item.pinned) {
return false;
}
if (this.searchBy) {
let foundPosition = item.text.toLowerCase().indexOf(this.searchBy);
if (foundPosition === -1) {
return false;
}
}
return true;
});
switch (this.sortBy) {
case 'AZ': {
result = _sortBy(result, ['text']);
break;
}
case 'sortByNumber': {
result = _sortBy(result, item => {
if (item.showCount === false) return 0;
return this.userItems[item.purchaseType][item.key] || 0;
});
break;
}
}
return result;
},
},
methods: {
itemSelected (item) {
this.$root.$emit('buyModal::showItem', item);
},
},
};
</script>
@@ -0,0 +1,169 @@
<template lang="pug">
layout-section(:title="$t('equipment')")
div(slot="filters")
filter-dropdown(
:label="$t('class')",
:initialItem="selectedGearCategory",
:items="marketGearCategories",
:withIcon="true",
@selected="selectedGroupGearByClass = $event.id"
)
span(slot="item", slot-scope="ctx")
span.svg-icon.inline.icon-16(v-html="icons[ctx.item.id]")
span.text {{ getClassName(ctx.item.id) }}
filter-dropdown(
:label="$t('sortBy')",
:initialItem="selectedSortGearBy",
:items="sortGearBy",
@selected="selectedSortGearBy = $event"
)
span(slot="item", slot-scope="ctx")
span.text {{ $t(ctx.item.id) }}
itemRows(
:items="sortedGearItems",
:itemWidth=94,
:itemMargin=24,
:type="'gear'",
:noItemsLabel="$t('noGearItemsOfClass')",
slot="content"
)
template(slot="item", slot-scope="ctx")
shopItem(
:key="ctx.item.key",
:item="ctx.item",
:emptyItem="userItems.gear[ctx.item.key] === undefined",
:popoverPosition="'top'",
@click="gearSelected(ctx.item)"
)
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)"
)
span.svg-icon.inline.icon-12.color(v-html="icons.pin")
</template>
<script>
import {mapState} from 'client/libs/store';
import LayoutSection from 'client/components/ui/layoutSection';
import FilterDropdown from 'client/components/ui/filterDropdown';
import ItemRows from 'client/components/ui/itemRows';
import ShopItem from '../shopItem';
import shops from 'common/script/libs/shops';
import svgPin from 'assets/svg/pin.svg';
import svgWarrior from 'assets/svg/warrior.svg';
import svgWizard from 'assets/svg/wizard.svg';
import svgRogue from 'assets/svg/rogue.svg';
import svgHealer from 'assets/svg/healer.svg';
import _filter from 'lodash/filter';
import _sortBy from 'lodash/sortBy';
import pinUtils from '../../../mixins/pinUtils';
const sortGearTypes = ['sortByType', 'sortByPrice', 'sortByCon', 'sortByPer', 'sortByStr', 'sortByInt'].map(g => ({id: g}));
const sortGearTypeMap = {
sortByType: 'type',
sortByPrice: 'value',
sortByCon: 'con',
sortByStr: 'str',
sortByInt: 'int',
};
export default {
mixins: [pinUtils],
props: ['hideLocked', 'hidePinned', 'searchBy'],
components: {
LayoutSection,
FilterDropdown,
ItemRows,
ShopItem,
},
data () {
return {
sortGearBy: sortGearTypes,
selectedSortGearBy: sortGearTypes[0],
selectedGroupGearByClass: '',
icons: Object.freeze({
pin: svgPin,
warrior: svgWarrior,
wizard: svgWizard,
rogue: svgRogue,
healer: svgHealer,
}),
};
},
computed: {
...mapState({
content: 'content',
user: 'user.data',
userItems: 'user.data.items',
userStats: 'user.data.stats',
}),
marketGearCategories () {
return shops.getMarketGearCategories(this.user).map(c => {
c.id = c.identifier;
return c;
});
},
selectedGearCategory () {
return this.marketGearCategories.filter(c => c.id === this.selectedGroupGearByClass)[0];
},
sortedGearItems () {
let category = _filter(this.marketGearCategories, ['identifier', this.selectedGroupGearByClass]);
let result = _filter(category[0].items, (gear) => {
if (this.hideLocked && gear.locked) {
return false;
}
if (this.hidePinned && gear.pinned) {
return false;
}
if (this.searchBy) {
let foundPosition = gear.text.toLowerCase().indexOf(this.searchBy);
if (foundPosition === -1) {
return false;
}
}
// hide already owned
return !this.userItems.gear.owned[gear.key];
});
// first all unlocked
// then the selected sort
result = _sortBy(result, [(item) => item.locked, sortGearTypeMap[this.selectedSortGearBy.id]]);
return result;
},
},
methods: {
getClassName (classType) {
if (classType === 'wizard') {
return this.$t('mage');
} else {
return this.$t(classType);
}
},
gearSelected (item) {
if (!item.locked) {
this.$root.$emit('buyModal::showItem', item);
}
},
},
created () {
this.selectedGroupGearByClass = this.userStats.class;
},
};
</script>
<style scoped>
</style>
@@ -0,0 +1,46 @@
<template lang="pug">
.form
h2(v-once) {{ $t('filter') }}
.form-group
checkbox(
v-for="category in categories",
:key="category.identifier",
:id="`category-${category.identifier}`",
:checked.sync="viewOptions[category.identifier].selected",
:text="category.text"
)
div.form-group.clearfix
h3.float-left(v-once) {{ $t('hideLocked') }}
toggle-switch.float-right(
v-model="lockedChecked",
@change="$emit('update:hideLocked', $event)"
)
div.form-group.clearfix
h3.float-left(v-once) {{ $t('hidePinned') }}
toggle-switch.float-right(
v-model="pinnedChecked",
@change="$emit('update:hidePinned', $event)"
)
</template>
<script>
import Checkbox from 'client/components/ui/checkbox';
import toggleSwitch from 'client/components/ui/toggleSwitch';
export default {
props: ['hidePinned', 'hideLocked', 'categories', 'viewOptions'],
components: {
Checkbox,
toggleSwitch,
},
data () {
return {
lockedChecked: this.hideLocked,
pinnedChecked: this.hidePinned,
};
},
};
</script>
<style scoped>
</style>
+141 -543
View File
@@ -1,245 +1,79 @@
<template lang="pug">
.row.market
.standard-sidebar.d-none.d-sm-block
page-layout.market
div(slot="sidebar")
.form-group
input.form-control.input-search(type="text", v-model="searchText", :placeholder="$t('search')")
.form
h2(v-once) {{ $t('filter') }}
.form-group
.form-check(
v-for="category in categories",
:key="category.identifier",
)
.custom-control.custom-checkbox
input.custom-control-input(type="checkbox", v-model="viewOptions[category.identifier].selected", :id="`category-${category.identifier}`")
label.custom-control-label(v-once, :for="`category-${category.identifier}`") {{ category.text }}
div.form-group.clearfix
h3.float-left(v-once) {{ $t('hideLocked') }}
toggle-switch.float-right(
v-model="hideLocked",
)
div.form-group.clearfix
h3.float-left(v-once) {{ $t('hidePinned') }}
toggle-switch.float-right(
v-model="hidePinned",
)
.standard-page
div.featuredItems
.background(:class="{broken: broken}")
.background(:class="{cracked: broken, broken: broken}")
div.npc
div.featured-label
span.rectangle
span.text Alex
span.rectangle
div.content
div.featured-label.with-border
span.rectangle
span.text {{ market.featured.text }}
span.rectangle
div.items.margin-center
shopItem(
v-for="item in market.featured.items",
:key="item.key",
:item="item",
:price="item.value",
:itemContentClass="'shop_'+item.key",
:emptyItem="false",
:popoverPosition="'top'",
@click="featuredItemSelected(item)"
)
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)"
)
span.svg-icon.inline.icon-12.color(v-html="icons.pin")
market-filter(
:categories="categories",
:hideLocked.sync="hideLocked",
:hidePinned.sync="hidePinned",
:viewOptions="viewOptions"
)
div(slot="page")
featured-items-header(
:broken="broken",
:npcName="'Alex'",
:featuredText="market.featured.text",
:featuredItems="market.featured.items"
@featuredItemSelected="featuredItemSelected($event)"
)
h1.mb-4.page-header(v-once) {{ $t('market') }}
.clearfix(v-if="viewOptions['equipment'].selected")
h2.float-left.mb-3.filters-title
| {{ $t('equipment') }}
.filters.float-right
span.dropdown-label {{ $t('class') }}
b-dropdown(right=true)
span.dropdown-icon-item(slot="text")
span.svg-icon.inline.icon-16(v-html="icons[selectedGroupGearByClass]")
span.text {{ getClassName(selectedGroupGearByClass) }}
b-dropdown-item(
v-for="gearCategory in marketGearCategories",
@click="selectedGroupGearByClass = gearCategory.identifier",
:active="selectedGroupGearByClass === gearCategory.identifier",
:key="gearCategory.identifier"
)
span.dropdown-icon-item
span.svg-icon.inline.icon-16(v-html="icons[gearCategory.identifier]")
span.text {{ gearCategory.text }}
span.dropdown-label {{ $t('sortBy') }}
b-dropdown(:text="$t(selectedSortGearBy)", right=true)
b-dropdown-item(
v-for="sort in sortGearBy",
@click="selectedSortGearBy = sort",
:active="selectedSortGearBy === sort",
:key="sort"
) {{ $t(sort) }}
br
itemRows(
:items="filteredGear(selectedGroupGearByClass, searchTextThrottled, selectedSortGearBy, hideLocked, hidePinned)",
:itemWidth=94,
:itemMargin=24,
:type="'gear'",
:noItemsLabel="$t('noGearItemsOfClass')",
v-if="viewOptions['equipment'].selected"
equipment-section(
v-if="viewOptions['equipment'].selected",
:hidePinned="hidePinned",
:hideLocked="hideLocked",
:searchBy="searchTextThrottled"
)
template(slot="item", slot-scope="ctx")
shopItem(
:key="ctx.item.key",
:item="ctx.item",
:emptyItem="userItems.gear[ctx.item.key] === undefined",
:popoverPosition="'top'",
@click="gearSelected(ctx.item)"
layout-section(:title="$t('items')")
div(slot="filters")
filter-dropdown(
:label="$t('sortBy')",
:initialItem="selectedSortItemsBy",
:items="sortItemsBy",
@selected="selectedSortItemsBy = $event"
)
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)"
)
span.svg-icon.inline.icon-12.color(v-html="icons.pin")
.clearfix
h2.float-left.mb-3
| {{ $t('items') }}
div.float-right
span.dropdown-label {{ $t('sortBy') }}
b-dropdown(:text="$t(selectedSortItemsBy)", right=true)
b-dropdown-item(
v-for="sort in sortItemsBy",
@click="selectedSortItemsBy = sort",
:active="selectedSortItemsBy === sort",
:key="sort"
) {{ $t(sort) }}
span(slot="item", slot-scope="ctx")
span.text {{ $t(ctx.item.id) }}
div(
v-for="category in categories",
v-if="viewOptions[category.identifier].selected && category.identifier !== 'equipment'"
)
h4 {{ category.text }}
div.items
shopItem(
v-for="item in sortedMarketItems(category, selectedSortItemsBy, searchTextThrottled, hidePinned)",
:key="item.key",
:item="item",
:emptyItem="false",
:popoverPosition="'top'",
@click="itemSelected(item)"
category-row(
:hidePinned="hidePinned",
:hideLocked="hideLocked",
:searchBy="searchTextThrottled",
:sortBy="selectedSortItemsBy.id",
:category="category"
)
span(slot="popoverContent")
strong(v-if='item.key === "gem" && gemsLeft === 0') {{ $t('maxBuyGems') }}
h4.popover-content-title {{ item.text }}
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"
)
.badge.badge-pill.badge-purple.gems-left(v-if='item.key === "gem"')
| {{ gemsLeft }}
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)"
)
span.svg-icon.inline.icon-12.color(v-html="icons.pin")
keys-to-kennel(v-if='category.identifier === "special"')
div.fill-height
//- @TODO: Create new InventoryDrawer component and re-use in 'inventory/stable' component.
drawer(
:title="$t('quickInventory')"
:errorMessage="inventoryDrawerErrorMessage(selectedDrawerItemType)"
)
div(slot="drawer-header")
drawer-header-tabs(
:tabs="drawerTabs",
@changedPosition="tabSelected($event)"
)
div(slot="right-item")
#petLikeToEatMarket.drawer-help-text(v-once)
| {{ $t('petLikeToEat') + ' ' }}
span.svg-icon.inline.icon-16(v-html="icons.information")
b-popover(
target="petLikeToEatMarket",
:placement="'top'",
)
.popover-content-text(v-html="$t('petLikeToEatText')", v-once)
drawer-slider(
v-if="hasOwnedItemsForType(selectedDrawerItemType)"
:items="ownedItems(selectedDrawerItemType) || []",
slot="drawer-slider",
:itemWidth=94,
:itemMargin=24,
:itemType="selectedDrawerTab"
)
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", slot-scope="ctx")
countBadge(
:show="true",
:count="userItems[drawerTabs[selectedDrawerTab].contentType][ctx.item.key] || 0"
)
span(slot="popoverContent")
h4.popover-content-title {{ getItemName(selectedDrawerItemType, ctx.item) }}
sellModal(
:item="selectedItemToSell",
:itemType="selectedDrawerItemType",
:itemCount="selectedItemToSell != null ? userItems[drawerTabs[selectedDrawerTab].contentType][selectedItemToSell.key] : 0",
:text="selectedItemToSell != null ? getItemName(selectedDrawerItemType, selectedItemToSell) : ''",
@change="resetItemToSell($event)"
)
inventoryDrawer(:showEggs="true", :showPotions="true")
template(slot="item", slot-scope="ctx")
item.flat(
item(
:item="ctx.item",
:itemContentClass="getItemClass(selectedDrawerItemType, ctx.item.key)",
:showPopover="false"
:itemContentClass="ctx.itemClass",
popoverPosition="top",
@click="sellItem(ctx)"
)
template(slot="itemBadge", slot-scope="ctx")
countBadge(
:show="true",
:count="userItems[drawerTabs[selectedDrawerTab].contentType][ctx.item.key] || 0"
)
countBadge(
slot="itemBadge"
:show="true",
:count="ctx.itemCount"
)
h4.popover-content-title(slot="popoverContent") {{ ctx.itemName }}
sellModal
</template>
<style lang="scss">
@import '~client/assets/scss/colors.scss';
@import '~client/assets/scss/variables.scss';
.market .drawer-slider {
min-height: 60px;
.message {
top: 10px;
}
}
.fill-height {
height: 38px; // button + margin + padding
}
@@ -250,10 +84,6 @@
height: 48px;
}
.featured-label {
margin: 24px auto;
}
.item-wrapper.bordered-item .item {
width: 112px;
height: 112px;
@@ -265,43 +95,15 @@
margin: 0 auto;
}
.standard-page {
position: relative;
}
.featuredItems {
height: 216px;
.background {
background: url('~assets/images/npc/#{$npc_market_flavor}/market_background.png');
background-repeat: repeat-x;
width: 100%;
height: 216px;
position: absolute;
top: 0;
left: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.content {
display: flex;
flex-direction: column;
z-index: 1; // Always cover background.
}
.npc {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 216px;
background: url('~assets/images/npc/#{$npc_market_flavor}/market_banner_npc.png');
background-repeat: no-repeat;
@@ -312,23 +114,6 @@
left: 80px;
}
}
.background.broken {
background: url('~assets/images/npc/broken/market_broken_background.png');
background-repeat: repeat-x;
}
.background.cracked {
background: url('~assets/images/npc/broken/market_broken_layer.png');
background-repeat: repeat-x;
}
.broken .npc {
background: url('~assets/images/npc/broken/market_broken_npc.png');
background-repeat: no-repeat;
}
}
}
@@ -336,20 +121,6 @@
right: -.5em;
top: -.5em;
}
@media only screen and (max-width: 768px) {
.featuredItems .content {
display: none !important;
}
.filters, .filters-title {
float: none;
button {
margin-right: 4em;
margin-bottom: 1em;
}
}
}
</style>
@@ -358,14 +129,18 @@
import ShopItem from '../shopItem';
import KeysToKennel from './keysToKennel';
import EquipmentSection from './equipmentSection';
import CategoryRow from './categoryRow';
import Item from 'client/components/inventory/item';
import CountBadge from 'client/components/ui/countBadge';
import Drawer from 'client/components/ui/drawer';
import DrawerSlider from 'client/components/ui/drawerSlider';
import DrawerHeaderTabs from 'client/components/ui/drawerHeaderTabs';
import ItemRows from 'client/components/ui/itemRows';
import toggleSwitch from 'client/components/ui/toggleSwitch';
import Avatar from 'client/components/avatar';
import InventoryDrawer from 'client/components/shared/inventoryDrawer';
import FeaturedItemsHeader from '../featuredItemsHeader';
import PageLayout from 'client/components/ui/pageLayout';
import LayoutSection from 'client/components/ui/layoutSection';
import FilterDropdown from 'client/components/ui/filterDropdown';
import MarketFilter from './filter';
import SellModal from './sellModal.vue';
import EquipmentAttributesGrid from '../../inventory/equipment/attributesGrid.vue';
@@ -374,52 +149,45 @@
import svgPin from 'assets/svg/pin.svg';
import svgGem from 'assets/svg/gem.svg';
import svgInformation from 'assets/svg/information.svg';
import svgWarrior from 'assets/svg/warrior.svg';
import svgWizard from 'assets/svg/wizard.svg';
import svgRogue from 'assets/svg/rogue.svg';
import svgHealer from 'assets/svg/healer.svg';
import getItemInfo from 'common/script/libs/getItemInfo';
import isPinned from 'common/script/libs/isPinned';
import shops from 'common/script/libs/shops';
import planGemLimits from 'common/script/libs/planGemLimits';
import _filter from 'lodash/filter';
import _sortBy from 'lodash/sortBy';
import _map from 'lodash/map';
import _throttle from 'lodash/throttle';
const sortGearTypes = ['sortByType', 'sortByPrice', 'sortByCon', 'sortByPer', 'sortByStr', 'sortByInt'];
const sortItems = ['AZ', 'sortByNumber'].map(g => ({id: g}));
import notifications from 'client/mixins/notifications';
import buyMixin from 'client/mixins/buy';
import currencyMixin from '../_currencyMixin';
const sortGearTypeMap = {
sortByType: 'type',
sortByPrice: 'value',
sortByCon: 'con',
sortByStr: 'str',
sortByInt: 'int',
};
import inventoryUtils from 'client/mixins/inventoryUtils';
import pinUtils from 'client/mixins/pinUtils';
export default {
mixins: [notifications, buyMixin, currencyMixin],
mixins: [notifications, buyMixin, currencyMixin, inventoryUtils, pinUtils],
components: {
ShopItem,
KeysToKennel,
Item,
CountBadge,
Drawer,
DrawerSlider,
DrawerHeaderTabs,
ItemRows,
toggleSwitch,
SellModal,
EquipmentAttributesGrid,
Avatar,
InventoryDrawer,
FeaturedItemsHeader,
PageLayout,
LayoutSection,
FilterDropdown,
EquipmentSection,
CategoryRow,
MarketFilter,
SelectMembersModal,
},
watch: {
@@ -438,24 +206,10 @@ export default {
pin: svgPin,
gem: svgGem,
information: svgInformation,
warrior: svgWarrior,
wizard: svgWizard,
rogue: svgRogue,
healer: svgHealer,
}),
selectedDrawerTab: 0,
selectedDrawerItemType: 'eggs',
selectedGroupGearByClass: '',
sortGearBy: sortGearTypes,
selectedSortGearBy: 'sortByType',
sortItemsBy: ['AZ', 'sortByNumber'],
selectedSortItemsBy: 'AZ',
selectedItemToSell: null,
sortItemsBy: sortItems,
selectedSortItemsBy: sortItems[0],
hideLocked: false,
hidePinned: false,
@@ -474,120 +228,79 @@ export default {
userStats: 'user.data.stats',
userItems: 'user.data.items',
}),
marketGearCategories () {
return shops.getMarketGearCategories(this.user);
},
market () {
return shops.getMarketShop(this.user);
},
categories () {
if (this.market) {
let categories = [
...this.market.categories,
];
if (!this.market) return [];
categories.push({
identifier: 'equipment',
text: this.$t('equipment'),
});
categories.push({
identifier: 'cards',
text: this.$t('cards'),
items: _map(_filter(this.content.cardTypes, (value) => {
return value.yearRound;
}), (value) => {
return {
...getItemInfo(this.user, 'card', value),
showCount: false,
};
}),
});
let specialItems = [{
...getItemInfo(this.user, 'fortify'),
showCount: false,
}];
if (this.user.purchased.plan.customerId) {
let gemItem = getItemInfo(this.user, 'gem');
specialItems.push({
...gemItem,
showCount: false,
});
}
if (this.user.flags.rebirthEnabled) {
let rebirthItem = getItemInfo(this.user, 'rebirth_orb');
specialItems.push({
showCount: false,
...rebirthItem,
});
}
if (specialItems.length > 0) {
categories.push({
identifier: 'special',
text: this.$t('special'),
items: specialItems,
});
}
categories.map((category) => {
if (!this.viewOptions[category.identifier]) {
this.$set(this.viewOptions, category.identifier, {
selected: true,
});
}
});
return categories;
} else {
return [];
}
},
drawerTabs () {
return [
{
key: 'eggs',
contentType: 'eggs',
label: this.$t('eggs'),
},
{
key: 'food',
contentType: 'food',
label: this.$t('foodTitle'),
},
{
key: 'hatchingPotions',
contentType: 'hatchingPotions',
label: this.$t('hatchingPotions'),
},
{
key: 'special',
contentType: 'food',
label: this.$t('special'),
},
let categories = [
...this.market.categories,
];
},
gemsLeft () {
if (!this.user.purchased.plan) return 0;
return planGemLimits.convCap + this.user.purchased.plan.consecutive.gemCapExtra - this.user.purchased.plan.gemsBought;
categories.push({
identifier: 'equipment',
text: this.$t('equipment'),
});
categories.push({
identifier: 'cards',
text: this.$t('cards'),
items: _map(_filter(this.content.cardTypes, (value) => {
return value.yearRound;
}), (value) => {
return {
...getItemInfo(this.user, 'card', value),
showCount: false,
};
}),
});
let specialItems = [{
...getItemInfo(this.user, 'fortify'),
showCount: false,
}];
if (this.user.purchased.plan.customerId) {
let gemItem = getItemInfo(this.user, 'gem');
specialItems.push({
...gemItem,
showCount: false,
});
}
if (this.user.flags.rebirthEnabled) {
let rebirthItem = getItemInfo(this.user, 'rebirth_orb');
specialItems.push({
showCount: false,
...rebirthItem,
});
}
if (specialItems.length > 0) {
categories.push({
identifier: 'special',
text: this.$t('special'),
items: specialItems,
});
}
categories.map((category) => {
if (!this.viewOptions[category.identifier]) {
this.$set(this.viewOptions, category.identifier, {
selected: true,
});
}
});
return categories;
},
},
methods: {
getClassName (classType) {
if (classType === 'wizard') {
return this.$t('mage');
} else {
return this.$t(classType);
}
},
tabSelected ($event) {
this.selectedDrawerTab = $event;
this.selectedDrawerItemType = this.drawerTabs[$event].key;
sellItem (itemScope) {
this.$root.$emit('sellItem', itemScope);
},
ownedItems (type) {
let mappedItems = _filter(this.content[type], i => {
@@ -620,133 +333,18 @@ export default {
return this.$t('noItemsAvailableForType', { type: this.$t(`${type}ItemType`) });
}
},
getItemClass (type, itemKey) {
switch (type) {
case 'food':
case 'special':
return `Pet_Food_${itemKey}`;
case 'eggs':
return `Pet_Egg_${itemKey}`;
case 'hatchingPotions':
return `Pet_HatchingPotion_${itemKey}`;
default:
return '';
}
},
getItemName (type, item) {
switch (type) {
case 'eggs':
return this.$t('egg', {eggType: item.text()});
case 'hatchingPotions':
return this.$t('potion', {potionType: item.text()});
default:
return item.text();
}
},
filteredGear (groupByClass, searchBy, sortBy, hideLocked, hidePinned) {
let category = _filter(this.marketGearCategories, ['identifier', groupByClass]);
let result = _filter(category[0].items, (gear) => {
if (hideLocked && gear.locked) {
return false;
}
if (hidePinned && gear.pinned) {
return false;
}
if (searchBy) {
let foundPosition = gear.text.toLowerCase().indexOf(searchBy);
if (foundPosition === -1) {
return false;
}
}
// hide already owned
return !this.userItems.gear.owned[gear.key];
});
// first all unlocked
// then the selected sort
result = _sortBy(result, [(item) => item.locked, sortGearTypeMap[sortBy]]);
return result;
},
sortedMarketItems (category, sortBy, searchBy, hidePinned) {
let result = _map(category.items, (e) => {
return {
...e,
pinned: isPinned(this.user, e),
};
});
result = _filter(result, (item) => {
if (hidePinned && item.pinned) {
return false;
}
if (searchBy) {
let foundPosition = item.text.toLowerCase().indexOf(searchBy);
if (foundPosition === -1) {
return false;
}
}
return true;
});
switch (sortBy) {
case 'AZ': {
result = _sortBy(result, ['text']);
break;
}
case 'sortByNumber': {
result = _sortBy(result, item => {
if (item.showCount === false) return 0;
return this.userItems[item.purchaseType][item.key] || 0;
});
break;
}
}
return result;
},
resetItemToSell ($event) {
if (!$event) {
this.selectedItemToSell = null;
}
},
isGearLocked (gear) {
if (gear.klass !== this.userStats.class) {
return true;
}
return false;
},
togglePinned (item) {
if (!this.$store.dispatch('user:togglePinnedItem', {type: item.pinType, path: item.path})) {
this.$parent.showUnpinNotification(item);
}
},
itemSelected (item) {
this.$root.$emit('buyModal::showItem', item);
},
featuredItemSelected (item) {
if (item.purchaseType === 'gear') {
this.gearSelected(item);
if (!item.locked) {
this.itemSelected(item);
}
} else {
this.itemSelected(item);
}
},
gearSelected (item) {
if (!item.locked) {
this.$root.$emit('buyModal::showItem', item);
}
},
},
created () {
this.selectedGroupGearByClass = this.userStats.class;
},
};
</script>
@@ -1,6 +1,5 @@
<template lang="pug">
b-modal#sell-modal(
:visible="item != null",
:hide-header="true",
@change="onChange($event)"
)
@@ -8,30 +7,37 @@
span.svg-icon.inline.icon-10(aria-hidden="true", v-html="icons.close", @click="hideDialog()")
div.content(v-if="item")
div.inner-content
item.flat(
:item="item",
:itemContentClass="itemContextToSell.itemClass",
:showPopover="false"
)
countBadge(
slot="itemBadge",
:show="true",
:count="itemContextToSell.itemCount"
)
div.inner-content(v-if="item.sellWarningNote")
slot(name="item", :item="item")
h4.title {{ itemContextToSell.itemName }}
h4.title {{ text ? text : item.text() }}
div.text {{ item.sellWarningNote() }}
br
div(v-if="item.sellWarningNote")
div.text {{ item.sellWarningNote() }}
br
div.inner-content(v-else)
slot(name="item", :item="item")
div(v-once)
div.text {{ item.notes() }}
h4.title {{ text ? text : item.text() }}
div.text {{ item.notes() }}
div
b.how-many-to-sell {{ $t('howManyToSell') }}
div
b.how-many-to-sell {{ $t('howManyToSell') }}
div
b-input.itemsToSell(type="number", v-model="selectedAmountToSell", :max="itemContextToSell.itemCount", min="1", @keyup.native="preventNegative($event)", step="1")
div
b-input.itemsToSell(type="number", v-model="selectedAmountToSell", :max="itemCount", min="1", @keyup.native="preventNegative($event)", step="1")
span.svg-icon.inline.icon-32(aria-hidden="true", v-html="icons.gold")
span.value {{ item.value }}
span.svg-icon.inline.icon-32(aria-hidden="true", v-html="icons.gold")
span.value {{ item.value }}
button.btn.btn-primary(@click="sellItems()") {{ $t('sell') }}
button.btn.btn-primary(@click="sellItems()") {{ $t('sell') }}
div.clearfix(slot="modal-footer")
span.balance.float-left {{ $t('yourBalance') }}
@@ -119,14 +125,19 @@
import svgGem from 'assets/svg/gem.svg';
import BalanceInfo from '../balanceInfo.vue';
import Item from 'client/components/inventory/item';
import CountBadge from 'client/components/ui/countBadge';
export default {
components: {
BalanceInfo,
Item,
CountBadge,
},
data () {
return {
selectedAmountToSell: 1,
itemContextToSell: null,
icons: Object.freeze({
close: svgClose,
@@ -135,6 +146,20 @@
}),
};
},
computed: {
item () {
return this.itemContextToSell && this.itemContextToSell.item;
},
},
mounted () {
this.$root.$on('sellItem', (itemCtx) => {
this.itemContextToSell = itemCtx;
this.$root.$emit('bv::show::modal', 'sell-modal');
});
},
destroyed () {
this.$root.$off('sellItem');
},
methods: {
onChange ($event) {
this.$emit('change', $event);
@@ -155,7 +180,7 @@
}
this.$store.dispatch('shops:sellItems', {
type: this.itemType,
type: this.itemContextToSell.itemType,
key: this.item.key,
amount: this.selectedAmountToSell,
});
@@ -165,19 +190,5 @@
this.$root.$emit('bv::hide::modal', 'sell-modal');
},
},
props: {
item: {
type: Object,
},
itemType: {
type: String,
},
text: {
type: String,
},
itemCount: {
type: Number,
},
},
};
</script>
@@ -339,6 +339,7 @@
import toggleSwitch from 'client/components/ui/toggleSwitch';
import Avatar from 'client/components/avatar';
import buyMixin from 'client/mixins/buy';
import pinUtils from 'client/mixins/pinUtils';
import currencyMixin from '../_currencyMixin';
import BuyModal from './buyQuestModal.vue';
@@ -357,7 +358,7 @@
import _map from 'lodash/map';
export default {
mixins: [buyMixin, currencyMixin],
mixins: [buyMixin, currencyMixin, pinUtils],
components: {
ShopItem,
Item,
@@ -474,11 +475,6 @@ export default {
return false;
},
togglePinned (item) {
if (!this.$store.dispatch('user:togglePinnedItem', {type: item.pinType, path: item.path})) {
this.$parent.showUnpinNotification(item);
}
},
selectItem (item) {
if (item.locked) return;
@@ -294,6 +294,7 @@
import Avatar from 'client/components/avatar';
import buyMixin from 'client/mixins/buy';
import currencyMixin from '../_currencyMixin';
import pinUtils from 'client/mixins/pinUtils';
import svgPin from 'assets/svg/pin.svg';
import svgWarrior from 'assets/svg/warrior.svg';
@@ -318,7 +319,7 @@
import shops from 'common/script/libs/shops';
export default {
mixins: [buyMixin, currencyMixin],
mixins: [buyMixin, currencyMixin, pinUtils],
components: {
ShopItem,
Item,
@@ -514,11 +515,6 @@
return false;
},
togglePinned (item) {
if (!this.$store.dispatch('user:togglePinnedItem', {type: item.pinType, path: item.path})) {
this.$parent.showUnpinNotification(item);
}
},
itemSelected (item) {
if (item.locked) return;
this.$root.$emit('buyModal::showItem', item);
@@ -240,8 +240,10 @@
import isPinned from 'common/script/libs/isPinned';
import shops from 'common/script/libs/shops';
import pinUtils from 'client/mixins/pinUtils';
export default {
mixins: [pinUtils],
components: {
ShopItem,
Item,
@@ -369,11 +371,6 @@
getGrouped (entries) {
return _groupBy(entries, 'group');
},
togglePinned (item) {
if (!this.$store.dispatch('user:togglePinnedItem', {type: item.pinType, path: item.path})) {
this.$parent.showUnpinNotification(item);
}
},
selectItemToBuy (item) {
this.$root.$emit('buyModal::showItem', item);
},
+6 -2
View File
@@ -42,6 +42,11 @@
}
}
.home-header, .home-header .btn {
font-family: 'Varela Round', sans-serif;
font-weight: normal;
}
.btn-primary.pull-right {
height: 2.5em;
margin: auto 0px auto auto;
@@ -66,9 +71,8 @@
.nav-item {
.nav-link {
font-size: 16px;
font-size: 16px !important;
color: $white;
font-weight: bold;
line-height: 1.5;
padding: 16px 24px;
transition: none;
+63 -3
View File
@@ -24,7 +24,8 @@
span {{$t('or')}}
.form(@keyup.enter="register()")
p.form-text {{$t('usernameLimitations')}}
input.form-control(type='text', placeholder='Login Name', v-model='username', :class='{"input-valid": username.length > 3}')
input#usernameInput.form-control(type='text', placeholder='Login Name', v-model='username', :class='{"input-valid": usernameValid, "input-invalid": usernameInvalid}')
.input-error(v-for="issue in usernameIssues") {{ issue }}
input.form-control(type='email', placeholder='Email', v-model='email', :class='{"input-invalid": emailInvalid, "input-valid": emailValid}')
input.form-control(type='password', placeholder='Password', v-model='password', :class='{"input-valid": password.length > 3}')
input.form-control(type='password', placeholder='Confirm Password', v-model='passwordConfirm', :class='{"input-invalid": passwordConfirmInvalid, "input-valid": passwordConfirmValid}')
@@ -125,6 +126,8 @@
<style lang="scss" scoped>
@import '~client/assets/scss/colors.scss';
@import url('https://fonts.googleapis.com/css?family=Varela+Round');
#front {
.form-text a {
color: #fff !important;
@@ -193,6 +196,11 @@
.pixel-horizontal-3 {
color: #271b3d;
}
h1, h2, h3, h4, h5, h6, button, .strike > span, input {
font-family: 'Varela Round', sans-serif;
font-weight: normal;
}
}
#intro-signup {
@@ -255,6 +263,7 @@
.strike > span {
position: relative;
display: inline-block;
line-height: 1.14;
}
.strike > span:before,
@@ -293,6 +302,10 @@
transition: border .5s, color .5s;
}
#usernameInput.input-invalid {
margin-bottom: 0.5em;
}
.input-valid {
color: #fff;
}
@@ -357,13 +370,17 @@
}
strong {
font-size: 20px;
font-size: 24px;
font-family: 'Varela Round', sans-serif;
line-height: 1.33;
}
}
#use-cases {
strong {
font-size: 20px;
font-size: 24px;
font-family: 'Varela Round', sans-serif;
line-height: 1.33;
}
img {
@@ -440,6 +457,11 @@
.featured {
text-align: center;
font-family: 'Varela Round', sans-serif;
strong {
font-size: 12px;
}
.svg-icon {
vertical-align: bottom;
@@ -525,10 +547,19 @@
margin-bottom: .5em;
}
}
.input-error {
color: #fff;
font-size: 90%;
width: 100%;
text-align: center;
margin-bottom: 1em;
}
</style>
<script>
import hello from 'hellojs';
import debounce from 'lodash/debounce';
import googlePlay from 'assets/images/home/google-play-badge.svg';
import iosAppStore from 'assets/images/home/ios-app-store.svg';
import iphones from 'assets/images/home/iphones.svg';
@@ -575,6 +606,7 @@
password: '',
passwordConfirm: '',
email: '',
usernameIssues: [],
};
},
mounted () {
@@ -600,6 +632,14 @@
if (this.email.length <= 3) return false;
return !this.validateEmail(this.email);
},
usernameValid () {
if (this.username.length <= 3) return false;
return this.usernameIssues.length === 0;
},
usernameInvalid () {
if (this.username.length <= 3) return false;
return !this.usernameValid;
},
passwordConfirmValid () {
if (this.passwordConfirm.length <= 3) return false;
return this.passwordConfirm === this.password;
@@ -609,11 +649,31 @@
return this.passwordConfirm !== this.password;
},
},
watch: {
username () {
this.validateUsername(this.username);
},
},
methods: {
validateEmail (email) {
let re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(email);
},
// eslint-disable-next-line func-names
validateUsername: debounce(function (username) {
if (username.length <= 3) {
return;
}
this.$store.dispatch('auth:verifyUsername', {
username: this.username,
}).then(res => {
if (res.issues !== undefined) {
this.usernameIssues = res.issues;
} else {
this.usernameIssues = [];
}
});
}, 500),
// @TODO this is totally duplicate from the registerLogin component
async register () {
let groupInvite = '';
@@ -17,16 +17,20 @@
img.img-fluid.img-rendering-auto.press-img(:src="`/static/presskit/${category}/${secondaryCategory}/${img}`")
h1 {{ $t('FAQ') }}
#faq(role='tablist')
div(v-for='(QA, index) in faq')
.faq-question(v-for='(QA, index) in faq')
h2(v-b-toggle="QA.question", tabindex="0", role="button", v-html="$t(QA.question)")
b-collapse(:id="QA.question", accordion="pkAccordian", role="tabpanel")
p(v-html="$t(QA.answer)")
p {{ $t('pkMoreQuestions') }}
</template>
<style lang="scss" scoped>
.faq-question {
cursor: pointer;
}
</style>
<script>
// @TODO: EMAILS.PRESS_ENQUIRY_EMAIL
const PRESS_ENQUIRY_EMAIL = 'admin@habitica.com';
+2 -2
View File
@@ -449,9 +449,9 @@ export default {
});
if (this.type !== 'todo') return;
this.$root.$on('habitica::resync-requested', () => {
this.$root.$on('habitica::resync-completed', () => {
if (this.activeFilter.label !== 'complete2') return;
this.loadCompletedTodos(true);
this.loadCompletedTodos();
});
},
destroyed () {
+1 -1
View File
@@ -282,7 +282,7 @@
color: $gray-50;
font-size: 14px;
line-height: 1.43;
margin-bottom: 10px;
margin-bottom: -3px;
min-height: 0px;
width: 100%;
margin-left: 8px;
@@ -30,7 +30,7 @@
.input-group
.input-group-prepend.input-group-icon.align-items-center
.svg-icon.gold(v-html="icons.gold")
input.form-control(type="number", v-model="task.value", required, placeholder="1.0", step="0.01", min="0")
input.form-control(type="number", v-model="task.value", required, placeholder="Enter a Value", step="0.01", min="0")
.option.mt-0(v-if="checklistEnabled")
label(v-once) {{ $t('checklist') }}
+26
View File
@@ -0,0 +1,26 @@
<template lang="pug">
.form-check
.custom-control.custom-checkbox
input.custom-control-input(type="checkbox", v-model="isChecked", :id="id")
label.custom-control-label(v-once, :for="id") {{ text }}
</template>
<script>
export default {
props: {
checked: Boolean,
id: String,
text: String,
},
data () {
return {
isChecked: this.checked,
};
},
watch: {
isChecked (after) {
this.$emit('update:checked', after);
},
},
};
</script>
+1 -1
View File
@@ -1,6 +1,6 @@
<template lang="pug">
span.badge.badge-pill.badge-item.badge-count(
v-if="show && count != 0",
v-if="show && count > 0",
) {{ count }}
</template>
@@ -0,0 +1,45 @@
<template lang="pug">
span
span.dropdown-label {{ label }}
b-dropdown(right=true)
span(slot="text", :class="{'dropdown-icon-item': withIcon}")
slot(name="item", :item="selectedItem")
b-dropdown-item(
v-for="item in items",
@click="selectItem(item)",
:active="selectedItem.id === item.id",
:key="item.id"
)
span(:class="{'dropdown-icon-item': withIcon}")
slot(name="item", :item="item")
</template>
<script>
export default {
props: {
label: String,
items: Array,
initialItem: Object,
withIcon: {
type: Boolean,
default: false,
},
},
data () {
return {
selectedItem: this.initialItem,
};
},
methods: {
selectItem (item) {
this.selectedItem = item;
this.$emit('selected', item);
},
},
};
</script>
<style scoped lang="scss">
</style>
@@ -0,0 +1,33 @@
<template lang="pug">
div
.clearfix
h2.float-left.mb-3.filters-title {{ title }}
.filters.float-right
slot(name="filters")
br
slot(name="content")
</template>
<script>
export default {
props: {
title: String,
},
};
</script>
<style scoped lang="scss">
@media only screen and (max-width: 768px) {
.filters, .filters-title {
float: none;
button {
margin-right: 4em;
margin-bottom: 1em;
}
}
}
</style>
@@ -0,0 +1,25 @@
<template lang="pug">
.row
.standard-sidebar.d-none.d-sm-block(v-if="showSidebar")
slot(name="sidebar")
.standard-page
slot(name="page")
</template>
<script>
export default {
props: {
showSidebar: {
type: Boolean,
default: true,
},
},
};
</script>
<style scoped lang="scss">
.standard-page {
position: relative;
}
</style>
+100 -75
View File
@@ -1,5 +1,5 @@
<template lang="pug">
b-modal#inbox-modal(title="", :hide-footer="true", size='lg')
b-modal#inbox-modal(title="", :hide-footer="true", size='lg', @shown="onModalShown", @hide="onModalHide")
.header-wrap.container.align-items-center(slot="modal-header")
.row.align-items-center
.col-4
@@ -7,21 +7,17 @@
.col-2
.svg-icon.envelope(v-html="icons.messageIcon")
.col-6
h2.text-center(v-once) {{$t('messages')}}
// @TODO: Implement this after we fix username bug
// .col-2.offset-1
// button.btn.btn-secondary(@click='toggleClick()') +
.col-4.offset-4
.svg-icon.close(v-html="icons.svgClose", @click='close()')
h2.text-center(v-once) {{ $t('messages') }}
.col-4.offset-3
toggle-switch.float-right(
:label="optTextSet.switchDescription",
:checked="!this.user.inbox.optOut"
:hoverText="optTextSet.popoverText",
@change="toggleOpt()"
)
// .col-8.to-form(v-if='displayCreate')
// strong To:
// b-form-input
.col-1
.close
span.svg-icon.inline.icon-10(aria-hidden="true", v-html="icons.svgClose", @click="close()")
.row
.col-4.sidebar
.search-section
@@ -38,19 +34,22 @@
span.timeago {{conversation.date | timeAgo}}
div {{conversation.lastMessageText ? conversation.lastMessageText.substring(0, 30) : ''}}
.col-8.messages.d-flex.flex-column.justify-content-between
.empty-messages.text-center(v-if='activeChat.length === 0 && !selectedConversation.key')
.empty-messages.text-center(v-if='!selectedConversation.key')
.svg-icon.envelope(v-html="icons.messageIcon")
h4 {{placeholderTexts.title}}
p(v-html="placeholderTexts.description")
.empty-messages.text-center(v-if='activeChat.length === 0 && selectedConversation.key')
.empty-messages.text-center(v-if='selectedConversation.key && selectedConversationMessages.length === 0')
p {{ $t('beginningOfConversation', {userName: selectedConversation.name})}}
chat-message.message-scroll(v-if="activeChat.length > 0", :chat.sync='activeChat', :inbox='true', ref="chatscroll")
chat-messages.message-scroll(
v-if="selectedConversation.messages && selectedConversationMessages.length > 0",
:chat='selectedConversationMessages',
:inbox='true',
@message-removed='messageRemoved',
ref="chatscroll"
)
.pm-disabled-caption.text-center(v-if="user.inbox.optOut && selectedConversation.key")
h4 {{$t('PMDisabledCaptionTitle')}}
p {{$t('PMDisabledCaptionText')}}
// @TODO: Implement new message header here when we fix the above
.new-message-row(v-if='selectedConversation.key && !user.flags.chatRevoked')
textarea(
v-model='newMessage',
@@ -79,14 +78,6 @@
margin: 0;
}
.close {
margin-top: .5em;
width: 15px;
position: absolute;
top: -1.9em;
right: 0.3em;
}
.sidebar {
background-color: $gray-700;
min-height: 600px;
@@ -214,39 +205,43 @@ import groupBy from 'lodash/groupBy';
import { mapState } from 'client/libs/store';
import styleHelper from 'client/mixins/styleHelper';
import toggleSwitch from 'client/components/ui/toggleSwitch';
import axios from 'axios';
import messageIcon from 'assets/svg/message.svg';
import chatMessage from '../chat/chatMessages';
import chatMessages from '../chat/chatMessages';
import svgClose from 'assets/svg/close.svg';
export default {
mixins: [styleHelper],
components: {
chatMessage,
chatMessages,
toggleSwitch,
},
mounted () {
this.$root.$on('habitica::new-inbox-message', (data) => {
this.$root.$emit('bv::show::modal', 'inbox-modal');
const conversation = this.conversations.find(convo => {
return convo.key === data.userIdToMessage;
});
// Wait for messages to be loaded
const unwatchLoaded = this.$watch('loaded', (loaded) => {
if (!loaded) return;
const conversation = this.conversations.find(convo => {
return convo.key === data.userIdToMessage;
});
if (loaded) setImmediate(() => unwatchLoaded());
if (conversation) {
this.selectConversation(data.userIdToMessage);
return;
}
this.initiatedConversation = {
user: data.userName,
uuid: data.userIdToMessage,
};
if (conversation) {
this.selectConversation(data.userIdToMessage);
return;
}
const newMessage = {
text: '',
timestamp: new Date(),
user: data.userName,
uuid: data.userIdToMessage,
id: '',
};
this.$set(this.user.inbox.messages, data.userIdToMessage, newMessage);
this.selectConversation(data.userIdToMessage);
}, {immediate: true});
});
},
destroyed () {
@@ -262,8 +257,10 @@ export default {
selectedConversation: {},
search: '',
newMessage: '',
activeChat: [],
showPopover: false,
messages: [],
loaded: false,
initiatedConversation: null,
};
},
filters: {
@@ -274,13 +271,23 @@ export default {
computed: {
...mapState({user: 'user.data'}),
conversations () {
const inboxGroup = groupBy(this.user.inbox.messages, 'uuid');
const inboxGroup = groupBy(this.messages, 'uuid');
// Add placeholder for new conversations
if (this.initiatedConversation && this.initiatedConversation.uuid) {
inboxGroup[this.initiatedConversation.uuid] = [{
id: '',
text: '',
user: this.initiatedConversation.user,
uuid: this.initiatedConversation.uuid,
timestamp: new Date(),
}];
}
// Create conversation objects
const convos = [];
for (let key in inboxGroup) {
const convoSorted = sortBy(inboxGroup[key], [(o) => {
return o.timestamp;
return (new Date(o.timestamp)).getTime();
}]);
// Fix poor inbox chat models
@@ -297,12 +304,9 @@ export default {
return newChat;
});
// In case the last message is a placeholder, remove it
const recentMessage = newChatModels[newChatModels.length - 1];
// Special case where we have placeholder message because conversations are just grouped messages for now
if (!recentMessage.text) {
newChatModels.splice(newChatModels.length - 1, 1);
}
if (!recentMessage.text) newChatModels.splice(newChatModels.length - 1, 1);
const convoModel = {
name: recentMessage.toUser ? recentMessage.toUser : recentMessage.user, // Handles case where from user sent the only message or the to user sent the only message
@@ -322,6 +326,12 @@ export default {
return conversations.reverse();
},
// Separate from selectedConversation which is not coputed so messages don't update automatically
selectedConversationMessages () {
const selectedConversationKey = this.selectedConversation.key;
const selectedConversation = this.conversations.find(c => c.key === selectedConversationKey);
return selectedConversation ? selectedConversation.messages : [];
},
filtersConversations () {
if (!this.search) return this.conversations;
return filter(this.conversations, (conversation) => {
@@ -357,6 +367,25 @@ export default {
},
},
methods: {
async onModalShown () {
this.loaded = false;
const res = await axios.get('/api/v4/inbox/messages');
this.messages = res.data.data;
this.loaded = true;
},
onModalHide () {
this.messages = [];
this.loaded = false;
this.initiatedConversation = null;
},
messageRemoved (message) {
const messageIndex = this.messages.findIndex(msg => msg.id === message.id);
if (messageIndex !== -1) this.messages.splice(messageIndex, 1);
if (this.selectedConversationMessages.length === 0) this.initiatedConversation = {
user: this.selectedConversation.name,
uuid: this.selectedConversation.key,
};
},
toggleClick () {
this.displayCreate = !this.displayCreate;
},
@@ -368,14 +397,7 @@ export default {
return conversation.key === key;
});
this.selectedConversation = convoFound;
let activeChat = convoFound.messages;
activeChat = sortBy(activeChat, [(o) => {
return moment(o.timestamp).toDate();
}]);
this.$set(this, 'activeChat', activeChat);
this.selectedConversation = convoFound || {};
Vue.nextTick(() => {
if (!this.$refs.chatscroll) return;
@@ -386,35 +408,38 @@ export default {
sendPrivateMessage () {
if (!this.newMessage) return;
let convoFound = this.conversations.find((conversation) => {
return conversation.key === this.selectedConversation.key;
});
this.$store.dispatch('members:sendPrivateMessage', {
toUserId: this.selectedConversation.key,
message: this.newMessage,
});
convoFound.messages.push({
this.messages.push({
sent: true,
text: this.newMessage,
timestamp: new Date(),
user: this.user.profile.name,
uuid: this.user._id,
user: this.selectedConversation.name,
uuid: this.selectedConversation.key,
contributor: this.user.contributor,
});
this.activeChat = convoFound.messages;
// Remove the placeholder message
if (this.initiatedConversation && this.initiatedConversation.uuid === this.selectedConversation.key) {
this.initiatedConversation = null;
}
convoFound.lastMessageText = this.newMessage;
convoFound.date = new Date();
this.newMessage = '';
this.selectedConversation.lastMessageText = this.newMessage;
this.selectedConversation.date = new Date();
Vue.nextTick(() => {
if (!this.$refs.chatscroll) return;
let chatscroll = this.$refs.chatscroll.$el;
chatscroll.scrollTop = chatscroll.scrollHeight;
});
this.$store.dispatch('members:sendPrivateMessage', {
toUserId: this.selectedConversation.key,
message: this.newMessage,
}).then(response => {
const newMessage = response.data.data.message;
Object.assign(this.messages[this.messages.length - 1], newMessage);
});
this.newMessage = '';
},
close () {
this.$root.$emit('bv::hide::modal', 'inbox-modal');
@@ -99,7 +99,7 @@
span.hint(:popover-title='$t(statInfo.title)', popover-placement='right',
:popover='$t(statInfo.popover)', popover-trigger='mouseenter')
.stat-title(:class='stat') {{ $t(statInfo.title) }}
strong.number {{ statsComputed[stat] | floorWholeNumber }}
strong.number {{totalStatPoints(stat) | floorWholeNumber}}
.col-12.col-md-6
ul.bonus-stats
li
@@ -113,7 +113,7 @@
| {{statsComputed.classBonus[stat]}}
li
strong {{$t('allocated')}}:
| {{user.stats[stat]}}
| {{totalAllocatedStats(stat)}}
li
strong {{$t('buffs')}}:
| {{user.stats.buffs[stat]}}
@@ -124,7 +124,7 @@
h3
| {{$t('statPoints')}}
.counter.badge(v-if='user.stats.points || userLevel100Plus')
| {{user.stats.points}}&nbsp;
| {{pointsRemaining}}&nbsp;
.col-12.col-md-6
.float-right
toggle-switch(
@@ -137,7 +137,7 @@
.box.white.row.col-12
.col-9
div(:class='stat') {{ $t(stats[stat].title) }}
.number {{ user.stats[stat] }}
.number {{totalAllocatedStats(stat)}}
.points {{$t('pts')}}
.col-3
div
@@ -157,7 +157,7 @@
import Content from '../../../common/script/content';
import { beastMasterProgress, mountMasterProgress } from '../../../common/script/count';
import autoAllocate from '../../../common/script/fns/autoAllocate';
import allocate from '../../../common/script/ops/stats/allocate';
import allocateBulk from '../../../common/script/ops/stats/allocateBulk';
import statsComputed from '../../../common/script/libs/statsComputed';
import axios from 'axios';
@@ -239,14 +239,27 @@
return this.user.stats.lvl >= 100;
},
showStatsSave () {
const statsAreBeingUpdated = Object.values(this.statUpdates).find(stat => stat > 0);
return Boolean(this.user.stats.points) || statsAreBeingUpdated;
return Boolean(this.user.stats.points);
},
pointsRemaining () {
let points = this.user.stats.points;
Object.values(this.statUpdates).forEach(value => {
points -= value;
});
return points;
},
},
methods: {
getGearTitle (key) {
return this.flatGear[key].text();
},
totalAllocatedStats (stat) {
return this.user.stats[stat] + this.statUpdates[stat];
},
totalStatPoints (stat) {
return this.statsComputed[stat] + this.statUpdates[stat];
},
totalCount (objectToCount) {
let total = size(objectToCount);
return total;
@@ -292,14 +305,12 @@
return display;
},
allocate (stat) {
allocate(this.user, {query: { stat }});
this.statUpdates[stat] += 1;
if (this.pointsRemaining === 0) return;
this.statUpdates[stat]++;
},
deallocate (stat) {
if (this.user.stats[stat] === 0) return;
this.user.stats[stat] -= 1;
this.user.stats.points += 1;
this.statUpdates[stat] -= 1;
if (this.statUpdates[stat] === 0) return;
this.statUpdates[stat]--;
},
async saveAttributes () {
this.loading = true;
@@ -309,10 +320,7 @@
if (this.statUpdates[stat] > 0) statUpdates[stat] = this.statUpdates[stat];
});
await axios.post('/api/v4/user/allocate-bulk', {
stats: statUpdates,
});
// reset statUpdates to zero before request to avoid display errors while waiting for server
this.statUpdates = {
str: 0,
int: 0,
@@ -320,6 +328,12 @@
per: 0,
};
allocateBulk(this.user, { body: { stats: statUpdates } });
await axios.post('/api/v4/user/allocate-bulk', {
stats: statUpdates,
});
this.loading = false;
},
allocateNow () {