web/satellite: Add user account tabs to signup ui

- toggle logic to trigger visual tab change
- toggling tabs renders appropriate fields
- error handling for professional account inputs
- successfully create professional user account

Goal is to be able to target users with specific professional focused communication.

Change-Id: Iffbeb712dac24ea1a83bb374740e0c1cd2100663
This commit is contained in:
Malcolm Bouzi 2021-02-08 09:38:01 -05:00
parent c290e5ac9a
commit a7ca78a519
7 changed files with 214 additions and 25 deletions

View File

@ -205,7 +205,7 @@ export class AuthHttpApi {
* @returns id of created user
* @throws Error
*/
public async register(user: { fullName: string; shortName: string; email: string; partner: string; partnerId: string; password: string }, secret: string): Promise<string> {
public async register(user: {fullName: string; shortName: string; email: string; partner: string; partnerId: string; password: string; isProfessional: boolean; position: string; companyName: string; employeeCount: string}, secret: string): Promise<string> {
const path = `${this.ROOT_PATH}/register`;
const body = {
secret: secret,
@ -215,6 +215,10 @@ export class AuthHttpApi {
email: user.email,
partner: user.partner ? user.partner : '',
partnerId: user.partnerId ? user.partnerId : '',
isProfessional: user.isProfessional,
position: user.position,
companyName: user.companyName,
employeeCount: user.employeeCount,
};
const response = await this.http.post(path, JSON.stringify(body));

View File

@ -19,7 +19,25 @@
:style="style.inputStyle"
@focus="showPasswordStrength"
@blur="hidePasswordStrength"
@click="showOptions"
:optionsShown="optionsShown"
@optionsList="optionsList"
/>
<!-- Shown if there are input choice options -->
<InputCaret v-if="optionsList.length > 0" class="headerless-input__caret" />
<ul v-click-outside="hideOptions" class="headerless-input__options-wrapper" v-if="optionsShown">
<li
class="headerless-input__option"
@click="chooseOption(option)"
v-for="(option, index) in optionsList"
:key="index"
>
{{option}}
</li>
</ul>
<!-- end of option render logic-->
<!--2 conditions of eye image (crossed or not) -->
<PasswordHiddenIcon
class="input-wrap__image"
@ -38,6 +56,7 @@
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import InputCaret from '@/../static/images/common/caret.svg';
import PasswordHiddenIcon from '@/../static/images/common/passwordHidden.svg';
import PasswordShownIcon from '@/../static/images/common/passwordShown.svg';
import ErrorIcon from '@/../static/images/register/ErrorInfo.svg';
@ -45,6 +64,7 @@ import ErrorIcon from '@/../static/images/register/ErrorInfo.svg';
// Custom input component for login page
@Component({
components: {
InputCaret,
ErrorIcon,
PasswordHiddenIcon,
PasswordShownIcon,
@ -73,6 +93,12 @@ export default class HeaderlessInput extends Vue {
protected readonly error: string;
@Prop({default: Number.MAX_SAFE_INTEGER})
protected readonly maxSymbols: number;
@Prop({default: []})
protected readonly optionsList: [string];
@Prop({default: false})
protected optionsShown: boolean;
@Prop({default: false})
protected inputClicked: boolean;
@Prop({default: false})
private readonly isWhite: boolean;
@ -120,6 +146,36 @@ export default class HeaderlessInput extends Vue {
this.type = this.isPasswordShown ? this.textType : this.passwordType;
}
/**
* Chose a dropdown option as the input value.
*/
public chooseOption(option: string): void {
this.value = option;
this.$emit('setData', this.value);
this.optionsShown = false;
}
/**
* Show dropdown options when the input is clicked, if they exist.
*/
public showOptions(): void {
if (this.optionsList.length > 0) {
this.optionsShown = true;
this.inputClicked = true;
}
}
/**
* Hide the dropdown options from view when there is a click outside of the dropdown.
*/
public hideOptions(): void {
if (this.optionsList.length > 0 && !this.inputClicked && this.optionsShown) {
this.optionsShown = false;
this.inputClicked = false;
}
this.inputClicked = false;
}
public get isLabelShown(): boolean {
return !!(!this.error && this.label);
}
@ -170,6 +226,72 @@ export default class HeaderlessInput extends Vue {
fill: #2683ff !important;
}
}
.headerless-input {
font-size: 16px;
line-height: 21px;
resize: none;
height: 46px;
padding: 0 30px 0 0;
width: calc(100% - 30px) !important;
text-indent: 20px;
border: 1px solid rgba(56, 75, 101, 0.4);
border-radius: 6px;
&__caret {
position: absolute;
right: 28px;
bottom: 18px;
}
&__options-wrapper {
border: 1px solid rgba(56, 75, 101, 0.4);
position: absolute;
width: 100%;
top: 89px;
padding: 0;
background: #fff;
z-index: 21;
border-radius: 6px;
list-style: none;
border-top-right-radius: 0;
border-top-left-radius: 0;
border-top: none;
height: 176px;
margin-top: 0;
}
&__option {
cursor: pointer;
padding: 20px 22px;
&:hover {
background: #2582ff;
color: #fff;
}
}
}
.headerless-input::placeholder {
color: #384b65;
opacity: 0.4;
}
&:focus-within {
.headerless-input {
position: relative;
z-index: 22;
&__options-wrapper {
border-top: 3px solid #145ecc;
}
&__caret {
z-index: 23;
}
}
}
}
.label-container {
@ -199,23 +321,6 @@ export default class HeaderlessInput extends Vue {
}
}
.headerless-input {
font-size: 16px;
line-height: 21px;
resize: none;
height: 46px;
padding: 0 30px 0 0;
width: calc(100% - 30px) !important;
text-indent: 20px;
border: 1px solid rgba(56, 75, 101, 0.4);
border-radius: 6px;
}
.headerless-input::placeholder {
color: #384b65;
opacity: 0.4;
}
.inputError::placeholder {
color: #eb5757;
opacity: 0.4;

View File

@ -34,6 +34,10 @@ export class User {
public partnerId: string = '',
public password: string = '',
public projectLimit: number = 0,
public isProfessional: boolean = false,
public position: string = '',
public companyName: string = '',
public employeeCount: string = '',
) {}
public getFullName(): string {

View File

@ -55,8 +55,12 @@ export default class RegisterArea extends Vue {
private emailError: string = '';
private passwordError: string = '';
private repeatedPasswordError: string = '';
private companyNameError: string = '';
private employeeCountError: string = '';
private positionError: string = '';
private isTermsAcceptedError: boolean = false;
private isLoading: boolean = false;
private isProfessional: boolean = false;
// Only for beta sats (like US2).
private areBetaTermsAcceptedError: boolean = false;
@ -68,6 +72,10 @@ export default class RegisterArea extends Vue {
// tardigrade logic
public isDropdownShown: boolean = false;
// Employee Count dropdown options
public employeeCountOptions = ['1-50', '51-1000', '1001+'];
public optionsShown = false;
/**
* Lifecycle hook before vue instance is created.
* Initializes google tag manager (Tardigrade).
@ -161,7 +169,6 @@ export default class RegisterArea extends Vue {
return;
}
await this.createUser();
this.isLoading = false;
@ -221,6 +228,37 @@ export default class RegisterArea extends Vue {
return this.$store.state.appStateModule.isBetaSatellite;
}
/**
* Sets user's company name field from value string.
*/
public setCompanyName(value: string): void {
this.user.companyName = value.trim();
this.companyNameError = '';
}
/**
* Sets user's company size field from value string.
*/
public setEmployeeCount(value: string): void {
this.user.employeeCount = value;
this.employeeCountError = '';
}
/**
* Sets user's position field from value string.
*/
public setPosition(value: string): void {
this.user.position = value.trim();
this.positionError = '';
}
/**
* toggle user account type
*/
public toggleAccountType(value: boolean): void {
this.isProfessional = value;
}
/**
* Validates input values to satisfy expected rules.
*/
@ -242,6 +280,25 @@ export default class RegisterArea extends Vue {
isNoErrors = false;
}
if (this.isProfessional) {
if (!this.user.companyName.trim()) {
this.companyNameError = 'No Company Name filled in';
isNoErrors = false;
}
if (!this.user.position.trim()) {
this.positionError = 'No Position filled in';
isNoErrors = false;
}
if (!this.user.employeeCount.trim()) {
this.employeeCountError = 'No Company Size filled in';
isNoErrors = false;
}
}
if (this.repeatedPassword !== this.password) {
this.repeatedPasswordError = 'Password doesn\'t match';
isNoErrors = false;
@ -265,14 +322,23 @@ export default class RegisterArea extends Vue {
* Creates user and toggles successful registration area visibility.
*/
private async createUser(): Promise<void> {
this.user.isProfessional = this.isProfessional;
try {
this.userId = await this.auth.register(this.user, this.secret);
LocalData.setUserId(this.userId);
this.$segment.identify(this.userId, {
email: this.$store.getters.user.email,
});
if (this.user.isProfessional) {
this.$segment.identify(this.userId, {
email: this.$store.getters.user.email,
position: this.$store.getters.user.position,
company_name: this.$store.getters.user.companyName,
employee_count: this.$store.getters.user.employeeCount,
});
} else {
this.$segment.identify(this.userId, {
email: this.$store.getters.user.email,
});
}
const verificationPageURL: string = MetaUtils.getMetaContent('verification-page-url');
if (verificationPageURL) {
@ -285,7 +351,6 @@ export default class RegisterArea extends Vue {
return;
}
await this.$store.dispatch(APP_STATE_ACTIONS.TOGGLE_SUCCESSFUL_REGISTRATION);
} catch (error) {
await this.$notify.error(error.message);

View File

@ -0,0 +1,3 @@
<svg width="15" height="7" viewBox="0 0 15 7" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.5 7L0.138785 0.25L14.8612 0.250001L7.5 7Z" fill="#384761"/>
</svg>

After

Width:  |  Height:  |  Size: 173 B

View File

@ -11,6 +11,8 @@ exports[`ApiKeysCreationPopup renders correctly 1`] = `
</div> <input placeholder="Enter API Key Name" type="text" class="headerless-input" style="width: 100%; height: 48px;">
<!---->
<!---->
<!---->
<!---->
</div>
<div class="next-button container" style="width: 128px; height: 48px;"><span class="label">Next &gt;</span></div>
<div class="new-api-key__close-cross-container"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">

View File

@ -9,6 +9,8 @@ exports[`HeaderlessInput.vue renders correctly with default props 1`] = `
</div> <input placeholder="default" type="text" class="headerless-input" style="width: 100%; height: 48px;">
<!---->
<!---->
<!---->
<!---->
</div>
`;
@ -18,7 +20,9 @@ exports[`HeaderlessInput.vue renders correctly with isPassword prop 1`] = `
<!---->
<!---->
<!---->
</div> <input placeholder="default" type="password" class="headerless-input password" style="width: 100%; height: 48px;"> <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" class="input-wrap__image">
</div> <input placeholder="default" type="password" class="headerless-input password" style="width: 100%; height: 48px;">
<!---->
<!----> <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" class="input-wrap__image">
<path d="M10 4C4.70642 4 1 10 1 10C1 10 3.6999 16 10 16C16.3527 16 19 10 19 10C19 10 15.3472 4 10 4ZM10 13.8176C7.93537 13.8176 6.2946 12.1271 6.2946 10C6.2946 7.87285 7.93537 6.18239 10 6.18239C12.0646 6.18239 13.7054 7.87285 13.7054 10C13.7054 12.1271 12.0646 13.8176 10 13.8176Z" fill="#AFB7C1" class="input-wrap__image__path"></path>
<path d="M11.6116 9.96328C11.6116 10.8473 10.8956 11.5633 10.0116 11.5633C9.12763 11.5633 8.41162 10.8473 8.41162 9.96328C8.41162 9.07929 9.12763 8.36328 10.0116 8.36328C10.8956 8.36328 11.6116 9.07929 11.6116 9.96328Z" fill="#AFB7C1"></path>
</svg>
@ -35,5 +39,7 @@ exports[`HeaderlessInput.vue renders correctly with size props 1`] = `
</div> <input placeholder="test" type="text" class="headerless-input" style="width: 30px; height: 20px;">
<!---->
<!---->
<!---->
<!---->
</div>
`;