Merge branch 'develop' into release
This commit is contained in:
@@ -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');
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user