Changes to smoothen page transitions

This commit is contained in:
Nithin David Thomas
2024-02-12 18:09:43 +05:30
parent 2d69cd0fdb
commit 922db887e8
13 changed files with 638 additions and 274 deletions
@@ -75,6 +75,7 @@
}
},
"COMPANY": {
"PAGE_TITLE": "Hello %{userName},\nwe are are almost done.",
"TITLE": "Tell us about your company",
"BODY": "In this section, we'll set up the language, timezone, and company size. This information allows us to provide contextually accurate information and suggest relevant features for you.",
"SUBMIT": {
@@ -8,9 +8,10 @@ import helpcenterRoutes from './helpcenter/helpcenter.routes';
const AppContainer = () => import('./Dashboard.vue');
const Suspended = () => import('./suspended/Index.vue');
// const SetupProfile = () => import('v3/views/onboarding/SetupProfile.vue');
// const SetupCompany = () => import('v3/views/onboarding/SetupCompany.vue');
// const InviteTeam = () => import('v3/views/onboarding/InviteTeam.vue');
const RouteLayout = () => import('v3/views/onboarding/RouteLayout.vue');
const SetupProfile = () => import('v3/views/onboarding/SetupProfile.vue');
const SetupCompany = () => import('v3/views/onboarding/SetupCompany.vue');
const InviteTeam = () => import('v3/views/onboarding/InviteTeam.vue');
// const FoundersNote = () => import('v3/views/onboarding/FoundersNote.vue');
export default {
routes: [
@@ -32,23 +33,30 @@ export default {
roles: ['administrator', 'agent'],
component: Suspended,
},
{
path: frontendURL('accounts/:accountId/start'),
component: RouteLayout,
children: [
{
path: frontendURL('accounts/:accountId/start/setup-profile'),
name: 'onboarding_setup_profile',
component: SetupProfile,
},
{
path: frontendURL('accounts/:accountId/start/setup-company'),
name: 'onboarding_setup_company',
component: SetupCompany,
},
{
path: frontendURL('accounts/:accountId/start/invite-team'),
name: 'onboarding_invite_team',
component: InviteTeam,
},
],
},
// {
// path: frontendURL('accounts/:account_id/start/setup-profile'),
// name: 'onboarding_setup_profile',
// component: SetupProfile,
// },
// {
// path: frontendURL('accounts/:account_id/start/setup-company'),
// name: 'onboarding_setup_company',
// component: SetupCompany,
// },
// {
// path: frontendURL('accounts/:account_id/start/invite-team'),
// name: 'onboarding_invite_team',
// component: InviteTeam,
// },
// {
// path: frontendURL('accounts/:account_id/start/founders-note'),
// path: frontendURL('accounts/:accountId/start/founders-note'),
// name: 'onboarding_founders_note',
// component: FoundersNote,
// },
+3 -3
View File
@@ -27,7 +27,7 @@ export const login = async ({
export const register = async creds => {
try {
const response = await wootAPI.post('api/v1/accounts.json', {
const response = await wootAPI.post('api/v2/accounts.json', {
email: creds.email,
password: creds.password,
h_captcha_client_response: creds.hCaptchaClientResponse,
@@ -42,11 +42,11 @@ export const register = async creds => {
export const accountSetup = async ({ id, name, locale }) => {
try {
const response = await wootAPI.put(`api/v1/accounts/${id}.json`, {
const response = await wootAPI.put(`api/v2/accounts/${id}.json`, {
name,
locale,
});
setAuthCredentials(response);
// setAuthCredentials(response);
return response.data;
} catch (error) {
throwErrorMessage(error);
+1 -1
View File
@@ -23,7 +23,7 @@
'px-3 py-2 mb-0': spacing === 'compact',
'pl-9': icon,
}"
class="block w-full border-none rounded-xl shadow-sm appearance-none outline outline-1 outline-slate-200 dark:outline-slate-800 focus:outline-none focus:outline-0 text-slate-900 dark:text-slate-100 placeholder:text-slate-400 focus:ring-2 focus:ring-woot-500 sm:text-sm sm:leading-6 dark:bg-slate-800"
class="block w-full border-none rounded-xl shadow-sm appearance-none outline outline-1 outline-slate-200 dark:outline-slate-700 focus:outline-none focus:outline-0 text-slate-900 dark:text-slate-100 placeholder:text-slate-400 focus:ring-2 focus:ring-woot-500 sm:text-sm sm:leading-6 dark:bg-slate-800"
@input="onInput"
@blur="$emit('blur')"
/>
@@ -12,6 +12,7 @@
:variant="value === option.value ? '' : 'hollow'"
:color-scheme="value === option.value ? 'primary' : 'secondary'"
size="small"
type=""
@click="$emit('input', option.value)"
>
{{ option.label }}
+32 -2
View File
@@ -6,14 +6,32 @@
<div
class="absolute h-full w-full overlay-gradient top-0 left-0 blur-[3px]"
/>
<svg
width="528"
height="96"
viewBox="0 0 528 96"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<mask id="cutoutMask">
<path
d="M263.999 65.0769C293.048 65.0769 317.641 45.9499 325.929 19.5735C329.219 9.10348 337.794 0 348.744 0H508.172C519.122 0 527.999 8.89875 527.999 19.8759V95.5C527.999 95.5 519.122 95.5 508.172 95.5H263.999H-0.000976562V19.8759C-0.000976562 8.89877 8.87604 0 19.8264 0H179.254C190.204 0 198.779 9.10349 202.069 19.5735C210.357 45.9499 234.95 65.0769 263.999 65.0769Z"
fill="#FCFCFD"
/>
</mask>
</defs>
</svg>
<div v-show="!isLoading" class="flex h-full relative z-50">
<div
class="flex-1 min-h-[640px] inline-flex items-center h-full justify-center overflow-auto py-6"
>
<div
class="relative px-16 py-[104px] max-w-[528px] bg-[url('/assets/images/dashboard/onboarding/signup-card-box.svg')] dark:bg-[url('/assets/images/dashboard/onboarding/signup-card-box-dark.svg')] bg-contain bg-top bg-no-repeat w-full overflow-aut rounded-3xl"
class="relative bg-white dark:bg-slate-800 before:bg-white dark:before:bg-slate-800 px-16 pt-8 pb-16 max-w-[528px] w-full rounded-3xl signup-box"
>
<div class="absolute -top-12 right-0 left-0 w-24 h-24 mx-auto">
<div class="absolute -top-[116px] right-0 left-0 w-24 h-24 mx-auto">
<img
:src="globalConfig.logoThumbnail"
:alt="globalConfig.installationName"
@@ -109,4 +127,16 @@ export default {
linear-gradient(0deg, rgba(24, 24, 26, 0) 68.63%, #151718 100%),
linear-gradient(180deg, rgba(24, 24, 26, 0) 73.2%, #151718 100%);
}
.signup-box::before {
width: 100%;
height: 200px;
content: '';
display: block;
left: 0;
top: -66px;
position: absolute;
-webkit-mask-image: url('#cutoutMask');
mask-image: url('#cutoutMask');
}
</style>
@@ -61,7 +61,6 @@ import { required, minLength, email } from 'vuelidate/lib/validators';
import { mapGetters } from 'vuex';
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
import alertMixin from 'shared/mixins/alertMixin';
import { ONBOARDING_START_URL } from 'dashboard/constants/globals';
import VueHcaptcha from '@hcaptcha/vue-hcaptcha';
import FormInput from 'v3/components/Form/Input.vue';
import SubmitButton from '../../../../../components/Button/SubmitButton.vue';
@@ -148,8 +147,14 @@ export default {
}
this.isSignupInProgress = true;
try {
await register(this.credentials);
window.location = ONBOARDING_START_URL;
const {
data: { account_id: accountId },
} = await register(this.credentials);
this.$router.push({
name: 'onboarding_setup_profile',
params: { accountId },
});
} catch (error) {
let errorMessage =
error?.message || this.$t('REGISTER.API.ERROR_MESSAGE');
+7 -5
View File
@@ -69,11 +69,13 @@
</router-link>
</p>
</form-input>
<submit-button
:disabled="loginApi.showLoading"
:button-text="$t('LOGIN.SUBMIT')"
:loading="loginApi.showLoading"
/>
<div class="pt-2">
<submit-button
:disabled="loginApi.showLoading"
:button-text="$t('LOGIN.SUBMIT')"
:loading="loginApi.showLoading"
/>
</div>
</form>
</div>
<div v-else class="flex items-center justify-center">
@@ -1,91 +1,100 @@
<template>
<onboarding-layout
:title="$t('ONBOARDING.INVITE_TEAM.TITLE')"
:body="$t('ONBOARDING.INVITE_TEAM.BODY')"
:intro="$t('ONBOARDING.PROFILE.INTRO')"
current-step="invite"
<div
id="modal-body"
class="dark:shadow-[#000] rounded-3xl p-10 pt-14 border shadow border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 z-10 relative w-full max-w-[560px]"
>
<section class="space-y-5">
<div class="flex gap-2">
<form-input
v-model="emailToAdd"
name="email"
spacing="compact"
class="flex-grow"
:placeholder="$t('ONBOARDING.INVITE_TEAM.PLACEHOLDER')"
@keyup.enter="pushEmail"
/>
<woot-button
variant="smooth"
color-scheme="secondary"
@click="pushEmail"
>
{{ $t('ONBOARDING.INVITE_TEAM.ADD') }}
</woot-button>
</div>
<div
class="rounded-md divide-y divide-slate-100 dark:divide-slate-700 border border-slate-200 dark:border-slate-700 max-h-[40vh] overflow-auto"
:class="{
' border-dashed grid place-content-center':
emailsToInvite.length === 0,
}"
>
<div v-if="emailsToInvite.length === 0" class="px-5 py-5">
<p class="text-sm text-slate-400">No members to invite</p>
<h2
class="font-bold text-[28px] leading-8 tracking-[2%] text-slate-900 dark:text-white"
>
{{ $t('ONBOARDING.INVITE_TEAM.TITLE') }}
</h2>
<p
class="text-base text-slate-600 leading-6 tracking-[-1.1%] mt-2 dark:text-slate-200"
>
{{ $t('ONBOARDING.INVITE_TEAM.BODY') }}
</p>
<div class="mt-10">
<section class="space-y-5">
<div class="flex gap-2">
<form-input
v-model="emailToAdd"
name="email"
spacing="compact"
class="flex-grow"
:placeholder="$t('ONBOARDING.INVITE_TEAM.PLACEHOLDER')"
@keyup.enter="pushEmail"
/>
<woot-button
variant="smooth"
color-scheme="secondary"
@click="pushEmail"
>
{{ $t('ONBOARDING.INVITE_TEAM.ADD') }}
</woot-button>
</div>
<div
v-for="email in emailsToInvite"
:key="email"
class="flex items-center justify-between p-4"
class="rounded-md divide-y divide-slate-100 dark:divide-slate-700 border border-slate-200 dark:border-slate-700 max-h-[40vh] overflow-auto"
:class="{
' border-dashed grid place-content-center':
emailsToInvite.length === 0,
}"
>
<div class="flex items-center gap-2">
<woot-thumbnail :username="email" size="24px" />
<span
class="text-slate-700 dark:text-slate-200 text-sm font-medium"
>
{{ email }}
</span>
<div v-if="emailsToInvite.length === 0" class="px-5 py-16">
<p class="text-sm text-slate-400">No members to invite</p>
</div>
<div
v-for="email in emailsToInvite"
:key="email"
class="flex items-center justify-between p-4"
>
<div class="flex items-center gap-2">
<woot-thumbnail :username="email" size="24px" />
<span
class="text-slate-700 dark:text-slate-200 text-sm font-medium"
>
{{ email }}
</span>
</div>
<woot-button
variant="clear"
size="small"
color-scheme="secondary"
icon="dismiss"
@click="emailsToInvite.splice(emailsToInvite.indexOf(email), 1)"
/>
</div>
</div>
<div class="space-y-2">
<submit-button
button-class="flex justify-center w-full text-sm text-center"
:button-text="$t('ONBOARDING.INVITE_TEAM.SUBMIT.BUTTON')"
@click="onSubmit"
/>
<woot-button
variant="clear"
size="small"
color-scheme="alert"
icon="delete"
@click="emailsToInvite.splice(emailsToInvite.indexOf(email), 1)"
/>
color-scheme="secondary"
size="expanded"
is-expanded
@click="skipToFoundersNote"
>
{{ $t('ONBOARDING.INVITE_TEAM.SKIP') }}
</woot-button>
</div>
</div>
<div class="space-y-2">
<submit-button
button-class="flex justify-center w-full text-sm text-center"
:button-text="$t('ONBOARDING.INVITE_TEAM.SUBMIT.BUTTON')"
@click="onSubmit"
/>
<woot-button
variant="clear"
color-scheme="secondary"
size="expanded"
is-expanded
>
{{ $t('ONBOARDING.INVITE_TEAM.SKIP') }}
</woot-button>
</div>
</section>
</onboarding-layout>
</section>
</div>
</div>
</template>
<script>
import FormInput from 'v3/components/Form/Input.vue';
import SubmitButton from 'dashboard/components/buttons/FormSubmitButton.vue';
import OnboardingLayout from './Layout.vue';
import AgentAPI from 'dashboard/api/agents.js';
import alertMixin from 'shared/mixins/alertMixin';
export default {
components: {
OnboardingLayout,
SubmitButton,
FormInput,
},
@@ -103,6 +112,9 @@ export default {
this.emailsToInvite = [...new Set(this.emailsToInvite)];
this.emailToAdd = '';
},
skipToFoundersNote() {
this.$router.push({ name: 'onboarding_founders_note' });
},
async onSubmit() {
if (this.emailsToInvite.length === 0) {
this.showAlert(
@@ -112,11 +124,11 @@ export default {
}
try {
await AgentAPI.bulkInvite(this.emailsToInvite);
this.showAlert(this.$t('ONBOARDING.PROFILE.SUBMIT.SUCCESS'));
this.$router.push({ name: 'onboarding_setup_company' });
await AgentAPI.bulkInvite({ emails: this.emailsToInvite });
this.showAlert(this.$t('ONBOARDING.INVITE_TEAM.SUBMIT.SUCCESS'));
this.$router.push({ name: 'onboarding_founders_note' });
} catch (error) {
this.showAlert(this.$t('ONBOARDING.PROFILE.SUBMIT.SUCCESS'));
this.showAlert(this.$t('ONBOARDING.INVITE_TEAM.SUBMIT.ERROR'));
}
},
},
@@ -1,6 +1,6 @@
<template>
<div
class="flex items-center gap-2 p-2 text-lg font-bold mb-6 relative before:absolute before:h-10 before:w-[1px] before:bg-slate-200 before:-bottom-8 before:left-[24px] hide-before-of-last"
class="flex items-center gap-2 p-2 text-lg font-medium mb-6 relative before:absolute before:h-10 before:w-[1px] before:bg-slate-200 before:-bottom-8 before:left-[24px] hide-before-of-last"
:class="{
'text-woot-500 ': isActive,
'text-slate-400': !isActive || isComplete,
@@ -10,7 +10,7 @@
<div
class="grid w-8 h-8 border border-solid rounded-full place-content-center"
:class="{
'border-woot-500': !isActive || isComplete,
'border-woot-500': (!isActive && isComplete) || isComplete,
'border-slate-200 dark:border-slate-600': !isActive && !isComplete,
'text-woot-500': isComplete,
}"
@@ -0,0 +1,196 @@
<template>
<section
class="relative min-h-screen px-8 dark:text-white bg-[#FCFCFD] dark:bg-slate-900 flex items-center justify-center bg-[url('/assets/images/dashboard/onboarding/intro.svg')] dark:bg-[url('/assets/images/dashboard/onboarding/intro-dark.svg')] bg-[length:715px_555px] bg-no-repeat bg-[left_calc(-56px)_bottom_calc(-133px)]"
>
<div
class="relative max-w-[1440px] w-full mx-auto flex gap-16 min-h-[80vh] justify-center"
>
<div class="relative w-5/12 px-16 py-[88px] mt-10">
<div class="mb-10 z-0">
<img
:src="globalConfig.logo"
:alt="globalConfig.installationName"
class="h-8 w-auto block dark:hidden"
/>
<img
v-if="globalConfig.logoDark"
:src="globalConfig.logoDark"
:alt="globalConfig.installationName"
class="h-8 w-auto hidden dark:block"
/>
<transition name="fade">
<h1
v-show="showIntroHeader"
class="font-bold dark:text-white text-slate-900 text-[40px] leading-[56px] whitespace-pre-line"
>
{{ intro }}
</h1>
</transition>
</div>
<transition name="slide-fade">
<div v-if="showIntroHeader" id="steps" class="z-0">
<onboarding-step
v-for="(step, index) in steps"
:key="step.name"
v-bind="step"
:step-number="index + 1"
:icon="step.icon"
/>
</div>
</transition>
</div>
<div
class="relative w-7/12 py-[88px] flex justify-center overflow-hidden h-fit"
>
<div
class="absolute inset-0 h-full w-full bg-white dark:bg-slate-900 bg-[radial-gradient(var(--w-200)_1px,transparent_1px)] [background-size:16px_16px]"
/>
<div
class="absolute h-full w-full overlay-gradient top-0 left-0 scale-y-110 blur-[3px]"
/>
<transition name="slide-fade">
<router-view />
</transition>
</div>
</div>
</section>
</template>
<script>
import { mapGetters } from 'vuex';
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
import OnboardingStep from './OnboardingStep.vue';
const UISteps = [
{
name: 'onboarding_setup_profile',
title: 'Setup Profile',
icon: 'person',
isActive: false,
isComplete: false,
},
{
name: 'onboarding_setup_company',
title: 'Setup Company',
icon: 'toolbox',
isActive: false,
isComplete: false,
},
{
name: 'onboarding_invite_team',
title: 'Invite your team',
icon: 'people-team',
isActive: false,
isComplete: false,
},
// {
// name: 'onboarding_founders_note',
// title: "Founder's Note",
// icon: '',
// hidden: true,
// isActive: false,
// isComplete: false,
// },
];
export default {
components: {
OnboardingStep,
},
mixins: [globalConfigMixin],
data() {
return {
showIntroHeader: false,
};
},
computed: {
currentStep() {
return this.$route.name;
},
intro() {
if (
this.currentStep === 'onboarding_setup_company' ||
this.currentStep === 'onboarding_invite_team'
) {
return this.$t('ONBOARDING.COMPANY.PAGE_TITLE', {
userName: this.userName,
});
}
return this.$t('ONBOARDING.PROFILE.INTRO');
},
steps() {
let foundCurrentStep = false;
return UISteps.map(step => {
if (step.name === this.currentStep) {
foundCurrentStep = true;
}
return {
...step,
isActive: step.name === this.currentStep,
isComplete: !foundCurrentStep,
};
});
},
userName() {
return this.currentUser.name;
},
...mapGetters({
globalConfig: 'globalConfig/get',
currentUser: 'getCurrentUser',
}),
},
watch: {
watch: {
$route() {
this.showIntroHeader = false;
setTimeout(() => {
this.showIntroHeader = true;
}, 10);
},
},
},
mounted() {
this.showIntroHeader = true;
},
beforeDestroy() {
this.showIntroHeader = false;
},
};
</script>
<style>
.overlay-gradient {
background: linear-gradient(90deg, rgba(252, 252, 253, 0) 81.8%, #fcfcfd 100%),
linear-gradient(270deg, rgba(252, 252, 253, 0) 76.93%, #fcfcfd 100%),
linear-gradient(0deg, rgba(252, 252, 253, 0) 68.63%, #fcfcfd 100%),
linear-gradient(180deg, rgba(252, 252, 253, 0) 73.2%, #fcfcfd 100%);
}
.dark .overlay-gradient {
background: linear-gradient(270deg, rgba(24, 24, 26, 0) 76.93%, #151718 100%),
linear-gradient(90deg, rgba(24, 24, 26, 0) 81.8%, #151718 100%),
linear-gradient(0deg, rgba(24, 24, 26, 0) 68.63%, #151718 100%),
linear-gradient(180deg, rgba(24, 24, 26, 0) 73.2%, #151718 100%);
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
opacity: 0;
}
/* Enter and leave animations can use different */
/* durations and timing functions. */
.slide-fade-enter-active {
transition: all 0.5s ease;
}
.slide-fade-leave-active {
position: absolute;
transition: all 0.4s cubic-bezier(1, 0.5, 0.8, 1);
}
.slide-fade-enter, .slide-fade-leave-to
/* .slide-fade-leave-active below version 2.1.8 */ {
transform: translateY(10px);
opacity: 0;
}
</style>
@@ -1,11 +1,20 @@
<template>
<onboarding-layout
:intro="$t('ONBOARDING.PROFILE.INTRO')"
:title="$t('ONBOARDING.COMPANY.TITLE')"
:body="$t('ONBOARDING.COMPANY.BODY')"
current-step="company"
<div
id="modal-body"
class="dark:shadow-[#000] rounded-3xl p-10 pt-14 border shadow border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 z-10 relative w-full max-w-[560px]"
>
<form class="space-y-8" @submit.prevent="onSubmit">
<h2
class="font-bold text-[28px] leading-8 tracking-[2%] text-slate-900 dark:text-white"
>
{{ $t('ONBOARDING.COMPANY.TITLE') }}
</h2>
<p
class="text-base text-slate-600 leading-6 tracking-[-1.1%] mt-2 dark:text-slate-200"
>
{{ $t('ONBOARDING.COMPANY.BODY') }}
</p>
<div class="space-y-8 mt-10">
<div class="space-y-3">
<form-input
v-model="companyName"
@@ -68,9 +77,10 @@
<submit-button
button-class="flex justify-center w-full text-sm text-center"
:button-text="$t('ONBOARDING.COMPANY.SUBMIT.BUTTON')"
@click="onSubmit"
/>
</form>
</onboarding-layout>
</div>
</div>
</template>
<script>
@@ -78,9 +88,9 @@ import FormInput from 'v3/components/Form/Input.vue';
import FormSelect from 'v3/components/Form/Select.vue';
import FormRadioTags from 'v3/components/Form/RadioTags.vue';
import { required, minLength } from 'vuelidate/lib/validators';
import { mapGetters } from 'vuex';
import SubmitButton from 'dashboard/components/buttons/FormSubmitButton.vue';
import OnboardingLayout from './Layout.vue';
import { timeZoneOptions } from 'dashboard/routes/dashboard/settings/inbox/helpers/businessHour.js';
import alertMixin from 'shared/mixins/alertMixin';
import configMixin from 'shared/mixins/configMixin';
@@ -88,7 +98,6 @@ export default {
components: {
FormInput,
SubmitButton,
OnboardingLayout,
FormSelect,
FormRadioTags,
},
@@ -123,6 +132,13 @@ export default {
},
},
computed: {
...mapGetters({
getAccount: 'accounts/getAccount',
currentAccountId: 'getCurrentAccountId',
}),
accountDetails() {
return this.getAccount(this.currentAccountId);
},
languagesSortedByCode() {
const enabledLanguages = [...this.enabledLanguages];
return enabledLanguages.sort((l1, l2) =>
@@ -132,45 +148,41 @@ export default {
timeZones() {
return [...timeZoneOptions()];
},
intelligentData() {
const { clearbit_data: data } = this.getAccount;
return data;
},
},
watch: {
locale(val) {
this.setLocale(val);
},
accountDetails() {
this.initFormData();
},
},
mounted() {
const localeWithVariant = window.navigator.language.replace('-', '_');
const localeWithoutVariant = localeWithVariant.split('_')[0];
const { iso_639_1_code: locale } =
this.enabledLanguages.find(
lang =>
lang.iso_639_1_code === localeWithVariant ||
lang.iso_639_1_code === localeWithoutVariant
) || {};
if (locale) {
this.locale = locale;
this.setLocale(locale);
}
this.setLocaleFromBrowser();
this.setTimezone();
this.initFormData();
},
methods: {
async onSubmit(event) {
event.stopPropagation();
event.preventDefault();
async onSubmit() {
this.$v.$touch();
if (this.$v.$invalid) {
return;
}
try {
await this.$store.dispatch('updateProfile', {
companyName: this.companyName,
// phone
// avatar
await this.$store.dispatch('accounts/update', {
name: this.companyName,
timezone: this.timezone,
locale: this.locale,
company_size: this.companySize,
industry: this.industry,
});
this.showAlert(this.$t('ONBOARDING.COMPANY.SUBMIT.SUCCESS'));
this.$router.push({ name: 'onboarding_invite_team' });
@@ -181,8 +193,62 @@ export default {
setLocale(locale) {
this.$root.$i18n.locale = locale;
},
setLocaleFromBrowser() {
const localeWithVariant = window.navigator.language.replace('-', '_');
const localeWithoutVariant = localeWithVariant.split('_')[0];
const { iso_639_1_code: locale } =
this.enabledLanguages.find(
lang =>
lang.iso_639_1_code === localeWithVariant ||
lang.iso_639_1_code === localeWithoutVariant
) || {};
if (locale) {
this.locale = locale;
this.setLocale(locale);
}
},
setTimezone() {
this.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const browserTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
if (!this.intelligentData) {
this.timezone = browserTimezone;
return;
}
const { timezone } = this.intelligentData;
const allZones = this.timeZones.map(zone => zone.value);
const matchForIntelligentZone = allZones.find(zone => zone === timezone);
if (matchForIntelligentZone) {
this.timezone = timezone;
}
},
setFromIntelligentData() {
const { name: companyName, industry, size } = this.intelligentData;
this.companyName = companyName;
this.industry = industry;
this.companySize = size;
},
setSavedData() {
const {
name: companyName,
industry,
size,
locale,
timezone,
} = this.accountDetails;
this.companyName = companyName;
this.industry = industry;
this.companySize = size;
this.locale = locale;
this.timezone = timezone;
},
initFormData() {
const { name } = this.accountDetails;
if (!name) {
this.setFromIntelligentData();
} else {
this.setSavedData();
}
},
},
};
@@ -1,130 +1,142 @@
<template>
<onboarding-layout
:intro="$t('ONBOARDING.PROFILE.INTRO')"
:title="$t('ONBOARDING.PROFILE.TITLE')"
:body="$t('ONBOARDING.PROFILE.BODY')"
current-step="profile"
<div
id="modal-body"
class="dark:shadow-[#000] rounded-3xl p-10 pt-14 border shadow border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 z-10 relative max-w-[560px]"
>
<form class="space-y-6" @submit="onSubmit">
<div class="space-y-6">
<div>
<with-label name="user_avatar">
<template #label>
{{ $t('ONBOARDING.PROFILE.AVATAR.LABEL') }}
</template>
<div class="flex gap-2">
<woot-thumbnail
v-if="avatarUrl"
size="40px"
:src="avatarUrl"
username=""
variant="square"
/>
<div
v-else
class="rounded-xl h-10 w-10 bg-slate-200 flex items-center justify-center"
>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
<h2
class="font-bold text-[28px] leading-8 tracking-[2%] text-slate-900 dark:text-white"
>
{{ $t('ONBOARDING.PROFILE.TITLE') }}
</h2>
<p
class="text-base text-slate-600 leading-6 tracking-[-1.1%] mt-2 dark:text-slate-200"
>
{{ $t('ONBOARDING.PROFILE.BODY') }}
</p>
<div class="mt-10">
<form class="space-y-6" @submit="onSubmit">
<div class="space-y-6">
<div>
<with-label name="user_avatar">
<template #label>
{{ $t('ONBOARDING.PROFILE.AVATAR.LABEL') }}
</template>
<div class="flex gap-2">
<woot-thumbnail
v-if="avatarUrl"
size="40px"
:src="avatarUrl"
:username="fullName"
variant="square"
:has-border="true"
/>
<div
v-else
class="rounded-xl h-10 w-10 bg-slate-200 flex items-center justify-center"
>
<g id="user-3-fill">
<path
id="Vector"
d="M18.8574 20.5714H5.14307V18.8571C5.14307 17.7205 5.5946 16.6304 6.39832 15.8267C7.20205 15.0229 8.29214 14.5714 9.42878 14.5714H14.5716C15.7083 14.5714 16.7984 15.0229 17.6021 15.8267C18.4058 16.6304 18.8574 17.7205 18.8574 18.8571V20.5714ZM12.0002 12.8571C11.3248 12.8571 10.6561 12.7241 10.0321 12.4656C9.40816 12.2072 8.84122 11.8284 8.36366 11.3508C7.8861 10.8733 7.50728 10.3063 7.24883 9.68235C6.99038 9.05839 6.85735 8.38964 6.85735 7.71427C6.85735 7.0389 6.99038 6.37014 7.24883 5.74618C7.50728 5.12222 7.8861 4.55528 8.36366 4.07772C8.84122 3.60016 9.40816 3.22134 10.0321 2.96289C10.6561 2.70444 11.3248 2.57141 12.0002 2.57141C13.3642 2.57141 14.6723 3.11325 15.6368 4.07772C16.6012 5.04219 17.1431 6.3503 17.1431 7.71427C17.1431 9.07824 16.6012 10.3863 15.6368 11.3508C14.6723 12.3153 13.3642 12.8571 12.0002 12.8571Z"
fill="#B9BBC6"
/>
</g>
</svg>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g id="user-3-fill">
<path
id="Vector"
d="M18.8574 20.5714H5.14307V18.8571C5.14307 17.7205 5.5946 16.6304 6.39832 15.8267C7.20205 15.0229 8.29214 14.5714 9.42878 14.5714H14.5716C15.7083 14.5714 16.7984 15.0229 17.6021 15.8267C18.4058 16.6304 18.8574 17.7205 18.8574 18.8571V20.5714ZM12.0002 12.8571C11.3248 12.8571 10.6561 12.7241 10.0321 12.4656C9.40816 12.2072 8.84122 11.8284 8.36366 11.3508C7.8861 10.8733 7.50728 10.3063 7.24883 9.68235C6.99038 9.05839 6.85735 8.38964 6.85735 7.71427C6.85735 7.0389 6.99038 6.37014 7.24883 5.74618C7.50728 5.12222 7.8861 4.55528 8.36366 4.07772C8.84122 3.60016 9.40816 3.22134 10.0321 2.96289C10.6561 2.70444 11.3248 2.57141 12.0002 2.57141C13.3642 2.57141 14.6723 3.11325 15.6368 4.07772C16.6012 5.04219 17.1431 6.3503 17.1431 7.71427C17.1431 9.07824 16.6012 10.3863 15.6368 11.3508C14.6723 12.3153 13.3642 12.8571 12.0002 12.8571Z"
fill="#B9BBC6"
/>
</g>
</svg>
</div>
<input
id="user_avatar"
ref="imageUpload"
name="user_avatar"
class="invisible h-0 w-0"
type="file"
accept="image/png, image/jpeg, image/jpg, image/gif, image/webp"
@change="handleImageUpload"
/>
<woot-button
v-if="!avatarFile"
color-scheme="secondary"
variant="hollow"
@click="onFileBrowse"
>
{{ $t('ONBOARDING.PROFILE.AVATAR.PLACEHOLDER') }}
</woot-button>
<woot-button
v-else
type="button"
color-scheme="alert"
variant="hollow"
@click="deleteAvatar"
>
{{ $t('PROFILE_SETTINGS.DELETE_AVATAR') }}
</woot-button>
</div>
<input
id="user_avatar"
ref="imageUpload"
name="user_avatar"
class="invisible h-0 w-0"
type="file"
accept="image/png, image/jpeg, image/jpg, image/gif, image/webp"
@change="handleImageUpload"
/>
<woot-button
v-if="!avatarBlobId"
color-scheme="secondary"
variant="hollow"
@click="onFileBrowse"
>
{{ $t('ONBOARDING.PROFILE.AVATAR.PLACEHOLDER') }}
</woot-button>
<woot-button
v-else
type="button"
color-scheme="alert"
variant="hollow"
>
{{ $t('PROFILE_SETTINGS.DELETE_AVATAR') }}
</woot-button>
</div>
</with-label>
</with-label>
</div>
<form-input
v-model="fullName"
name="full_name"
spacing="compact"
:has-error="$v.fullName.$error"
:label="$t('ONBOARDING.PROFILE.FULL_NAME.LABEL')"
:placeholder="$t('ONBOARDING.PROFILE.FULL_NAME.PLACEHOLDER')"
:error-message="$t('ONBOARDING.PROFILE.FULL_NAME.ERROR')"
/>
<form-input
v-model="displayName"
name="display_name"
spacing="compact"
:has-error="$v.displayName.$error"
:label="$t('ONBOARDING.PROFILE.DISPLAY_NAME.LABEL')"
:placeholder="$t('ONBOARDING.PROFILE.DISPLAY_NAME.PLACEHOLDER')"
:error-message="$t('ONBOARDING.PROFILE.DISPLAY_NAME.ERROR')"
/>
<form-input
v-model="phoneNumber"
name="phone_number"
spacing="compact"
:has-error="$v.phoneNumber.$error"
:label="$t('ONBOARDING.PROFILE.PHONE_NUMBER.LABEL')"
:placeholder="$t('ONBOARDING.PROFILE.PHONE_NUMBER.PLACEHOLDER')"
:error-message="$t('CONTACT_FORM.FORM.PHONE_NUMBER.ERROR')"
/>
<form-text-area
v-model="signature"
name="signature"
spacing="compact"
:allow-resize="false"
:label="$t('ONBOARDING.PROFILE.SIGNATURE.LABEL')"
:placeholder="$t('ONBOARDING.PROFILE.SIGNATURE.PLACEHOLDER')"
/>
</div>
<form-input
v-model="fullName"
name="full_name"
spacing="compact"
:has-error="$v.fullName.$error"
:label="$t('ONBOARDING.PROFILE.FULL_NAME.LABEL')"
:placeholder="$t('ONBOARDING.PROFILE.FULL_NAME.PLACEHOLDER')"
:error-message="$t('ONBOARDING.PROFILE.FULL_NAME.ERROR')"
<submit-button
button-class="flex justify-center w-full text-sm text-center"
:button-text="$t('ONBOARDING.PROFILE.SUBMIT.BUTTON')"
/>
<form-input
v-model="displayName"
name="display_name"
spacing="compact"
:has-error="$v.displayName.$error"
:label="$t('ONBOARDING.PROFILE.DISPLAY_NAME.LABEL')"
:placeholder="$t('ONBOARDING.PROFILE.DISPLAY_NAME.PLACEHOLDER')"
:error-message="$t('ONBOARDING.PROFILE.DISPLAY_NAME.ERROR')"
/>
<form-input
v-model="phoneNumber"
name="phone_number"
spacing="compact"
:has-error="$v.phoneNumber.$error"
:label="$t('ONBOARDING.PROFILE.PHONE_NUMBER.LABEL')"
:placeholder="$t('ONBOARDING.PROFILE.PHONE_NUMBER.PLACEHOLDER')"
:error-message="$t('CONTACT_FORM.FORM.PHONE_NUMBER.ERROR')"
/>
<form-text-area
v-model="signature"
name="signature"
spacing="compact"
:allow-resize="false"
:label="$t('ONBOARDING.PROFILE.SIGNATURE.LABEL')"
:placeholder="$t('ONBOARDING.PROFILE.SIGNATURE.PLACEHOLDER')"
/>
</div>
<submit-button
button-class="flex justify-center w-full text-sm text-center"
:button-text="$t('ONBOARDING.PROFILE.SUBMIT.BUTTON')"
/>
</form>
</onboarding-layout>
</form>
</div>
</div>
</template>
<script>
import md5 from 'md5';
import { required, minLength, helpers } from 'vuelidate/lib/validators';
import parsePhoneNumber from 'libphonenumber-js';
import { uploadFile } from 'dashboard/helper/uploadHelper';
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
import { validationMixin } from 'vuelidate';
import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin';
import WithLabel from 'v3/components/Form/WithLabel.vue';
import FormInput from 'v3/components/Form/Input.vue';
import FormTextArea from 'v3/components/Form/Textarea.vue';
import SubmitButton from 'dashboard/components/buttons/FormSubmitButton.vue';
import OnboardingLayout from './Layout.vue';
const MAXIMUM_FILE_UPLOAD_SIZE = 4; // in MB
@@ -133,17 +145,15 @@ export default {
WithLabel,
FormInput,
SubmitButton,
OnboardingLayout,
FormTextArea,
},
mixins: [validationMixin, alertMixin],
data() {
return {
avatarUrl: '',
avatarBlobId: '',
avatarFile: '',
fullName: '',
displayName: '',
activeDialCode: '',
phoneNumber: '',
signature: '',
};
@@ -161,6 +171,44 @@ export default {
!helpers.req(value) || parsePhoneNumber(value).isValid(),
},
},
computed: {
...mapGetters({
getAccount: 'accounts/getAccount',
currentUser: 'getCurrentUser',
}),
intelligentData() {
const { clearbit_data: data } = this.getAccount;
return data;
},
},
watch: {
currentUser(user) {
if (!user.name && this.intelligentData.name) {
this.fullName = this.intelligentData.name;
}
},
},
mounted() {
const {
email,
name: fullName,
avatar_url: avatarUrl,
display_name: displayName,
message_signature: signature,
} = this.currentUser;
if (!fullName && this.intelligentData.name) {
this.fullName = this.intelligentData.name;
} else {
this.fullName = fullName;
}
this.displayName = displayName;
this.signature = signature;
this.avatarUrl =
avatarUrl ||
`https://gravatar.com/avatar/${md5(email)}?s=400&d=robohash&r=x`;
},
methods: {
async onSubmit(event) {
event.stopPropagation();
@@ -173,24 +221,34 @@ export default {
await this.$store.dispatch('updateProfile', {
name: this.name,
displayName: this.displayName,
// phone
// avatar
phone_number: this.phoneNumber,
avatar: this.avatarFile,
message_signature: this.signature || '',
});
this.showAlert(this.$t('ONBOARDING.PROFILE.SUBMIT.SUCCESS'));
this.$router.push({ name: 'onboarding_setup_company' });
document.startViewTransition(() => {
this.$router.push({ name: 'onboarding_setup_company' });
});
} catch (error) {
this.showAlert(this.$t('ONBOARDING.PROFILE.SUBMIT.ERROR'));
}
},
async deleteAvatar() {
this.logoUrl = '';
this.avatarBlobId = '';
this.$emit('delete-logo');
async deleteAvatar(event) {
event.stopPropagation();
event.preventDefault();
const { email } = this.currentUser;
this.avatarUrl = `https://gravatar.com/avatar/${md5(
email
)}?s=400&d=robohash&r=x`;
this.avatarFile = '';
this.$refs.imageUpload.value = '';
},
handleImageUpload() {
const file = this.$refs.imageUpload.files[0];
if (checkFileSizeLimit(file, MAXIMUM_FILE_UPLOAD_SIZE)) {
this.uploadLogoToStorage(file);
this.avatarFile = file;
this.avatarUrl = URL.createObjectURL(file);
} else {
this.showAlert(
this.$t(
@@ -204,21 +262,6 @@ export default {
this.$refs.imageUpload.value = '';
},
async uploadLogoToStorage(file) {
try {
// Get accountId from store??
const accountId = 1;
const { fileUrl, blobId } = await uploadFile(file, accountId);
if (fileUrl) {
this.avatarUrl = fileUrl;
this.avatarBlobId = blobId;
}
} catch (error) {
this.showAlert(
this.$t('HELP_CENTER.PORTAL.ADD.LOGO.IMAGE_DELETE_ERROR')
);
}
},
onFileBrowse(event) {
event.preventDefault();
const fileInputElement = this.$refs.imageUpload;