Merge branch 'develop' into release

This commit is contained in:
Sabe Jones
2019-12-19 16:55:10 -06:00
303 changed files with 28278 additions and 21288 deletions
@@ -1,104 +1,16 @@
<template>
<div
class="modal-footer"
style="margin-top:0"
ng-init="loadWidgets()"
>
<div class="container-fluid share-buttons">
<div class="row">
<div class="col-12 text-center">
<a
class="twitter-share-button share-button"
:href="twitterLink"
target="_blank"
>
<div
class="social-icon twitter svg-icon"
v-html="icons.twitter"
></div>
{{ $t('tweet') }}
</a>
<a
class="fb-share-button share-button"
:href="facebookLink"
target="_blank"
>
<div
class="social-icon facebook svg-icon"
v-html="icons.facebook"
></div>
{{ $t('share') }}
</a>
<!-- @TODO: Still want this?
.col-4a.tumblr-share-button(:data-href='socialLevelLink', data-notes='none')-->
</div>
</div>
</div>
<!-- TODO disabled for now, see https://github.com/HabitRPG/habitica/issues/11283 -->
</div>
</template>
<style scoped>
.share-buttons {
margin-top: 1em;
margin-bottom: 1em;
}
.share-button {
display: inline-block;
width: 77px;
padding: .5em;
border-radius: 2px;
text-align: center;
color: #fff;
}
.fb-share-button {
background-color: #2995cd;
}
.twitter-share-button {
margin-right: .5em;
background-color: #3bcad7;
}
.social-icon {
width: 16px;
display: inline-block;
vertical-align: bottom;
margin-right: .5em;
}
.social-icon.facebook svg {
width: 7.5px;
margin-bottom: .2em;
}
.social-icon.twitter {
margin-bottom: .2em;
}
<style lang="scss" scoped>
.modal-footer {
border-top: none;
}
</style>
<script>
// @TODO:
import twitter from '@/assets/svg/twitter.svg';
import facebook from '@/assets/svg/facebook.svg';
const BASE_URL = 'https://habitica.com';
export default {
data () {
const tweet = this.$t('achievementShare');
return {
icons: Object.freeze({
twitter,
facebook,
}),
tweet,
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}`,
};
},
};
</script>
@@ -3,14 +3,32 @@
id="generic-achievement"
:title="data.message"
size="md"
:hide-footer="true"
:hide-header="true"
>
<div class="modal-body">
<div class="col-12">
<achievement-avatar class="avatar" />
<span
class="close-icon svg-icon inline icon-10"
@click="close()"
v-html="icons.close"
></span>
<div class="content">
<div
v-once
class="dialog-header title"
>
{{ $t('earnedAchievement') }}
</div>
<div class="col-6 offset-3 text-center">
<p v-html="data.modalText"></p>
<div class="inner-content">
<div class="achievement-background d-flex align-items-center">
<div
class="icon"
:class="achievementClass"
></div>
</div>
<h4
class="title"
v-html="$t(achievement.titleKey)"
>
</h4>
<button
class="btn btn-primary"
@click="close()"
@@ -19,33 +37,86 @@
</button>
</div>
</div>
<achievement-footer />
<div
slot="modal-footer"
class="clearfix"
></div>
</b-modal>
</template>
<style scoped>
.avatar {
width: 140px;
<style lang="scss">
@import '~@/assets/scss/modal.scss';
#generic-achievement {
@include centeredModal();
.modal-dialog {
width: 330px;
}
.modal-footer {
padding-top: 0px;
}
}
</style>
<style scoped lang="scss">
@import '~@/assets/scss/colors.scss';
.content {
text-align: center;
}
.inner-content {
margin: 24px auto auto;
display: flex;
flex-direction: column;
align-items: center;
}
.achievement-background {
width: 112px;
height: 112px;
border-radius: 4px;
background-color: $gray-700;
}
.dialog-header {
margin-top: 16px !important;
color: $purple-200 !important;
}
.title {
margin-bottom: 24px !important;
}
.icon {
margin: 0 auto;
margin-bottom: 1.5em;
margin-top: 1.5em;
}
</style>
<script>
import achievementFooter from './achievementFooter';
import achievementAvatar from './achievementAvatar';
import { mapState } from '@/libs/store';
import achievements from '@/../../common/script/content/achievements';
import svgClose from '@/assets/svg/close.svg';
export default {
components: {
achievementFooter,
achievementAvatar,
},
props: ['data'],
data () {
return {
icons: Object.freeze({
close: svgClose,
}),
};
},
computed: {
...mapState({ user: 'user.data' }),
achievement () {
return achievements[this.data.achievement];
},
achievementClass () {
return `${this.achievement.icon}2x`;
},
},
methods: {
close () {
@@ -27,36 +27,6 @@
label(style='display:inline-block') {{ $t('dontShowAgain') }}
-->
</div>
<div class="container-fluid share-buttons">
<div class="row">
<div class="col-12 text-center">
<a
class="twitter-share-button share-button"
:href="twitterLink"
target="_blank"
>
<div
class="social-icon twitter svg-icon"
v-html="icons.twitter"
></div>
{{ $t('tweet') }}
</a>
<a
class="fb-share-button share-button"
:href="facebookLink"
target="_blank"
>
<div
class="social-icon facebook svg-icon"
v-html="icons.facebook"
></div>
{{ $t('share') }}
</a>
</div>
<!-- @TODO: Still want this? .col-4a.tumblr
-share-button(:data-href='socialLevelLink', data-notes='none')-->
</div>
</div>
</b-modal>
</template>
@@ -72,7 +42,6 @@ label(style='display:inline-block') {{ $t('dontShowAgain') }}
.modal-body {
padding-top: 1em;
padding-bottom: 0;
}
.modal-footer {
@@ -99,45 +68,6 @@ label(style='display:inline-block') {{ $t('dontShowAgain') }}
margin-top: 1em;
min-height: 0px;
}
.share-buttons {
margin-top: 1em;
margin-bottom: 1em;
}
.share-button {
display: inline-block;
width: 77px;
padding: .5em;
border-radius: 2px;
text-align: center;
color: #fff;
}
.fb-share-button {
background-color: #2995cd;
}
.twitter-share-button {
margin-right: .5em;
background-color: #3bcad7;
}
.social-icon {
width: 16px;
display: inline-block;
vertical-align: bottom;
margin-right: .5em;
}
.social-icon.facebook svg {
width: 7.5px;
margin-bottom: .2em;
}
.social-icon.twitter {
margin-bottom: .2em;
}
}
</style>
@@ -152,10 +82,6 @@ import Avatar from '../avatar';
import { mapState } from '@/libs/store';
import { MAX_HEALTH as maxHealth } from '@/../../common/script/constants';
import styleHelper from '@/mixins/styleHelper';
import twitter from '@/assets/svg/twitter.svg';
import facebook from '@/assets/svg/facebook.svg';
const BASE_URL = 'https://habitica.com';
export default {
components: {
@@ -163,18 +89,9 @@ export default {
},
mixins: [styleHelper],
data () {
const tweet = this.$t('levelUpShare');
return {
icons: Object.freeze({
twitter,
facebook,
}),
statsAllocationBoxIsOpen: true,
maxHealth,
tweet,
socialLevelLink: `${BASE_URL}/social/level-up`,
twitterLink: `https://twitter.com/intent/tweet?text=${tweet}&via=habitica&url=${BASE_URL}/social/level-up&count=none`,
facebookLink: `https://www.facebook.com/sharer/sharer.php?text=${tweet}&u=${BASE_URL}/social/level-up`,
};
},
computed: {
@@ -183,16 +100,10 @@ export default {
return this.$store.getters['members:hasClass'](this.user) && !this.user.preferences.automaticAllocation;
},
},
mounted () {
this.loadWidgets();
},
methods: {
close () {
this.$root.$emit('bv::hide::modal', 'level-up');
},
loadWidgets () {
// @TODO:
},
changeLevelupSuppress () {
// @TODO: dispatch set({"preferences.suppressModals.levelUp":
// user.preferences.suppressModals.levelUp?true: false})
@@ -0,0 +1,106 @@
<template>
<b-modal
id="onboarding-complete"
size="sm"
:hide-footer="true"
:hide-header="true"
>
<div class="content text-center">
<span
class="close-icon svg-icon inline icon-10"
@click="close()"
v-html="icons.close"
></span>
<h2>{{ $t('congratulations') }}</h2>
<img
class="onboarding-complete-banner d-block"
src="~@/assets/images/onboarding-complete-banner@2x.png"
>
<p
class="onboarding-complete-text"
v-html="$t('onboardingCompleteDesc')"
></p>
<button
class="btn btn-primary"
@click="closeWithAction()"
>
{{ $t('viewAchievements') }}
</button>
</div>
</b-modal>
</template>
<style lang="scss">
#onboarding-complete {
.modal-content {
min-width: 330px;
}
.modal-body {
padding-top: 1em;
padding-bottom: 0;
}
.modal-footer {
margin-top: 0;
}
}
</style>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
h2 {
color: $purple-200;
margin-bottom: 24px;
margin-top: 2px;
}
.content {
padding: 0 8px;
margin-top: 18px;
}
.onboarding-complete-banner {
width: 282px;
margin: 0 auto;
margin-bottom: 24px;
}
.onboarding-complete-text {
margin-bottom: 24px;
}
.onboarding-complete-text ::v-deep .gold-amount {
color: $yellow-5;
}
button {
margin-bottom: 24px;
}
</style>
<script>
import svgClose from '@/assets/svg/close.svg';
export default {
data () {
return {
icons: Object.freeze({
close: svgClose,
}),
};
},
methods: {
close () {
this.$root.$emit('bv::hide::modal', 'onboarding-complete');
},
closeWithAction () {
this.close();
setTimeout(() => {
this.$router.push({ name: 'achievements' });
}, 200);
},
},
};
</script>
@@ -30,10 +30,10 @@
</button>
<div class="checkbox">
<input
id="user-preferences-suppressModals-streak"
v-model="user.preferences.suppressModals.streak"
type="checkbox"
@change="suppressModals"
id="user-preferences-suppressModals-streak"
>
<label for="user-preferences-suppressModals-streak">{{ $t('dontShowAgain') }}</label>
</div>
@@ -46,6 +46,13 @@
:placeholder="$t('usernamePlaceholder')"
:class="{'input-valid': usernameValid, 'input-invalid': usernameInvalid}"
>
<div
v-for="issue in usernameIssues"
:key="issue"
class="input-error"
>
{{ issue }}
</div>
</div>
<div
v-if="!registering"
@@ -97,7 +104,17 @@
class="form-control"
type="password"
:placeholder="$t(registering ? 'passwordPlaceholder' : 'password')"
:class="{
'input-valid': registering ? passwordValid : false,
'input-invalid': registering ? passwordInvalid: false,
}"
>
<div
v-if="passwordInvalid && registering"
class="input-error"
>
{{ $t('minPasswordLength') }}
</div>
</div>
<div
v-if="registering"
@@ -115,6 +132,12 @@
:placeholder="$t('confirmPasswordPlaceholder')"
:class="{'input-invalid': passwordConfirmInvalid, 'input-valid': passwordConfirmValid}"
>
<div
v-if="passwordConfirmInvalid"
class="input-error"
>
{{ $t('passwordConfirmationMatch') }}
</div>
<small
v-once
class="form-text"
@@ -183,8 +206,11 @@
text-align: center;
}
.input-valid {
color: #fff;
.input-error {
margin-top: 0.25em;
font-weight: normal;
font-size: 90%;
width: 100%;
}
}
</style>
@@ -194,7 +220,7 @@ import hello from 'hellojs';
import debounce from 'lodash/debounce';
import isEmail from 'validator/lib/isEmail';
import { setUpAxios } from '@/libs/auth';
import { MINIMUM_PASSWORD_LENGTH } from '@/../../common/script/constants';
import facebookSquareIcon from '@/assets/svg/facebook-square.svg';
import googleIcon from '@/assets/svg/google.svg';
@@ -223,6 +249,7 @@ export default {
return isEmail(this.email);
},
emailInvalid () {
if (this.email.length <= 3) return false;
return !this.emailValid;
},
usernameValid () {
@@ -230,13 +257,23 @@ export default {
return this.usernameIssues.length === 0;
},
usernameInvalid () {
if (this.username.length < 1) return false;
return !this.usernameValid;
},
passwordValid () {
if (this.password.length <= 0) return false;
return this.password.length >= MINIMUM_PASSWORD_LENGTH;
},
passwordInvalid () {
if (this.password.length <= 0) return false;
return this.password.length < MINIMUM_PASSWORD_LENGTH;
},
passwordConfirmValid () {
if (this.passwordConfirm.length <= 3) return false;
return this.passwordConfirm === this.password;
},
passwordConfirmInvalid () {
if (this.passwordConfirm.length <= 3) return false;
return !this.passwordConfirmValid;
},
},
@@ -6,8 +6,7 @@
<form
v-if="!forgotPassword && !resetPasswordSetNewOne"
id="login-form"
@submit.prevent="handleSubmit"
@keyup.enter="handleSubmit"
@submit.prevent.stop="handleSubmit"
>
<div class="text-center">
<div>
@@ -69,7 +68,7 @@
<input
id="usernameInput"
v-model="username"
class="form-control"
class="form-control input-with-error"
type="text"
:placeholder="$t('usernamePlaceholder')"
:class="{'input-valid': usernameValid, 'input-invalid': usernameInvalid}"
@@ -132,7 +131,17 @@
class="form-control"
type="password"
:placeholder="$t(registering ? 'passwordPlaceholder' : 'password')"
:class="{
'input-invalid input-with-error': registering && passwordInvalid,
'input-valid': registering && passwordValid
}"
>
<div
v-if="passwordInvalid && registering"
class="input-error"
>
{{ $t('minPasswordLength') }}
</div>
</div>
<div
v-if="registering"
@@ -145,11 +154,17 @@
<input
id="confirmPasswordInput"
v-model="passwordConfirm"
class="form-control"
class="form-control input-with-error"
type="password"
:placeholder="$t('confirmPasswordPlaceholder')"
:class="{'input-invalid': passwordConfirmInvalid, 'input-valid': passwordConfirmValid}"
>
<div
v-if="passwordConfirmInvalid"
class="input-error"
>
{{ $t('passwordConfirmationMatch') }}
</div>
<small
v-once
class="form-text"
@@ -157,22 +172,22 @@
></small>
</div>
<div class="text-center">
<div
<button
v-if="registering"
v-once
type="submit"
class="btn btn-info"
@click="register()"
:disabled="signupFormInvalid"
>
{{ $t('joinHabitica') }}
</div>
<div
</button>
<button
v-if="!registering"
v-once
type="submit"
class="btn btn-info"
@click="login()"
>
{{ $t('login') }}
</div>
</button>
<div class="toggle-links">
<router-link
v-if="registering"
@@ -275,10 +290,17 @@
<input
id="passwordInput"
v-model="password"
class="form-control"
class="form-control input-with-error"
type="password"
:placeholder="$t('password')"
:class="{'input-invalid': passwordInvalid, 'input-valid': passwordValid}"
>
<div
v-if="passwordInvalid"
class="input-error"
>
{{ $t('minPasswordLength') }}
</div>
</div>
<div class="form-group">
<label
@@ -288,10 +310,17 @@
<input
id="confirmPasswordInput"
v-model="passwordConfirm"
class="form-control"
class="form-control input-with-error"
type="password"
:placeholder="$t('confirmPasswordPlaceholder')"
:class="{'input-invalid': passwordConfirmInvalid, 'input-valid': passwordConfirmValid}"
>
<div
v-if="passwordConfirmInvalid"
class="input-error"
>
{{ $t('passwordConfirmationMatch') }}
</div>
</div>
<div class="text-center">
<div
@@ -426,10 +455,14 @@
color: $white;
}
#usernameInput.input-invalid {
.input-with-error.input-invalid {
margin-bottom: 0.5em;
}
#confirmPasswordInput + .input-error {
margin-bottom: 2em;
}
.form-text {
font-size: 14px;
color: $white;
@@ -480,7 +513,7 @@
background-image: url('~@/assets/images/auth/seamless_mountains_demo.png');
background-repeat: repeat-x;
width: 100%;
height: 500px;
height: 300px;
position: absolute;
z-index: 0;
bottom: 0;
@@ -512,7 +545,6 @@
color: #fff;
font-size: 90%;
width: 100%;
text-align: center;
}
</style>
@@ -522,6 +554,7 @@ import hello from 'hellojs';
import debounce from 'lodash/debounce';
import isEmail from 'validator/lib/isEmail';
import { MINIMUM_PASSWORD_LENGTH } from '@/../../common/script/constants';
import gryphon from '@/assets/svg/gryphon.svg';
import habiticaIcon from '@/assets/svg/habitica-logo.svg';
import facebookSquareIcon from '@/assets/svg/facebook-square.svg';
@@ -580,6 +613,14 @@ export default {
if (this.username.length < 1) return false;
return !this.usernameValid;
},
passwordValid () {
if (this.password.length <= 0) return false;
return this.password.length >= MINIMUM_PASSWORD_LENGTH;
},
passwordInvalid () {
if (this.password.length <= 0) return false;
return this.password.length < MINIMUM_PASSWORD_LENGTH;
},
passwordConfirmValid () {
if (this.passwordConfirm.length <= 3) return false;
return this.passwordConfirm === this.password;
@@ -588,6 +629,12 @@ export default {
if (this.passwordConfirm.length <= 3) return false;
return !this.passwordConfirmValid;
},
signupFormInvalid () {
return this.usernameInvalid
|| this.emailInvalid
|| this.passwordInvalid
|| this.passwordConfirmInvalid;
},
},
watch: {
$route: {
@@ -1,7 +1,10 @@
<template>
<div class="row">
<challenge-modal @updatedChallenge="updatedChallenge" />
<leave-challenge-modal :challenge-id="challenge._id" @update-challenge="updateChallenge" />
<leave-challenge-modal
:challenge-id="challenge._id"
@update-challenge="updateChallenge"
/>
<close-challenge-modal
:members="members"
:challenge-id="challenge._id"
@@ -215,7 +215,6 @@
}
.meta-info {
height: 24px;
margin-bottom: 8px;
}
@@ -595,7 +595,7 @@ export default {
this.$root.$emit('bv::hide::modal', 'challenge-modal');
this.$router.push(`/challenges/${challenge._id}`);
},
updateChallenge () {
async updateChallenge () {
const categoryKeys = this.workingChallenge.categories;
const serverCategories = [];
categoryKeys.forEach(key => {
@@ -610,10 +610,8 @@ export default {
const challengeDetails = clone(this.workingChallenge);
challengeDetails.categories = serverCategories;
this.$emit('updatedChallenge', {
challenge: challengeDetails,
});
this.$store.dispatch('challenges:updateChallenge', { challenge: challengeDetails });
const challenge = await this.$store.dispatch('challenges:updateChallenge', { challenge: challengeDetails });
this.$emit('updatedChallenge', { challenge });
this.resetWorkingChallenge();
this.$root.$emit('bv::hide::modal', 'challenge-modal');
},
+16 -2
View File
@@ -6,7 +6,15 @@
:hide-header="true"
:hide-footer="true"
:modal-class="{'page-2':modalPage > 1 && !editing}"
:no-close-on-esc="!editing"
:no-close-on-backdrop="!editing"
>
<span
v-if="editing"
class="close-icon svg-icon inline icon-10"
@click="close()"
v-html="icons.close"
></span>
<div
v-if="modalPage === 1 && !editing"
class="section row welcome-section"
@@ -1099,6 +1107,7 @@ import gold from '@/assets/svg/gold.svg';
import pin from '@/assets/svg/pin.svg';
import arrowRight from '@/assets/svg/arrow_right.svg';
import arrowLeft from '@/assets/svg/arrow_left.svg';
import svgClose from '@/assets/svg/close.svg';
import isPinned from '@/../../common/script/libs/isPinned';
import { avatarEditorUtilies } from '../mixins/avatarEditUtilities';
@@ -1138,6 +1147,7 @@ export default {
gold,
arrowRight,
arrowLeft,
close: svgClose,
}),
modalPage: 1,
activeTopPage: 'body',
@@ -1158,8 +1168,6 @@ export default {
},
computed: {
...mapState({ user: 'user.data' }),
editing () {
return this.$store.state.avatarEditorOptions.editingUser;
},
@@ -1228,6 +1236,9 @@ export default {
this.$root.$on('buyModal::boughtItem', this.backgroundPurchased);
},
methods: {
close () {
this.$root.$emit('bv::hide::modal', 'avatar-modal');
},
purchase (type, key) {
this.$store.dispatch('shops:purchase', {
type,
@@ -1278,6 +1289,9 @@ export default {
if (this.$route.path !== '/') {
this.$router.push('/');
}
// NOTE: it's important this flag is set AFTER the onboarding default tasks
// have been created or it'll break the onboarding guide achievement for creating a task
this.$store.dispatch('user:set', {
'flags.welcomed': true,
});
@@ -479,11 +479,19 @@ export default {
if (!isEmpty(this.sortOption)) {
// Use the memberlist filtered by searchTerm
sortedMembers = orderBy(
sortedMembers,
[this.sortOption.value],
[this.sortOption.direction],
);
if (this.sortOption.value === 'profile.name') {
// If members are to be sorted by name, use localeCompare for case-
// insensitive sort
sortedMembers.sort(
(a, b) => a.profile.name.localeCompare(b.profile.name),
);
} else {
sortedMembers = orderBy(
sortedMembers,
[this.sortOption.value],
[this.sortOption.direction],
);
}
}
return sortedMembers;
@@ -655,7 +663,7 @@ export default {
memberId: member._id,
groupId: this.groupId,
});
this.viewMembers();
if (this.invites.length === 0) this.viewMembers();
},
async promoteToLeader (member) {
const groupData = { ...this.group };
@@ -1,8 +1,9 @@
<template functional>
<span
class="message-count"
class="message-count d-flex align-items-center justify-content-center"
:class="{'top-count': props.top === true, 'top-count-gray': props.gray === true}"
> {{ props.count }} </span>
v-html="props.badge || props.count"
></span>
</template>
<style lang="scss">
@@ -18,6 +19,11 @@
text-align: center;
font-weight: bold;
font-size: 12px;
svg {
width: 12px;
height: 12px;
}
}
.message-count.top-count {
@@ -10,7 +10,10 @@
>
<slot name="icon"></slot>
</div>
<div class="notification-content">
<div
class="notification-content"
:class="{'has-text': hasText}"
>
<slot name="content"></slot>
</div>
<div
@@ -107,17 +110,19 @@
line-height: 1.43;
color: $gray-50;
max-width: calc(100% - 26px); // to make space for the close icon
max-width: 100%;
&.has-text {
padding-right: 12px;
}
}
.notification-remove {
// total distance from the notification top edge is 20 pixels
margin-top: 7px;
position: absolute;
width: 18px;
height: 18px;
margin-left: 12px;
padding: 4px;
right: 24px;
.svg-icon {
width: 10px;
@@ -131,7 +136,25 @@ import closeIcon from '@/assets/svg/close.svg';
import { mapActions, mapState } from '@/libs/store';
export default {
props: ['notification', 'canRemove', 'hasIcon', 'readAfterClick'],
props: {
notification: {
type: Object,
required: true,
},
canRemove: {
type: Boolean,
},
hasIcon: {
type: Boolean,
},
readAfterClick: {
type: Boolean,
},
hasText: {
type: Boolean,
default: true,
},
},
data () {
return {
icons: Object.freeze({
@@ -0,0 +1,74 @@
<template>
<base-notification
:can-remove="canRemove"
:has-icon="false"
:read-after-click="true"
:notification="notification"
:has-text="false"
@click="action"
>
<div
slot="content"
class="d-flex flex-column align-items-center onboarding-complete-content"
>
<img
class="onboarding-complete-banner d-block"
src="~@/assets/images/onboarding-complete-banner@2x.png"
>
<h3>{{ $t('congratulations') }}</h3>
<p
class="onboarding-complete-text"
v-html="$t('onboardingCompleteDesc')"
></p>
<div class="notifications-buttons">
<div
class="btn btn-small btn-primary btn-block"
>
{{ $t('viewAchievements') }}
</div>
</div>
</div>
</base-notification>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.onboarding-complete-content {
text-align: center;
}
.onboarding-complete-banner {
width: 282px;
margin-top: 12px;
margin-bottom: 12px;
}
.onboarding-complete-text {
margin-bottom: 4px;
}
.onboarding-complete-text ::v-deep .gold-amount {
color: $yellow-5;
}
.notifications-buttons {
width: 100%;
}
</style>
<script>
import BaseNotification from './base';
export default {
components: {
BaseNotification,
},
props: ['notification', 'canRemove'],
methods: {
action () {
this.$router.push({ name: 'achievements' });
},
},
};
</script>
@@ -11,10 +11,11 @@
:aria-label="$t('notifications')"
>
<message-count
v-if="notificationsCount > 0"
v-if="notificationsCount > 0 || hasSpecialBadge"
:count="notificationsCount"
:top="true"
:gray="!hasUnseenNotifications"
:gray="!hasUnseenNotifications && !hasSpecialBadge"
:badge="hasSpecialBadge ? icons.starBadge : null"
/>
<div
class="top-menu-icon svg-icon notifications"
@@ -22,7 +23,10 @@
></div>
</div>
</div>
<div slot="dropdown-content">
<div
v-if="openStatus === 1"
slot="dropdown-content"
>
<div
class="dropdown-item dropdown-separated
d-flex justify-content-between dropdown-inactive align-items-center"
@@ -41,6 +45,10 @@
>{{ $t('dismissAll') }}</a>
</div>
<world-boss />
<onboarding-guide
v-if="showOnboardingGuide"
:never-seen="hasSpecialBadge"
/>
<component
:is="notification.type"
v-for="notification in notifications"
@@ -49,7 +57,7 @@
:can-remove="!isActionable(notification)"
/>
<div
v-if="notificationsCount === 0"
v-if="notificationsCount === 0 && !showOnboardingGuide"
class="dropdown-item dropdown-separated
d-flex justify-content-center dropdown-inactive no-notifications flex-column"
>
@@ -106,10 +114,16 @@
<script>
import { mapState, mapActions } from '@/libs/store';
import * as quests from '@/../../common/script/content/quests';
import {
hasCompletedOnboarding,
hasActiveOnboarding,
} from '@/../../common/script/libs/onboarding';
import notificationsIcon from '@/assets/svg/notifications.svg';
import MenuDropdown from '../ui/customMenuDropdown';
import MessageCount from './messageCount';
import { CONSTANTS, getLocalSetting, setLocalSetting } from '@/libs/userlocalManager';
import successImage from '@/assets/svg/success.svg';
import starBadge from '@/assets/svg/star-badge.svg';
// Notifications
import NEW_STUFF from './notifications/newStuff';
@@ -132,6 +146,8 @@ import VERIFY_USERNAME from './notifications/verifyUsername';
import ACHIEVEMENT_JUST_ADD_WATER from './notifications/justAddWater';
import ACHIEVEMENT_LOST_MASTERCLASSER from './notifications/lostMasterclasser';
import ACHIEVEMENT_MIND_OVER_MATTER from './notifications/mindOverMatter';
import ONBOARDING_COMPLETE from './notifications/onboardingComplete';
import OnboardingGuide from './onboardingGuide';
export default {
components: {
@@ -158,13 +174,17 @@ export default {
ACHIEVEMENT_MIND_OVER_MATTER,
WorldBoss: WORLD_BOSS,
VERIFY_USERNAME,
OnboardingGuide,
ONBOARDING_COMPLETE,
},
data () {
return {
icons: Object.freeze({
notifications: notificationsIcon,
success: successImage,
starBadge,
}),
hasSpecialBadge: false,
quests,
openStatus: undefined,
actionableNotifications: [
@@ -177,11 +197,11 @@ export default {
handledNotifications: [
'NEW_STUFF', 'GROUP_TASK_NEEDS_WORK',
'GUILD_INVITATION', 'PARTY_INVITATION', 'CHALLENGE_INVITATION',
'QUEST_INVITATION', 'GROUP_TASK_ASSIGNED', 'GROUP_TASK_APPROVAL', 'GROUP_TASK_APPROVED', 'GROUP_TASK_CLAIMED',
'NEW_MYSTERY_ITEMS', 'CARD_RECEIVED',
'QUEST_INVITATION', 'GROUP_TASK_ASSIGNED', 'GROUP_TASK_APPROVAL', 'GROUP_TASK_APPROVED',
'GROUP_TASK_CLAIMED', 'NEW_MYSTERY_ITEMS', 'CARD_RECEIVED',
'NEW_INBOX_MESSAGE', 'NEW_CHAT_MESSAGE', 'UNALLOCATED_STATS_POINTS',
'ACHIEVEMENT_JUST_ADD_WATER', 'ACHIEVEMENT_LOST_MASTERCLASSER', 'ACHIEVEMENT_MIND_OVER_MATTER',
'VERIFY_USERNAME',
'VERIFY_USERNAME', 'ONBOARDING_COMPLETE',
],
};
},
@@ -275,6 +295,20 @@ export default {
hasClass () {
return this.$store.getters['members:hasClass'](this.user);
},
showOnboardingGuide () {
return hasActiveOnboarding(this.user) && !hasCompletedOnboarding(this.user);
},
},
mounted () {
const onboardingPanelState = getLocalSetting(CONSTANTS.keyConstants.ONBOARDING_PANEL_STATE);
if (
onboardingPanelState !== CONSTANTS.onboardingPanelValues.PANEL_OPENED
&& this.showOnboardingGuide
) {
// The first time the onboarding panel is opened a special
// badge for notifications should be used
this.hasSpecialBadge = true;
}
},
methods: {
...mapActions({
@@ -286,6 +320,18 @@ export default {
// Mark notifications as seen when the menu is opened
if (openStatus) this.markAllAsSeen();
// Reset the special notification badge as soon as it's opened
if (this.hasSpecialBadge) {
setLocalSetting(
CONSTANTS.keyConstants.ONBOARDING_PANEL_STATE,
CONSTANTS.onboardingPanelValues.PANEL_OPENED,
);
setTimeout(() => {
this.hasSpecialBadge = false;
}, 100);
}
},
markAllAsSeen () {
const idsToSee = this.notifications.map(notification => {
@@ -318,5 +364,6 @@ export default {
return this.actionableNotifications.indexOf(notification.type) !== -1;
},
},
};
</script>
@@ -0,0 +1,259 @@
<template>
<div
class="onboarding-guide-panel d-flex align-items-center flex-column p-4 dropdown-separated"
@click.stop.prevent="action"
>
<div
class="svg-icon onboarding-toggle"
:class="{'onboarding-toggle-open': open}"
@click="openPanel"
v-html="icons.down"
></div>
<div
class="svg-icon onboarding-guide-banner"
v-html="icons.onboardingGuideBanner"
></div>
<h3 class="getting-started">
{{ $t('gettingStarted') }}
</h3>
<span
class="getting-started-desc"
v-html="$t('gettingStartedDesc')"
></span>
<div
class="onboarding-progress-box d-flex flex-row justify-content-between small-text mb-2"
>
<strong>Your Progress</strong>
<span :class="{'has-progress': progress > 0}">{{ progressText }}</span>
</div>
<div class="onboarding-progress-bar mb-3">
<div
class="onboarding-progress-bar-fill"
:style="{width: `${progress}%`}"
></div>
</div>
<b-collapse
id="onboardingPanelCollapse"
v-model="open"
>
<div
v-for="(achievement, key) in onboardingAchievements"
:key="key"
:class="{
'achievement-earned': achievement.earned
}"
class="achievement-box d-flex flex-row"
>
<div class="achievement-icon-wrapper">
<div :class="`achievement-icon ${getAchievementIcon(achievement)}`"></div>
</div>
<div class="achievement-info d-flex flex-column">
<strong class="achievement-title">{{ achievement.title }}</strong>
<span class="small-text achievement-desc">{{ getAchievementText(key) }}</span>
</div>
</div>
</b-collapse>
</div>
</template>
<style lang='scss' scoped>
@import '~@/assets/scss/colors.scss';
.onboarding-guide-panel {
white-space: normal;
text-align: center;
}
.onboarding-toggle {
cursor: pointer;
position: absolute;
top: 75px;
right: 24px;
width: 16px;
& ::v-deep svg path {
stroke: $gray-200;
}
&:hover ::v-deep svg path {
stroke: $gray-100;
}
&-open {
transform: rotate(-180deg);
}
}
.onboarding-guide-banner {
margin-top: -8px;
margin-bottom: 12px;
width: 154px;
height: 48px;
}
.getting-started {
margin-bottom: 4px;
}
.getting-started-desc {
font-size: 14px;
padding-bottom: 12px;
}
.getting-started-desc ::v-deep .gold-amount {
color: $yellow-5;
}
.onboarding-progress-box {
width: 100%;
font-style: normal;
strong {
color: $gray-50;
}
.has-progress {
color: $yellow-5;
}
}
.onboarding-progress-bar {
width: 100%;
height: 4px;
border-radius: 2px;
background-color: $gray-600;
.onboarding-progress-bar-fill {
height: 100%;
border-radius: 2px;
background: $yellow-5;
}
}
.achievement-box {
padding-top: 11px;
padding-bottom: 12px;
border-bottom: 1px dashed $gray-500;
width: 100%;
&:last-child {
padding-bottom: 0px;
border-bottom: none;
}
}
.achievement-earned {
.achievement-icon-wrapper {
opacity: 1;
}
color: $gray-200;
.achievement-title {
text-decoration: line-through;
}
}
.achievement-icon-wrapper {
opacity: 0.5;
width: 40px;
.achievement-icon {
margin-left: -12px;
transform: scale(0.5);
}
}
.achievement-info {
width: 100%;
text-align: left;
font-size: 14px;
}
.achievement-title {
margin-bottom: 4px;
}
.achievement-desc {
font-style: normal;
}
</style>
<script>
import achievs from '@/../../common/script/libs/achievements';
import { mapState } from '@/libs/store';
import onboardingGuideBanner from '@/assets/svg/onboarding-guide-banner.svg';
import downIcon from '@/assets/svg/down.svg';
export default {
props: {
neverSeen: { // whether it's ever been seen by the user
type: Boolean,
default: false,
},
},
data () {
return {
icons: Object.freeze({
onboardingGuideBanner,
down: downIcon,
}),
open: false,
};
},
computed: {
...mapState({ user: 'user.data' }),
onboardingAchievements () {
return achievs.getAchievementsForProfile(this.user).onboarding.achievements;
},
progress () {
const keys = Object.keys(this.onboardingAchievements);
let nEarned = 0;
keys.forEach(key => {
if (this.onboardingAchievements[key].earned) nEarned += 1;
});
return (nEarned / keys.length) * 100;
},
progressText () {
if (this.progress === 0) {
return this.$t('letsGetStarted');
}
return this.$t('onboardingProgress', { percentage: this.progress });
},
},
created () {
// null means it's never been automatically toggled
if (this.neverSeen === true) {
// The first time the panel should be automatically opened
this.open = true;
}
},
methods: {
getAchievementIcon (achievement) {
if (achievement.earned) {
return `${achievement.icon}2x`;
}
return 'achievement-unearned2x';
},
getAchievementText (key) {
let stringKey = 'achievement';
stringKey += key.charAt(0).toUpperCase() + key.slice(1);
stringKey += 'ModalText';
return this.$t(stringKey);
},
openPanel (e) {
e.preventDefault();
e.stopPropagation();
this.open = !this.open;
},
action () {
// Do nothing, used to prevent closure on click.
},
},
};
</script>
@@ -3,6 +3,11 @@
id="hatchedPet-modal"
:hide-header="true"
>
<span
class="close-icon svg-icon inline icon-10"
@click="close()"
v-html="icons.close"
></span>
<div
v-if="pet != null"
class="content"
@@ -14,7 +19,7 @@
{{ $t('hatchedPetGeneric') }}
</div>
<div class="inner-content">
<div class="pet-background">
<div class="pet-background d-flex align-items-center">
<div :class="pet.class"></div>
</div>
<h4 class="title">
@@ -51,6 +56,10 @@
width: 330px;
}
.modal-footer {
padding-top: 0px;
}
.content {
text-align: center;
}
@@ -75,12 +84,25 @@
.dialog-header {
color: $purple-200;
margin-top: 16px;
}
.text {
margin-bottom: 24px;
min-height: 0;
&.markdown {
p {
margin-bottom: 0px;
}
}
}
}
</style>
<script>
import markdownDirective from '@/directives/markdown';
import svgClose from '@/assets/svg/close.svg';
export default {
directives: {
@@ -94,6 +116,9 @@ export default {
data () {
return {
pet: null,
icons: Object.freeze({
close: svgClose,
}),
};
},
mounted () {
@@ -26,10 +26,14 @@
<quest-completed />
<quest-invitation />
<verify-username />
<generic-achievement :data="notificationData" />
<generic-achievement
v-if="notificationData && notificationData.achievement"
:data="notificationData"
/>
<just-add-water />
<lost-masterclasser />
<mind-over-matter />
<onboarding-complete />
</div>
</template>
@@ -92,6 +96,10 @@
background-color: #4f2a93 !important;
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12) !important;
}
.introjs-skipbutton.btn-primary, .introjs-donebutton.btn-primary {
color: #fff;
}
</style>
<script>
@@ -99,9 +107,11 @@ import axios from 'axios';
import moment from 'moment';
import throttle from 'lodash/throttle';
import debounce from 'lodash/debounce';
import Vue from 'vue';
import { toNextLevel } from '@/../../common/script/statHelpers';
import { shouldDo } from '@/../../common/script/cron';
import { onOnboardingComplete } from '@/../../common/script/libs/onboarding';
import { mapState } from '@/libs/store';
import notifications from '@/mixins/notifications';
import guide from '@/mixins/guide';
@@ -132,6 +142,7 @@ import justAddWater from './achievements/justAddWater';
import lostMasterclasser from './achievements/lostMasterclasser';
import mindOverMatter from './achievements/mindOverMatter';
import loginIncentives from './achievements/login-incentives';
import onboardingComplete from './achievements/onboardingComplete';
import verifyUsername from './settings/verifyUsername';
const NOTIFICATIONS = {
@@ -164,21 +175,33 @@ const NOTIFICATIONS = {
achievement: true,
label: $t => `${$t('achievement')}: ${$t('achievementAllYourBase')}`,
modalId: 'generic-achievement',
data: {
achievement: 'allYourBase', // defined manually until the server sends all the necessary data
},
},
ACHIEVEMENT_BACK_TO_BASICS: {
achievement: true,
label: $t => `${$t('achievement')}: ${$t('achievementBackToBasics')}`,
modalId: 'generic-achievement',
data: {
achievement: 'backToBasics', // defined manually until the server sends all the necessary data
},
},
ACHIEVEMENT_DUST_DEVIL: {
achievement: true,
label: $t => `${$t('achievement')}: ${$t('achievementDustDevil')}`,
modalId: 'generic-achievement',
data: {
achievement: 'dustDevil', // defined manually until the server sends all the necessary data
},
},
ACHIEVEMENT_ARID_AUTHORITY: {
achievement: true,
label: $t => `${$t('achievement')}: ${$t('achievementAridAuthority')}`,
modalId: 'generic-achievement',
data: {
achievement: 'aridAuthority', // defined manually until the server sends all the necessary data
},
},
ACHIEVEMENT_PARTY_UP: {
achievement: true,
@@ -187,6 +210,7 @@ const NOTIFICATIONS = {
data: {
message: $t => $t('achievement'),
modalText: $t => $t('achievementPartyUp'),
achievement: 'partyUp', // defined manually until the server sends all the necessary data
},
},
ACHIEVEMENT_PARTY_ON: {
@@ -196,6 +220,7 @@ const NOTIFICATIONS = {
data: {
message: $t => $t('achievement'),
modalText: $t => $t('achievementPartyOn'),
achievement: 'partyOn', // defined manually until the server sends all the necessary data
},
},
ACHIEVEMENT_BEAST_MASTER: {
@@ -205,6 +230,7 @@ const NOTIFICATIONS = {
data: {
message: $t => $t('achievement'),
modalText: $t => $t('beastAchievement'),
achievement: 'beastMaster', // defined manually until the server sends all the necessary data
},
},
ACHIEVEMENT_MOUNT_MASTER: {
@@ -214,6 +240,7 @@ const NOTIFICATIONS = {
data: {
message: $t => $t('achievement'),
modalText: $t => $t('mountAchievement'),
achievement: 'mountMaster', // defined manually until the server sends all the necessary data
},
},
ACHIEVEMENT_TRIAD_BINGO: {
@@ -223,27 +250,49 @@ const NOTIFICATIONS = {
data: {
message: $t => $t('achievement'),
modalText: $t => $t('triadBingoAchievement'),
achievement: 'triadBingo', // defined manually until the server sends all the necessary data
},
},
ACHIEVEMENT_MONSTER_MAGUS: {
achievement: true,
label: $t => `${$t('achievement')}: ${$t('achievementMonsterMagus')}`,
modalId: 'generic-achievement',
data: {
achievement: 'monsterMagus', // defined manually until the server sends all the necessary data
},
},
ACHIEVEMENT_UNDEAD_UNDERTAKER: {
achievement: true,
label: $t => `${$t('achievement')}: ${$t('achievementUndeadUndertaker')}`,
modalId: 'generic-achievement',
data: {
achievement: 'undeadUndertaker', // defined manually until the server sends all the necessary data
},
},
ACHIEVEMENT: { // data filled in handleUserNotifications
achievement: true,
modalId: 'generic-achievement',
label: null, // data filled in handleUserNotifications
data: {
message: $t => $t('achievement'),
modalText: null, // data filled in handleUserNotifications
},
},
ACHIEVEMENT_PRIMED_FOR_PAINTING: {
achievement: true,
label: $t => `${$t('achievement')}: ${$t('achievementPrimedForPainting')}`,
modalId: 'generic-achievement',
data: {
achievement: 'primedForPainting', // defined manually until the server sends all the necessary data
},
},
ACHIEVEMENT_PEARLY_PRO: {
achievement: true,
label: $t => `${$t('achievement')}: ${$t('achievementPearlyPro')}`,
modalId: 'generic-achievement',
data: {
achievement: 'pearlyPro', // defined manually until the server sends all the necessary data
},
},
};
@@ -276,6 +325,7 @@ export default {
lostMasterclasser,
mindOverMatter,
justAddWater,
onboardingComplete,
},
mixins: [notifications, guide],
data () {
@@ -302,7 +352,7 @@ export default {
'GENERIC_ACHIEVEMENT', 'ACHIEVEMENT_PARTY_UP', 'ACHIEVEMENT_PARTY_ON', 'ACHIEVEMENT_BEAST_MASTER',
'ACHIEVEMENT_MOUNT_MASTER', 'ACHIEVEMENT_TRIAD_BINGO', 'ACHIEVEMENT_DUST_DEVIL', 'ACHIEVEMENT_ARID_AUTHORITY',
'ACHIEVEMENT_MONSTER_MAGUS', 'ACHIEVEMENT_UNDEAD_UNDERTAKER', 'ACHIEVEMENT_PRIMED_FOR_PAINTING',
'ACHIEVEMENT_PEARLY_PRO', 'GENERIC_ACHIEVEMENT',
'ACHIEVEMENT_PEARLY_PRO', 'ACHIEVEMENT', 'ONBOARDING_COMPLETE',
].forEach(type => {
handledNotifications[type] = true;
});
@@ -478,7 +528,7 @@ export default {
this.text(config.label(this.$t), () => {
this.notificationData = data;
this.$root.$emit('bv::show::modal', config.modalId);
}, false);
}, true, 10000);
}
},
debounceCheckUserAchievements: debounce(function debounceCheck () {
@@ -713,6 +763,18 @@ export default {
case 'GENERIC_ACHIEVEMENT':
this.showNotificationWithModal(notification);
break;
case 'ACHIEVEMENT': { // generic achievement
const { achievement } = notification.data;
const upperCaseAchievement = achievement.charAt(0).toUpperCase() + achievement.slice(1);
const achievementTitleKey = `achievement${upperCaseAchievement}`;
NOTIFICATIONS.ACHIEVEMENT.label = $t => `${$t('achievement')}: ${$t(achievementTitleKey)}`;
NOTIFICATIONS.ACHIEVEMENT.data.modalText = $t => $t(achievementTitleKey);
this.showNotificationWithModal(notification);
// Set the achievement as it's not defined in the user schema
Vue.set(this.user.achievements, achievement, true);
break;
}
case 'CRON':
if (notification.data) {
if (notification.data.hp) this.hp(notification.data.hp, 'hp');
@@ -742,6 +804,20 @@ export default {
this.$root.$emit('bv::show::modal', 'login-incentives');
}
break;
case 'ONBOARDING_COMPLETE':
// Award rewards
onOnboardingComplete(this.user);
// If the user cronned in the last 3 minutes
// Don't show too many modals on app load
// Use notification panel
if (moment().diff(this.user.lastCron, 'minutes') < 3) {
markAsRead = false;
} else {
// Otherwise use the modal
this.$root.$emit('bv::show::modal', 'onboarding-complete');
}
break;
}
if (markAsRead) notificationsToRead.push(notification.id);
+44 -25
View File
@@ -53,9 +53,9 @@
<div class="strike">
<span>{{ $t('or') }}</span>
</div>
<div
<form
class="form"
@keyup.enter="register()"
@submit.prevent.stop="register()"
>
<p class="form-text">
{{ $t('usernameLimitations') }}
@@ -63,7 +63,7 @@
<input
id="usernameInput"
v-model="username"
class="form-control"
class="form-control input-with-error"
type="text"
:placeholder="$t('username')"
:class="{'input-valid': usernameValid, 'input-invalid': usernameInvalid}"
@@ -85,32 +85,49 @@
>
<input
v-model="password"
class="form-control"
class="form-control input-with-error"
type="password"
:placeholder="$t('password')"
:class="{'input-valid': password.length > 3}"
:class="{
'input-valid': passwordValid,
'input-invalid': passwordInvalid,
}"
>
<div
v-if="passwordInvalid"
class="input-error"
>
{{ $t('minPasswordLength') }}
</div>
<input
v-model="passwordConfirm"
class="form-control"
class="form-control input-with-error"
type="password"
:placeholder="$t('confirmPassword')"
:class="{
'input-invalid': passwordConfirmInvalid,
'input-valid': passwordConfirmValid}"
>
<div
v-if="passwordConfirmInvalid"
class="input-error"
>
{{ $t('passwordConfirmationMatch') }}
</div>
<p
v-once
class="form-text"
v-html="$t('termsAndAgreement')"
></p>
<button
class="sign-up"
class="btn btn-block btn-info sign-up"
:disabled="signupFormInvalid"
type="submit"
@click="register()"
>
{{ $t('signup') }}
</button>
</div>
</form>
</div>
<div class="col-12">
<div
@@ -519,7 +536,7 @@
transition: border .5s, color .5s;
}
#usernameInput.input-invalid {
.input-invalid.input-with-error {
margin-bottom: 0.5em;
}
@@ -537,21 +554,9 @@
background-color: #36205d;
}
button.sign-up {
width: 100%;
height: 48px;
color: #fff;
border: none;
border-radius: 2px;
background-color: #2995cd;
font-size: 16px;
transition: all .5s ease;
}
.sign-up:hover {
background-color: #50b5e9;
box-shadow: 0 4px 4px 0 rgba(26, 24, 29, 0.16), 0 1px 8px 0 rgba(26, 24, 29, 0.12);
cursor: pointer;
.sign-up {
padding-top: 11px;
padding-bottom: 11px;
}
::-webkit-input-placeholder { /* Chrome/Opera/Safari */
@@ -769,7 +774,6 @@
color: #fff;
font-size: 90%;
width: 100%;
text-align: center;
margin-bottom: 1em;
}
</style>
@@ -796,6 +800,7 @@ import lifehacker from '@/assets/images/home/lifehacker.svg';
import makeuseof from '@/assets/images/home/make-use-of.svg';
import thenewyorktimes from '@/assets/images/home/the-new-york-times.svg';
import * as Analytics from '@/libs/analytics';
import { MINIMUM_PASSWORD_LENGTH } from '@/../../common/script/constants';
export default {
data () {
@@ -844,6 +849,14 @@ export default {
if (this.username.length < 1) return false;
return !this.usernameValid;
},
passwordValid () {
if (this.password.length <= 0) return false;
return this.password.length >= MINIMUM_PASSWORD_LENGTH;
},
passwordInvalid () {
if (this.password.length <= 0) return false;
return this.password.length < MINIMUM_PASSWORD_LENGTH;
},
passwordConfirmValid () {
if (this.passwordConfirm.length <= 3) return false;
return this.passwordConfirm === this.password;
@@ -852,6 +865,12 @@ export default {
if (this.passwordConfirm.length <= 3) return false;
return this.passwordConfirm !== this.password;
},
signupFormInvalid () {
return this.usernameInvalid
|| this.emailInvalid
|| this.passwordInvalid
|| this.passwordConfirmInvalid;
},
},
watch: {
username () {
@@ -85,10 +85,6 @@
}
}
#bottom-wrap.purple-4 {
background-color: #271b3d;
}
#purple-footer {
background-color: #271b3d;
@@ -115,33 +111,6 @@
}
}
#bottom-wrap {
padding-top: 10em;
}
#bottom-background {
position: relative;
.seamless_mountains_demo_repeat {
background-image: url('~@/assets/images/auth/seamless_mountains_demo.png');
background-repeat: repeat-x;
width: 100%;
height: 300px;
position: absolute;
z-index: 0;
bottom: 0;
}
.midground_foreground_extended2 {
background-image: url('~@/assets/images/auth/midground_foreground_extended2.png');
position: relative;
width: 1500px;
max-width: 100%;
height: 150px;
margin: 0 auto;
}
}
.static-wrapper {
.container-fluid {
margin: 5em 2em 2em 2em;
@@ -171,6 +140,39 @@
}
</style>
<style lang="scss" scoped>
#bottom-wrap.purple-4 {
background-color: #271b3d;
}
#bottom-wrap {
padding-top: 10em;
}
#bottom-background {
position: relative;
.seamless_mountains_demo_repeat {
background-image: url('~@/assets/images/auth/seamless_mountains_demo.png');
background-repeat: repeat-x;
width: 100%;
height: 300px;
position: absolute;
z-index: 0;
bottom: 0;
}
.midground_foreground_extended2 {
background-image: url('~@/assets/images/auth/midground_foreground_extended2.png');
position: relative;
width: 1500px;
max-width: 100%;
height: 150px;
margin: 0 auto;
}
}
</style>
<script>
import AppFooter from '@/components/appFooter';
import StaticHeader from './header.vue';
@@ -335,7 +335,6 @@ import BuyQuestModal from '@/components/shops/quests/buyQuestModal.vue';
import notifications from '@/mixins/notifications';
import { shouldDo } from '@/../../common/script/cron';
import inAppRewards from '@/../../common/script/libs/inAppRewards';
import spells from '@/../../common/script/content/spells';
import taskDefaults from '@/../../common/script/libs/taskDefaults';
import {
@@ -433,26 +432,6 @@ export default {
let watchRefresh = this.forceRefresh; // eslint-disable-line
const rewards = inAppRewards(this.user);
// Add season rewards if user is affected
// @TODO: Add buff conditional
const seasonalSkills = {
snowball: 'salt',
spookySparkles: 'opaquePotion',
shinySeed: 'petalFreePotion',
seafoam: 'sand',
};
for (const key in seasonalSkills) {
if (this.getUserBuffs(key)) {
const debuff = seasonalSkills[key];
const item = { ...spells.special[debuff] };
item.text = item.text();
item.notes = item.notes();
item.class = `shop_${key}`;
rewards.push(item);
}
}
return rewards;
},
hasRewardsList () {
@@ -0,0 +1,27 @@
/* eslint-disable import/no-extraneous-dependencies */
import { storiesOf } from '@storybook/vue';
import CountBadge from './countBadge.vue';
storiesOf('Count Badge', module)
.add('simple', () => ({
components: { CountBadge },
template: `
<div style="position: absolute; margin: 20px">
<count-badge :count="2" :show="true"></count-badge>
</div>
`,
}))
.add('bind count', () => ({
components: { CountBadge },
template: `
<div style="position: absolute; margin: 20px">
<count-badge :count="count" :show="true"></count-badge>
</div>
`,
data () {
return {
count: 3,
};
},
}));
@@ -50,8 +50,6 @@ A simplified dropdown component that doesn't rely on buttons as toggles like bo
.dropdown-menu {
cursor: auto;
box-shadow: 0 2px 2px 0 rgba($black, 0.16), 0 1px 4px 0 rgba($black, 0.12);
max-height: calc(100vh - 100px);
overflow: auto;
left: inherit;
right: 0px !important;
@@ -1,116 +0,0 @@
<template>
<div class="standard-page container">
<div
v-for="(category, key) in achievements"
:key="key"
class="row"
>
<h2 class="col-12">
{{ $t(key+'Achievs') }}
</h2>
<div
v-for="(achievment, achievmentKey) in category.achievements"
:key="achievmentKey"
class="col-3 text-center"
>
<div
class="achievement-container"
:data-popover-html="achievment.title + achievment.text"
popover-placement="achievPopoverPlacement"
popover-append-to-body="achievAppendToBody"
>
<div
popover-trigger="mouseenter"
:data-popover-html="achievment.title + achievment.text"
popover-placement="achievPopoverPlacement"
popover-append-to-body="achievAppendToBody"
>
<div
v-if="achievment.earned"
class="achievement"
:class="achievment.icon + '2x'"
>
<div
v-if="achievment.optionalCount"
class="counter badge badge-info stack-count"
>
{{ achievment.optionalCount }}
</div>
</div>
<div
v-if="!achievment.earned"
class="achievement achievement-unearned2x"
></div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-6">
<h2>Challeges Won</h2>
<div
v-for="chal in user.achievements.challenges"
:key="chal"
>
<span>{{ chal }}</span>
</div>
</div>
<div class="col-6">
<h2>Quests Completed</h2>
<div
v-for="(value, key) in user.achievements.quests"
:key="key"
>
<span>{{ content.quests[k].text() }}</span>
<span>{{ value }}</span>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
h2 {
margin-top: 2em;
}
.achievement-container {
margin-bottom: 1em;
padding: 2em;
background: #fff;
}
.achievement {
margin: 0 auto;
}
.counter.badge {
color: #fff;
position: absolute;
top: 0;
right: 0;
background-color: #ff944c;
}
</style>
<script>
import { mapState } from '@/libs/store';
import achievementsLib from '@/../../common/script/libs/achievements';
import Content from '@/../../common/script/content';
export default {
data () {
return {
achievements: {},
content: Content,
};
},
computed: {
...mapState({ user: 'user.data' }),
},
mounted () {
this.achievements = achievementsLib.getAchievementsForProfile(this.user);
},
};
</script>
@@ -300,50 +300,64 @@
<div
v-for="(category, key) in achievements"
:key="key"
class="row"
class="row category-row"
>
<h2 class="col-12 text-center">
<h3 class="col-12 text-center mb-3">
{{ $t(key+'Achievs') }}
</h2>
<div
v-for="(achievement, achievKey) in category.achievements"
:key="achievKey"
class="col-12 col-md-3 text-center"
>
<div
:id="achievKey + '-achievement'"
class="box achievement-container"
:class="{'achievement-unearned': !achievement.earned}"
>
<b-popover
:target="'#' + achievKey + '-achievement'"
triggers="hover"
placement="top"
>
<h4 class="popover-content-title">
{{ achievement.title }}
</h4>
<div
class="popover-content-text"
v-html="achievement.text"
></div>
</b-popover>
</h3>
<div class="col-12">
<div class="row achievements-row justify-content-center">
<div
v-if="achievement.earned"
class="achievement"
:class="achievement.icon + '2x'"
v-for="(achievement, achievKey) in achievementsCategory(key, category)"
:key="achievKey"
class="achievement-wrapper col text-center"
>
<div
v-if="achievement.optionalCount"
class="counter badge badge-info stack-count"
:id="achievKey + '-achievement'"
class="box achievement-container"
:class="{'achievement-unearned': !achievement.earned}"
>
{{ achievement.optionalCount }}
<b-popover
:target="'#' + achievKey + '-achievement'"
triggers="hover"
placement="top"
>
<h4 class="popover-content-title">
{{ achievement.title }}
</h4>
<div
class="popover-content-text"
v-html="achievement.text"
></div>
</b-popover>
<div
v-if="achievement.earned"
class="achievement"
:class="achievement.icon + '2x'"
>
<div
v-if="achievement.optionalCount"
class="counter badge badge-pill stack-count"
>
{{ achievement.optionalCount }}
</div>
</div>
<div
v-if="!achievement.earned"
class="achievement achievement-unearned achievement-unearned2x"
></div>
</div>
</div>
<div
v-if="!achievement.earned"
class="achievement achievement-unearned achievement-unearned2x"
></div>
v-if="achievementsCategories[key].number > 5"
class="btn btn-flat btn-show-more"
@click="toggleAchievementsCategory(key)"
>
{{ achievementsCategories[key].open ?
$t('hideAchievements', {category: $t(key+'Achievs')}) :
$t('showAllAchievements', {category: $t(key+'Achievs')})
}}
</div>
</div>
</div>
</div>
@@ -353,31 +367,38 @@
v-if="user.achievements.challenges"
class="col-12 col-md-6"
>
<div class="achievement-icon achievement-karaoke"></div>
<h2 class="text-center">
<div class="achievement-icon achievement-karaoke-2x"></div>
<h3 class="text-center mt-2 mb-4">
{{ $t('challengesWon') }}
</h2>
</h3>
<div
v-for="chal in user.achievements.challenges"
:key="chal"
class="achievement-list-item"
>
<span v-markdown="chal"></span>
<hr>
</div>
</div>
<div
v-if="user.achievements.quests"
class="col-12 col-md-6"
>
<div class="achievement-icon achievement-alien"></div>
<h2 class="text-center">
<div class="achievement-icon achievement-alien2x"></div>
<h3 class="text-center mt-2 mb-4">
{{ $t('questsCompleted') }}
</h2>
</h3>
<div
v-for="(value, key) in user.achievements.quests"
:key="key"
class="achievement-list-item d-flex justify-content-between"
>
<span>{{ content.quests[key].text() }} ({{ value }})</span>
<span>{{ content.quests[key].text() }}</span>
<span
v-if="value > 1"
class="badge badge-pill stack-count"
>
{{ value }}
</span>
</div>
</div>
</div>
@@ -519,37 +540,71 @@
}
#achievements {
.category-row {
margin-bottom: 34px;
&:last-child {
margin-bottom: 0px;
}
}
.achievements-row {
max-width: 590px;
margin: 0 auto;
}
.achievement-wrapper {
width: 94px;
max-width: 94px;
margin-right: 12px;
margin-left: 12px;
padding: 0px;
}
.box {
margin: 0 auto;
margin-top: 1em;
margin-bottom: 1em;
padding-top: 1.2em;
background: #fff;
background: $white;
}
hr {
margin-bottom: 48px;
margin-top: 48px;
}
.box.achievement-unearned {
background-color: #edecee;
}
h2 {
margin-top: 1em;
background-color: $gray-600;
}
.counter.badge {
position: absolute;
top: .5em;
right: 3em;
color: #fff;
background-color: #ff944c;
box-shadow: 0 1px 1px 0 rgba(26, 24, 29, 0.12);
min-width: 24px;
min-height: 24px;
border-radius: 2em;
padding: .5em;
top: -0.8em;
right: -0.5em;
color: $white;
background-color: $orange-100;
max-height: 24px;
}
.achievement-icon {
margin: 0 auto;
}
.achievement-list-item {
padding-top: 11px;
padding-bottom: 12px;
border-top: 1px solid $gray-500;
&:last-child {
border-bottom: 1px solid $gray-500;
}
.badge {
margin-right: 8px;
background: $gray-600;
color: $gray-300;
}
}
}
.achievement {
@@ -699,6 +754,7 @@ export default {
},
selectedPage: 'profile',
achievements: {},
achievementsCategories: {}, // number, open
content: Content,
user: undefined,
};
@@ -783,13 +839,23 @@ export default {
// @TODO: this common code should handle the above
this.achievements = achievementsLib.getAchievementsForProfile(user);
const achievementsCategories = {};
Object.keys(this.achievements).forEach(category => {
achievementsCategories[category] = {
open: false,
number: Object.keys(this.achievements[category].achievements).length,
};
});
this.achievementsCategories = achievementsCategories;
// @TODO For some reason markdown doesn't seem to be handling numbers or maybe undefined?
user.profile.blurb = user.profile.blurb ? `${user.profile.blurb}` : '';
this.user = user;
},
selectPage (page) {
this.selectedPage = page;
this.selectedPage = page || 'profile';
window.history.replaceState(null, null, '');
},
sendMessage () {
@@ -906,6 +972,27 @@ export default {
showAllocation () {
return this.user._id === this.userLoggedIn._id && this.hasClass;
},
achievementsCategory (categoryKey, category) {
const achievementsKeys = Object.keys(category.achievements);
if (this.achievementsCategories[categoryKey].open === true) {
return category.achievements;
}
const fiveAchievements = achievementsKeys.slice(0, 5);
const categoryAchievements = {};
fiveAchievements.forEach(key => {
categoryAchievements[key] = category.achievements[key];
});
return categoryAchievements;
},
toggleAchievementsCategory (categoryKey) {
const status = this.achievementsCategories[categoryKey].open;
this.achievementsCategories[categoryKey].open = !status;
},
},
};
</script>
@@ -24,6 +24,12 @@ export default {
components: {
profile,
},
props: ['userId', 'startingPage'],
props: {
userId: String,
startingPage: {
type: String,
default: 'profile',
},
},
};
</script>