feat(usernames): verification step during Justin intro for social users

This commit is contained in:
Sabe Jones
2018-10-27 12:37:22 -05:00
parent 5299c8d406
commit bb90dde1b6
9 changed files with 317 additions and 251 deletions

View File

@@ -65,6 +65,7 @@ input, textarea, input.form-control, textarea.form-control {
padding-right: 40px;
background-image: url(~client/assets/svg/for-css/alert.svg);
background-size: 16px 16px;
margin-bottom: 0.1rem;
}
}

View File

@@ -5,7 +5,7 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
h3(v-once) {{$t('welcomeTo')}}
.svg-icon.logo(v-html='icons.logoPurple')
.avatar-section.row(:class='{"page-2": modalPage === 2}')
.avatar-section.row(v-if='modalPage > 1 || user.auth.local.email', :class='{"page-2": modalPage === 2}')
.col-6.offset-3
.user-creation-bg(v-if='!editing')
avatar(:member='user', :avatarOnly='!editing', :class='{"edit-avatar": editing}')
@@ -187,18 +187,18 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
#extra.section.container.customize-section(v-if='activeTopPage === "extra"')
.row.sub-menu
.col-3.offset-1.text-center.sub-menu-item(@click='changeSubPage("glasses")', :class='{active: activeSubPage === "glasses"}')
strong(v-once) {{$t('glasses')}}
strong(v-once) {{ $t('glasses') }}
.col-4.text-center.sub-menu-item(@click='changeSubPage("wheelchair")', :class='{active: activeSubPage === "wheelchair"}')
strong(v-once) {{$t('wheelchair')}}
strong(v-once) {{ $t('wheelchair') }}
.col-3.text-center.sub-menu-item(@click='changeSubPage("flower")', :class='{active: activeSubPage === "flower"}')
strong(v-once) {{$t('accent')}}
strong(v-once) {{ $t('accent') }}
.row.sub-menu(v-if='editing')
.col-4.text-center.sub-menu-item(@click='changeSubPage("ears")' :class='{active: activeSubPage === "ears"}')
strong(v-once) {{$t('animalEars')}}
strong(v-once) {{ $t('animalEars') }}
.col-4.text-center.sub-menu-item(@click='changeSubPage("tails")' :class='{active: activeSubPage === "tails"}')
strong(v-once) {{$t('animalTails')}}
strong(v-once) {{ $t('animalTails') }}
.col-4.text-center.sub-menu-item(@click='changeSubPage("headband")' :class='{active: activeSubPage === "headband"}')
strong(v-once) {{$t('headband')}}
strong(v-once) {{ $t('headband') }}
#glasses.row(v-if='activeSubPage === "glasses"')
.col-12.customize-options
.option(v-for='option in eyewear', :class='{active: option.active}')
@@ -305,7 +305,7 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
)
span.svg-icon.inline.icon-12.color(v-html="icons.pin")
.purchase-background.set(v-if='!ownsSet("background", set.items) && set.identifier !== "incentiveBackgrounds"' @click='unlock(setKeys("background", set.items))')
span.label Purchase Set
span.label {{ $t('purchaseAll') }}
.svg-icon.gem(v-html='icons.gem')
span.price 15
.row.customize-menu(v-if='filterBackgrounds')
@@ -320,7 +320,7 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
.container.interests-section(v-if='modalPage === 3 && !editing')
.section.row
.col-12.text-center
h2 I want to work on:
h2 {{ $t('wantToWorkOn') }}
.section.row
.col-6
.task-option
@@ -353,28 +353,36 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
input.custom-control-input#self_care(type="checkbox", value='self_care', v-model='taskCategories')
label.custom-control-label(v-once, for="self_care") {{ $t('self_care') }}
.section.row.justin-message-section(:class='{top: modalPage > 1}', v-if='!editing')
.col-12
.justin-message.d-flex.flex-column.justify-content-center
.featured-label
span.rectangle
span.text Justin
span.rectangle
.npc_justin_textbox
.section.d-flex.justify-content-center(:class='{top: modalPage > 1}', v-if='!editing')
.justin-section.d-flex.align-items-center
.featured-label
span.rectangle
span.text Justin
span.rectangle
.justin-message
.corner-decoration(:style="{top: '-2px', right: '-2px'}")
.corner-decoration(:style="{top: '-2px', left: '-2px'}")
.corner-decoration(:style="{bottom: '-2px', right: '-2px'}")
.corner-decoration(:style="{bottom: '-2px', left: '-2px'}")
div(v-if='modalPage === 1')
p(v-once) {{$t('justinIntroMessage1')}}
p(v-once) {{$t('justinIntroMessage2')}}
p(v-once, v-html='$t("justinIntroMessage1")')
p(v-once, v-if='user.auth.local.email') {{ $t('justinIntroMessage2') }}
p(v-once, v-if='!user.auth.local.email') {{ $t('justinIntroMessageUsername') }}
div(v-if='modalPage === 2')
p So how would you like to look? Dont worry, you can change this later.
p {{ $t('justinIntroMessageAppearance') }}
div(v-if='modalPage === 3')
p(v-once) {{$t('justinIntroMessage3')}}
p(v-once) {{ $t('justinIntroMessage3') }}
.npc-justin-textbox
.section.mr-5.ml-5(v-if='modalPage === 1 && !user.auth.local.email')
username-form(@usernameConfirmed='modalPage += 1', :avatarIntro='"true"')
.small.text-center(v-html="$t('usernameTOSRequirements')")
.section.container.footer(v-if='!editing')
.row
.section.container.footer
.row(v-if='!editing && !(modalPage === 1 && !user.auth.local.email)')
.col-3.offset-1.text-center
div(v-if='modalPage > 1', @click='prev()')
.prev-arrow
.prev(v-once) {{$t('prev')}}
.prev(v-once) {{ $t('prev') }}
.col-4.text-center.circles
.circle(:class="{active: modalPage === 1}")
.circle(:class="{active: modalPage === 2}")
@@ -390,12 +398,9 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
<style>
/* @TODO do not rely on avatar-modal___BV_modal_body_,
it already changed once when bootstrap-vue reached version 1 */
.page-2 #avatar-modal___BV_modal_body_ {
margin-top: 9em;
}
.page-2 .modal-content {
margin-top: 7em;
.page-2 #avatar-modal___BV_modal_body_ {
margin-top: 2rem;
}
#avatar-modal___BV_modal_body_, #avatar-modal___BV_modal_body_ {
@@ -421,6 +426,25 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
position: relative;
}
.corner-decoration {
position: absolute;
width: 6px;
height: 6px;
background-color: #ffbe5d;
border: inherit;
outline: inherit;
}
.small {
color: $gray-200;
}
h3 {
font-size: 20px;
font-weight: normal;
color: $gray-200;
}
.purchase-all {
margin-bottom: 1em;
}
@@ -444,7 +468,7 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
.logo {
width: 190px;
margin: 0 auto;
margin: 0 auto 1.25em;
}
.user-creation-bg {
@@ -464,32 +488,22 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
left: 9.2em;
}
.justin-message {
background-image: url('~client/assets/svg/for-css/tutorial-border.svg');
height: 144px;
width: 400px;
padding: 2em;
margin: 0 auto;
.justin-section {
position: relative;
}
.featured-label {
position: absolute;
top: -1em;
.text {
min-height: auto;
color: $white;
}
}
.npc_justin_textbox {
position: absolute;
right: 1em;
top: -3.6em;
width: 48px;
height: 52px;
background-image: url('~client/assets/images/justin_textbox.png');
}
.justin-message {
border-color: #ffa623;
border-style: solid;
border-width: 2px;
outline-color: #b36213;
outline-style: solid;
outline-width: 2px;
position: relative;
padding: 2em;
margin: 2px;
height: 100%;
width: 400px;
p {
margin: auto;
@@ -500,15 +514,27 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
}
}
.justin-message-section {
margin-top: 4em;
margin-bottom: 2em;
.npc-justin-textbox {
position: absolute;
right: 1rem;
top: -3.1rem;
width: 48px;
height: 48px;
background-image: url('~client/assets/images/justin_textbox.png');
}
.justin-message-section.top {
.featured-label {
position: absolute;
top: -16em;
left: 3.5em;
top: -1rem;
left: 1.5rem;
border-radius: 2px;
margin: auto;
.text {
font-size: 12px;
min-height: auto;
color: $white;
}
}
.circles {
@@ -865,6 +891,7 @@ import get from 'lodash/get';
import groupBy from 'lodash/groupBy';
import { mapState } from 'client/libs/store';
import avatar from './avatar';
import usernameForm from './settings/usernameForm';
import { getBackgroundShopSets } from '../../common/script/libs/shops';
import unlock from '../../common/script/ops/unlock';
import buy from '../../common/script/ops/buy/buy';
@@ -1022,6 +1049,7 @@ export default {
components: {
avatar,
toggleSwitch,
usernameForm,
},
mounted () {
if (this.editing) this.modalPage = 2;

View File

@@ -0,0 +1,216 @@
<template lang="pug">
div
.form-group
.d-flex.align-items-center
label.mr-3(for='displayName') {{ $t('displayName') }}
.flex-grow-1
input#displayName.form-control(
type='text',
:placeholder="$t('newDisplayName')",
v-model='temporaryDisplayName',
@blur='restoreEmptyDisplayName()',
:class='{"is-invalid input-invalid": displayNameInvalid, "input-valid": displayNameValid, "text-darker": temporaryDisplayName.length > 0}')
.mb-3(v-if="displayNameIssues.length > 0")
.input-error.text-center(v-for="issue in displayNameIssues") {{ issue }}
.form-group
.d-flex.align-items-center
label.mr-3(for='username') {{ $t('username') }}
.flex-grow-1
.input-group-prepend.input-group-text @
input#username.form-control(
type='text',
:placeholder="$t('newUsername')",
v-model='temporaryUsername',
@blur='restoreEmptyUsername()',
:class='{"is-invalid input-invalid": usernameInvalid, "input-valid": usernameValid, "text-darker": temporaryUsername.length > 0}')
.mb-3(v-if="usernameIssues.length > 0")
.input-error.text-center(v-for="issue in usernameIssues") {{ issue }}
.small.text-center(v-if='!avatarIntro') {{ $t('usernameLimitations') }}
.row.justify-content-center
button.btn.btn-primary(type='submit', @click='submitNames()' :disabled='usernameCannotSubmit') {{ $t(avatarIntro ? 'getStarted' : 'saveAndConfirm') }}
</template>
<style lang="scss" scoped>
@import '~client/assets/scss/colors.scss';
button {
margin: 0.25rem auto 1rem;
}
.col-3 {
padding-right: 0rem;
}
.form-group {
background-color: $gray-700;
border-radius: 2px;
border: solid 1px $gray-500;
}
input {
border: 0px;
}
.input-error {
color: $red-50;
font-size: 90%;
width: 100%;
}
.input-group-prepend {
margin-right: 0px;
}
.input-group-text {
border: 0px;
background-color: $white;
color: $gray-300;
padding: 0rem 0rem 0rem 0.75rem;
}
label {
color: $gray-100;
font-weight: bold;
margin-bottom: 0rem;
margin-left: 1rem;
min-width: 90px;
}
.small {
color: $gray-200;
}
.text-darker {
color: $gray-50;
}
#username {
padding-left: 0.25rem;
}
</style>
<script>
import axios from 'axios';
import debounce from 'lodash/debounce';
import { mapState } from 'client/libs/store';
export default {
computed: {
...mapState({
user: 'user.data',
}),
displayNameInvalid () {
if (this.temporaryDisplayName.length < 1) return false;
return !this.displayNameValid;
},
displayNameValid () {
if (this.temporaryDisplayName.length < 1) return false;
return this.displayNameIssues.length === 0;
},
usernameCannotSubmit () {
if (this.temporaryUsername.length < 1) return true;
return !this.usernameValid || !this.displayNameValid;
},
usernameInvalid () {
if (this.temporaryUsername.length < 1) return false;
return !this.usernameValid;
},
usernameValid () {
if (this.temporaryUsername.length < 1) return false;
return this.usernameIssues.length === 0;
},
},
data () {
return {
displayNameIssues: [],
temporaryDisplayName: '',
temporaryUsername: '',
usernameIssues: [],
};
},
methods: {
async close () {
this.$root.$emit('habitica::resync-requested');
await this.$store.dispatch('user:fetch', {forceLoad: true});
this.$root.$emit('habitica::resync-completed');
if (this.avatarIntro) {
this.$emit('usernameConfirmed');
} else {
this.$root.$emit('bv::hide::modal', 'verify-username');
this.$router.go(0);
}
},
restoreEmptyDisplayName () {
if (this.temporaryDisplayName.length < 1) {
this.temporaryDisplayName = this.user.profile.name;
}
},
restoreEmptyUsername () {
if (this.temporaryUsername.length < 1) {
this.temporaryUsername = this.user.auth.local.username;
}
},
async submitNames () {
if (this.temporaryDisplayName !== this.user.profile.name) {
await axios.put('/api/v4/user/', {'profile.name': this.temporaryDisplayName});
}
await axios.put('/api/v4/user/auth/update-username', {username: this.temporaryUsername});
this.close();
},
validateDisplayName: debounce(function checkName (displayName) {
if (displayName.length <= 1 || displayName === this.user.profile.name) {
this.displayNameIssues = [];
return;
}
this.$store.dispatch('auth:verifyDisplayName', {
displayName,
}).then(res => {
if (res.issues !== undefined) {
this.displayNameIssues = res.issues;
} else {
this.displayNameIssues = [];
}
});
}, 500),
validateUsername: debounce(function checkName (username) {
if (username.length <= 1 || username === this.user.auth.local.username) {
this.usernameIssues = [];
return;
}
this.$store.dispatch('auth:verifyUsername', {
username,
}).then(res => {
if (res.issues !== undefined) {
this.usernameIssues = res.issues;
} else {
this.usernameIssues = [];
}
});
}, 500),
},
mounted () {
this.temporaryDisplayName = this.user.profile.name;
this.temporaryUsername = this.user.auth.local.username;
},
props: {
avatarIntro: {
type: Boolean,
default: false,
},
},
watch: {
temporaryDisplayName: {
handler () {
this.validateDisplayName(this.temporaryDisplayName);
},
deep: true,
},
temporaryUsername: {
handler () {
this.validateUsername(this.temporaryUsername);
},
deep: true,
},
},
};
</script>

View File

@@ -10,36 +10,7 @@
div.nametag-header(v-html='icons.helloNametag')
h2.text-center {{ $t('usernameTime') }}
p.text-center(v-html="$t('usernameInfo')")
.form-group
.row.align-items-center
.col-3
label(for='displayName') {{ $t('displayName') }}
.col-9
input#displayName.form-control(
type='text',
:placeholder="$t('newDisplayName')",
v-model='temporaryDisplayName',
@blur='restoreEmptyDisplayName()',
:class='{"is-invalid input-invalid": displayNameInvalid, "input-valid": displayNameValid, "text-darker": temporaryDisplayName.length > 0}')
.mb-3(v-if="displayNameIssues.length > 0")
.input-error.text-center(v-for="issue in displayNameIssues") {{ issue }}
.form-group
.row.align-items-center
.col-3
label(for='username') {{ $t('username') }}
.col-9
.input-group-prepend.input-group-text @
input#username.form-control(
type='text',
:placeholder="$t('newUsername')",
v-model='temporaryUsername',
@blur='restoreEmptyUsername()',
:class='{"is-invalid input-invalid": usernameInvalid, "input-valid": usernameValid, "text-darker": temporaryUsername.length > 0}')
.mb-3(v-if="usernameIssues.length > 0")
.input-error.text-center(v-for="issue in usernameIssues") {{ issue }}
.small.text-center {{ $t('usernameLimitations') }}
.row.justify-content-center
button.btn.btn-primary(type='submit', @click='submitNames()' :disabled='usernameCannotSubmit') {{ $t('saveAndConfirm') }}
username-form
.scene_veteran_pets.center-block
.small.text-center.mb-3 {{ $t('verifyUsernameVeteranPet') }}
.small.text-center.tos-footer(v-html="$t('usernameTOSRequirements')")
@@ -62,57 +33,15 @@
<style lang="scss" scoped>
@import '~client/assets/scss/colors.scss';
button {
margin: 1rem;
}
.center-block {
margin: 0 auto 1em auto;
}
.col-3 {
padding-right: 0rem;
}
.form-group {
background-color: $gray-700;
border-radius: 2px;
border: solid 1px $gray-500;
}
h2 {
color: $purple-200;
margin-top: 1.5rem;
}
input {
border: 0px;
}
.input-error {
color: $red-50;
font-size: 90%;
width: 100%;
}
.input-group-prepend {
margin-right: 0px;
}
.input-group-text {
border: 0px;
background-color: $white;
color: $gray-300;
padding: 0rem 0rem 0rem 0.75rem;
}
label {
color: $gray-100;
font-weight: bold;
margin-bottom: 0rem;
margin-left: 1rem;
}
.nametag-header {
background-color: $gray-700;
border-radius: 0.3rem 0.3rem 0rem 0rem;
@@ -122,17 +51,13 @@
}
p {
color: #686274;
color: $gray-100;
}
.small {
color: $gray-200;
}
.text-darker {
color: $gray-50;
}
.tos-footer {
background-color: $gray-700;
border-radius: 0rem 0rem 0.3rem 0.3rem;
@@ -140,129 +65,22 @@
margin-right: -3rem;
padding: 1rem 4rem 1rem 4rem;
}
#username {
padding-left: 0.25rem;
}
</style>
<script>
import axios from 'axios';
import debounce from 'lodash/debounce';
import helloNametag from 'assets/svg/hello-habitican.svg';
import { mapState } from 'client/libs/store';
import usernameForm from './usernameForm';
export default {
computed: {
...mapState({
user: 'user.data',
}),
displayNameInvalid () {
if (this.temporaryDisplayName.length < 1) return false;
return !this.displayNameValid;
},
displayNameValid () {
if (this.temporaryDisplayName.length < 1) return false;
return this.displayNameIssues.length === 0;
},
usernameCannotSubmit () {
if (this.temporaryUsername.length < 1) return true;
return !this.usernameValid || !this.displayNameValid;
},
usernameInvalid () {
if (this.temporaryUsername.length < 1) return false;
return !this.usernameValid;
},
usernameValid () {
if (this.temporaryUsername.length < 1) return false;
return this.usernameIssues.length === 0;
},
components: {
usernameForm,
},
data () {
return {
icons: Object.freeze({
helloNametag,
}),
displayNameIssues: [],
temporaryDisplayName: '',
temporaryUsername: '',
usernameIssues: [],
};
},
methods: {
async submitNames () {
if (this.temporaryDisplayName !== this.user.profile.name) {
await axios.put('/api/v4/user/', {'profile.name': this.temporaryDisplayName});
}
await axios.put('/api/v4/user/auth/update-username', {username: this.temporaryUsername});
this.close();
},
async close () {
this.$root.$emit('habitica::resync-requested');
await this.$store.dispatch('user:fetch', {forceLoad: true});
this.$root.$emit('habitica::resync-completed');
this.$root.$emit('bv::hide::modal', 'verify-username');
this.$router.go(0);
},
restoreEmptyDisplayName () {
if (this.temporaryDisplayName.length < 1) {
this.temporaryDisplayName = this.user.profile.name;
}
},
restoreEmptyUsername () {
if (this.temporaryUsername.length < 1) {
this.temporaryUsername = this.user.auth.local.username;
}
},
validateDisplayName: debounce(function checkName (displayName) {
if (displayName.length <= 1 || displayName === this.user.profile.name) {
this.displayNameIssues = [];
return;
}
this.$store.dispatch('auth:verifyDisplayName', {
displayName,
}).then(res => {
if (res.issues !== undefined) {
this.displayNameIssues = res.issues;
} else {
this.displayNameIssues = [];
}
});
}, 500),
validateUsername: debounce(function checkName (username) {
if (username.length <= 1 || username === this.user.auth.local.username) {
this.usernameIssues = [];
return;
}
this.$store.dispatch('auth:verifyUsername', {
username,
}).then(res => {
if (res.issues !== undefined) {
this.usernameIssues = res.issues;
} else {
this.usernameIssues = [];
}
});
}, 500),
},
mounted () {
this.temporaryDisplayName = this.user.profile.name;
this.temporaryUsername = this.user.auth.local.username;
},
watch: {
temporaryDisplayName: {
handler () {
this.validateDisplayName(this.temporaryDisplayName);
},
deep: true,
},
temporaryUsername: {
handler () {
this.validateUsername(this.temporaryUsername);
},
deep: true,
},
},
};
</script>

View File

@@ -7,7 +7,7 @@
"noPhoto": "This Habitican hasn't added a photo.",
"other": "Other",
"fullName": "Full Name",
"displayName": "Display Name",
"displayName": "Display name",
"changeDisplayName": "Change Display Name",
"newDisplayName": "New Display Name",
"displayPhoto": "Photo",

View File

@@ -329,7 +329,7 @@
"joinToday": "Join Habitica Today",
"featuredIn": "Featured in",
"signup": "Sign Up",
"getStarted": "Get Started",
"getStarted": "Get Started!",
"mobileApps": "Mobile Apps",
"learnMore": "Learn More"
}

View File

@@ -254,6 +254,7 @@
"resetFilters": "Clear all filters",
"applyFilters": "Apply Filters",
"wantToWorkOn": "I want to work on:",
"categories": "Categories",
"habiticaOfficial": "Habitica Official",
"animals": "Animals",

View File

@@ -6,9 +6,11 @@
"welcomeTo": "Welcome to",
"welcomeBack": "Welcome back!",
"justin": "Justin",
"justinIntroMessage1": "Hello there! You must be new here. My name is Justin, your guide to Habitica.",
"justinIntroMessage1": "Hello there! You must be new here. My name is <strong>Justin</strong>, and I'll be your guide in Habitica.",
"justinIntroMessage2": "To start, you'll need to create an avatar.",
"justinIntroMessage3": "Great! Now, what are you interested in working on throughout this journey?",
"justinIntroMessageUsername": "Before we begin, lets figure out what to call you. Below youll find a display name and username Ive generated for you. After youve picked a display name and username, well get started by creating an avatar!",
"justinIntroMessageAppearance": "So how would you like to look? Dont worry, you can change this later.",
"introTour": "Here we are! I've filled out some Tasks for you based on your interests, so you can get started right away. Click a Task to edit or add new Tasks to fit your routine!",
"prev": "Prev",
"next": "Next",

View File

@@ -210,7 +210,7 @@
"haveCouponCode": "Do you have a coupon code?",
"subscriptionAlreadySubscribedLeadIn": "Thanks for subscribing!",
"subscriptionAlreadySubscribed1": "To see your subscription details and cancel, renew, or change your subscription, please go to <a href='/user/settings/subscription'>User icon &gt; Settings &gt; Subscription</a>.",
"purchaseAll": "Purchase All",
"purchaseAll": "Purchase Set",
"gemsPurchaseNote": "Subscribers can buy gems for gold in the Market! For easy access, you can also pin the gem to your Rewards column.",
"gemsRemaining": "gems remaining",
"notEnoughGemsToBuy": "You are unable to buy that amount of gems"