feat: Support dark mode in login pages (#7420)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
This commit is contained in:
36
app/javascript/v3/views/auth/confirmation/Index.vue
Normal file
36
app/javascript/v3/views/auth/confirmation/Index.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-center h-full w-full">
|
||||
<spinner color-scheme="primary" size="" />
|
||||
<div class="ml-2">{{ $t('CONFIRM_EMAIL') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { DEFAULT_REDIRECT_URL } from 'dashboard/constants/globals';
|
||||
import { verifyPasswordToken } from '../../../api/auth';
|
||||
import Spinner from 'shared/components/Spinner';
|
||||
|
||||
export default {
|
||||
components: { Spinner },
|
||||
props: {
|
||||
confirmationToken: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.confirmToken();
|
||||
},
|
||||
methods: {
|
||||
async confirmToken() {
|
||||
try {
|
||||
await verifyPasswordToken({
|
||||
confirmationToken: this.confirmationToken,
|
||||
});
|
||||
window.location = DEFAULT_REDIRECT_URL;
|
||||
} catch (error) {
|
||||
window.location = DEFAULT_REDIRECT_URL;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
132
app/javascript/v3/views/auth/password/Edit.vue
Normal file
132
app/javascript/v3/views/auth/password/Edit.vue
Normal file
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col bg-woot-25 min-h-full w-full py-12 sm:px-6 lg:px-8 justify-center dark:bg-slate-900"
|
||||
>
|
||||
<form
|
||||
class="sm:mx-auto sm:w-full sm:max-w-lg bg-white dark:bg-slate-800 p-11 shadow sm:shadow-lg sm:rounded-lg"
|
||||
@submit.prevent="submitForm"
|
||||
>
|
||||
<h1
|
||||
class="mb-1 text-left text-2xl font-medium tracking-tight text-slate-900 dark:text-white"
|
||||
>
|
||||
{{ $t('SET_NEW_PASSWORD.TITLE') }}
|
||||
</h1>
|
||||
|
||||
<div class="column log-in-form space-y-5">
|
||||
<form-input
|
||||
v-model.trim="credentials.password"
|
||||
class="mt-3"
|
||||
name="password"
|
||||
type="password"
|
||||
:has-error="$v.credentials.password.$error"
|
||||
:error-message="$t('SET_NEW_PASSWORD.PASSWORD.ERROR')"
|
||||
:placeholder="$t('SET_NEW_PASSWORD.PASSWORD.PLACEHOLDER')"
|
||||
@blur="$v.credentials.password.$touch"
|
||||
/>
|
||||
<form-input
|
||||
v-model.trim="credentials.confirmPassword"
|
||||
class="mt-3"
|
||||
name="confirm_password"
|
||||
type="password"
|
||||
:has-error="$v.credentials.confirmPassword.$error"
|
||||
:error-message="$t('SET_NEW_PASSWORD.CONFIRM_PASSWORD.ERROR')"
|
||||
:placeholder="$t('SET_NEW_PASSWORD.CONFIRM_PASSWORD.PLACEHOLDER')"
|
||||
@blur="$v.credentials.confirmPassword.$touch"
|
||||
/>
|
||||
<submit-button
|
||||
:disabled="
|
||||
$v.credentials.password.$invalid ||
|
||||
$v.credentials.confirmPassword.$invalid ||
|
||||
newPasswordAPI.showLoading
|
||||
"
|
||||
:button-text="$t('SET_NEW_PASSWORD.SUBMIT')"
|
||||
:loading="newPasswordAPI.showLoading"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { required, minLength } from 'vuelidate/lib/validators';
|
||||
import FormInput from '../../../components/Form/Input.vue';
|
||||
import SubmitButton from '../../../components/Button/SubmitButton.vue';
|
||||
import { DEFAULT_REDIRECT_URL } from 'dashboard/constants/globals';
|
||||
import { setNewPassword } from '../../../api/auth';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FormInput,
|
||||
SubmitButton,
|
||||
},
|
||||
props: {
|
||||
resetPasswordToken: { type: String, default: '' },
|
||||
redirectUrl: { type: String, default: '' },
|
||||
config: { type: String, default: '' },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// We need to initialize the component with any
|
||||
// properties that will be used in it
|
||||
credentials: {
|
||||
confirmPassword: '',
|
||||
password: '',
|
||||
},
|
||||
newPasswordAPI: {
|
||||
message: '',
|
||||
showLoading: false,
|
||||
},
|
||||
error: '',
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
// If url opened without token
|
||||
// redirect to login
|
||||
if (!this.resetPasswordToken) {
|
||||
window.location = DEFAULT_REDIRECT_URL;
|
||||
}
|
||||
},
|
||||
validations: {
|
||||
credentials: {
|
||||
password: {
|
||||
required,
|
||||
minLength: minLength(6),
|
||||
},
|
||||
confirmPassword: {
|
||||
required,
|
||||
minLength: minLength(6),
|
||||
isEqPassword(value) {
|
||||
if (value !== this.credentials.password) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
showAlert(message) {
|
||||
// Reset loading, current selected agent
|
||||
this.newPasswordAPI.showLoading = false;
|
||||
bus.$emit('newToastMessage', message);
|
||||
},
|
||||
submitForm() {
|
||||
this.newPasswordAPI.showLoading = true;
|
||||
const credentials = {
|
||||
confirmPassword: this.credentials.confirmPassword,
|
||||
password: this.credentials.password,
|
||||
resetPasswordToken: this.resetPasswordToken,
|
||||
};
|
||||
setNewPassword(credentials)
|
||||
.then(() => {
|
||||
window.location = DEFAULT_REDIRECT_URL;
|
||||
})
|
||||
.catch(error => {
|
||||
this.showAlert(
|
||||
error?.message || this.$t('SET_NEW_PASSWORD.API.ERROR_MESSAGE')
|
||||
);
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
108
app/javascript/v3/views/auth/reset/password/Index.vue
Normal file
108
app/javascript/v3/views/auth/reset/password/Index.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col bg-woot-25 min-h-full w-full py-12 sm:px-6 lg:px-8 justify-center dark:bg-slate-900"
|
||||
>
|
||||
<form
|
||||
class="sm:mx-auto sm:w-full sm:max-w-lg bg-white dark:bg-slate-800 p-11 shadow sm:shadow-lg sm:rounded-lg"
|
||||
@submit.prevent="submit"
|
||||
>
|
||||
<h1
|
||||
class="mb-1 text-left text-2xl font-medium tracking-tight text-slate-900 dark:text-white"
|
||||
>
|
||||
{{ $t('RESET_PASSWORD.TITLE') }}
|
||||
</h1>
|
||||
<p
|
||||
class="text-sm text-slate-600 dark:text-woot-50 tracking-normal font-normal leading-6 mb-4"
|
||||
>
|
||||
{{
|
||||
useInstallationName(
|
||||
$t('RESET_PASSWORD.DESCRIPTION'),
|
||||
globalConfig.installationName
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<div class="column log-in-form space-y-5">
|
||||
<form-input
|
||||
v-model.trim="credentials.email"
|
||||
name="email_address"
|
||||
:has-error="$v.credentials.email.$error"
|
||||
:error-message="$t('RESET_PASSWORD.EMAIL.ERROR')"
|
||||
:placeholder="$t('RESET_PASSWORD.EMAIL.PLACEHOLDER')"
|
||||
@input="$v.credentials.email.$touch"
|
||||
/>
|
||||
<SubmitButton
|
||||
:disabled="$v.credentials.email.$invalid || resetPassword.showLoading"
|
||||
:button-text="$t('RESET_PASSWORD.SUBMIT')"
|
||||
:loading="resetPassword.showLoading"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-sm text-slate-600 dark:text-woot-50 mt-4 -mb-1">
|
||||
{{ $t('RESET_PASSWORD.GO_BACK_TO_LOGIN') }}
|
||||
<router-link to="/auth/login" class="text-link">
|
||||
{{ $t('COMMON.CLICK_HERE') }}.
|
||||
</router-link>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { required, minLength, email } from 'vuelidate/lib/validators';
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
import { mapGetters } from 'vuex';
|
||||
import FormInput from '../../../../components/Form/Input.vue';
|
||||
import { resetPassword } from '../../../../api/auth';
|
||||
import SubmitButton from '../../../../components/Button/SubmitButton.vue';
|
||||
|
||||
export default {
|
||||
components: { FormInput, SubmitButton },
|
||||
mixins: [globalConfigMixin],
|
||||
data() {
|
||||
return {
|
||||
credentials: { email: '' },
|
||||
resetPassword: {
|
||||
message: '',
|
||||
showLoading: false,
|
||||
},
|
||||
error: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({ globalConfig: 'globalConfig/get' }),
|
||||
},
|
||||
validations: {
|
||||
credentials: {
|
||||
email: {
|
||||
required,
|
||||
email,
|
||||
minLength: minLength(4),
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
showAlert(message) {
|
||||
// Reset loading, current selected agent
|
||||
this.resetPassword.showLoading = false;
|
||||
bus.$emit('newToastMessage', message);
|
||||
},
|
||||
submit() {
|
||||
this.resetPassword.showLoading = true;
|
||||
resetPassword(this.credentials)
|
||||
.then(res => {
|
||||
let successMessage = this.$t('RESET_PASSWORD.API.SUCCESS_MESSAGE');
|
||||
if (res.data && res.data.message) {
|
||||
successMessage = res.data.message;
|
||||
}
|
||||
this.showAlert(successMessage);
|
||||
})
|
||||
.catch(error => {
|
||||
let errorMessage = this.$t('RESET_PASSWORD.API.ERROR_MESSAGE');
|
||||
if (error?.response?.data?.message) {
|
||||
errorMessage = error.response.data.message;
|
||||
}
|
||||
this.showAlert(errorMessage);
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
87
app/javascript/v3/views/auth/signup/Index.vue
Normal file
87
app/javascript/v3/views/auth/signup/Index.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div class="h-full w-full dark:bg-slate-900">
|
||||
<div v-show="!isLoading" class="flex h-full">
|
||||
<div
|
||||
class="flex-1 min-h-[640px] inline-flex items-center h-full justify-center overflow-auto py-6"
|
||||
>
|
||||
<div class="px-8 max-w-[560px] w-full overflow-auto">
|
||||
<div class="mb-4">
|
||||
<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"
|
||||
/>
|
||||
<h2
|
||||
class="mb-7 mt-6 text-left text-3xl font-medium text-slate-900 dark:text-woot-50"
|
||||
>
|
||||
{{ $t('REGISTER.TRY_WOOT') }}
|
||||
</h2>
|
||||
</div>
|
||||
<signup-form />
|
||||
<div class="text-sm text-slate-800 dark:text-woot-50 px-1">
|
||||
<span>{{ $t('REGISTER.HAVE_AN_ACCOUNT') }}</span>
|
||||
<router-link class="text-link" to="/app/login">
|
||||
{{
|
||||
useInstallationName(
|
||||
$t('LOGIN.TITLE'),
|
||||
globalConfig.installationName
|
||||
)
|
||||
}}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<testimonials
|
||||
v-if="isAChatwootInstance"
|
||||
class="flex-1"
|
||||
@resize-containers="resizeContainers"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-show="isLoading"
|
||||
class="flex items-center justify-center h-full w-full"
|
||||
>
|
||||
<spinner color-scheme="primary" size="" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
import SignupForm from './components/Signup/Form.vue';
|
||||
import Testimonials from './components/Testimonials/Index.vue';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SignupForm,
|
||||
Spinner,
|
||||
Testimonials,
|
||||
},
|
||||
mixins: [globalConfigMixin],
|
||||
data() {
|
||||
return { isLoading: false };
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({ globalConfig: 'globalConfig/get' }),
|
||||
isAChatwootInstance() {
|
||||
return this.globalConfig.installationName === 'Chatwoot';
|
||||
},
|
||||
},
|
||||
beforeMount() {
|
||||
this.isLoading = this.isAChatwootInstance;
|
||||
},
|
||||
methods: {
|
||||
resizeContainers() {
|
||||
this.isLoading = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<woot-button
|
||||
size="expanded"
|
||||
color-scheme="primary"
|
||||
class-names="submit--button"
|
||||
:is-disabled="isDisabled"
|
||||
:is-loading="isLoading"
|
||||
@click="onClick"
|
||||
>
|
||||
{{ label }}
|
||||
<fluent-icon :icon="icon" size="18" />
|
||||
</woot-button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isDisabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onClick() {
|
||||
this.$emit('click');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.submit--button {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin: 0 0 var(--space-normal) 0;
|
||||
|
||||
&::v-deep .button__content {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
218
app/javascript/v3/views/auth/signup/components/Signup/Form.vue
Normal file
218
app/javascript/v3/views/auth/signup/components/Signup/Form.vue
Normal file
@@ -0,0 +1,218 @@
|
||||
<template>
|
||||
<div class="flex-1 overflow-auto px-1">
|
||||
<form class="space-y-3" @submit.prevent="submit">
|
||||
<div class="flex">
|
||||
<form-input
|
||||
v-model.trim="credentials.fullName"
|
||||
name="full_name"
|
||||
class="flex-1"
|
||||
:class="{ error: $v.credentials.fullName.$error }"
|
||||
:label="$t('REGISTER.FULL_NAME.LABEL')"
|
||||
:placeholder="$t('REGISTER.FULL_NAME.PLACEHOLDER')"
|
||||
:has-error="$v.credentials.fullName.$error"
|
||||
:error-message="$t('REGISTER.FULL_NAME.ERROR')"
|
||||
@blur="$v.credentials.fullName.$touch"
|
||||
/>
|
||||
<form-input
|
||||
v-model.trim="credentials.accountName"
|
||||
name="account_name"
|
||||
class="flex-1 ml-2"
|
||||
:class="{ error: $v.credentials.accountName.$error }"
|
||||
:label="$t('REGISTER.COMPANY_NAME.LABEL')"
|
||||
:placeholder="$t('REGISTER.COMPANY_NAME.PLACEHOLDER')"
|
||||
:has-error="$v.credentials.accountName.$error"
|
||||
:error-message="$t('REGISTER.COMPANY_NAME.ERROR')"
|
||||
@blur="$v.credentials.accountName.$touch"
|
||||
/>
|
||||
</div>
|
||||
<form-input
|
||||
v-model.trim="credentials.email"
|
||||
type="email"
|
||||
name="email_address"
|
||||
:class="{ error: $v.credentials.email.$error }"
|
||||
:label="$t('REGISTER.EMAIL.LABEL')"
|
||||
:placeholder="$t('REGISTER.EMAIL.PLACEHOLDER')"
|
||||
:has-error="$v.credentials.email.$error"
|
||||
:error-message="$t('REGISTER.EMAIL.ERROR')"
|
||||
@blur="$v.credentials.email.$touch"
|
||||
/>
|
||||
<form-input
|
||||
v-model.trim="credentials.password"
|
||||
type="password"
|
||||
name="password"
|
||||
:class="{ error: $v.credentials.password.$error }"
|
||||
:label="$t('LOGIN.PASSWORD.LABEL')"
|
||||
:placeholder="$t('SET_NEW_PASSWORD.PASSWORD.PLACEHOLDER')"
|
||||
:has-error="$v.credentials.password.$error"
|
||||
:error-message="passwordErrorText"
|
||||
@blur="$v.credentials.password.$touch"
|
||||
/>
|
||||
<div v-if="globalConfig.hCaptchaSiteKey" class="mb-3">
|
||||
<vue-hcaptcha
|
||||
ref="hCaptcha"
|
||||
:class="{ error: !hasAValidCaptcha && didCaptchaReset }"
|
||||
:sitekey="globalConfig.hCaptchaSiteKey"
|
||||
@verify="onRecaptchaVerified"
|
||||
/>
|
||||
<span
|
||||
v-if="!hasAValidCaptcha && didCaptchaReset"
|
||||
class="text-xs text-red-400"
|
||||
>
|
||||
{{ $t('SET_NEW_PASSWORD.CAPTCHA.ERROR') }}
|
||||
</span>
|
||||
</div>
|
||||
<submit-button
|
||||
:button-text="$t('REGISTER.SUBMIT')"
|
||||
:disabled="isSignupInProgress || !hasAValidCaptcha"
|
||||
:loading="isSignupInProgress"
|
||||
icon-class="arrow-chevron-right"
|
||||
/>
|
||||
</form>
|
||||
<GoogleOAuthButton v-if="showGoogleOAuth" class="flex-col-reverse">
|
||||
{{ $t('REGISTER.OAUTH.GOOGLE_SIGNUP') }}
|
||||
</GoogleOAuthButton>
|
||||
<p
|
||||
class="text-sm mb-1 mt-5 text-slate-800 dark:text-woot-50 [&>a]:text-woot-500 [&>a]:font-medium [&>a]:hover:text-woot-600"
|
||||
v-html="termsLink"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
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 { DEFAULT_REDIRECT_URL } from 'dashboard/constants/globals';
|
||||
import VueHcaptcha from '@hcaptcha/vue-hcaptcha';
|
||||
import FormInput from '../../../../../components/Form/Input.vue';
|
||||
import SubmitButton from '../../../../../components/Button/SubmitButton.vue';
|
||||
import { isValidPassword } from 'shared/helpers/Validators';
|
||||
import GoogleOAuthButton from '../../../../../components/GoogleOauth/Button.vue';
|
||||
import { register } from '../../../../../api/auth';
|
||||
var CompanyEmailValidator = require('company-email-validator');
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FormInput,
|
||||
GoogleOAuthButton,
|
||||
SubmitButton,
|
||||
VueHcaptcha,
|
||||
},
|
||||
mixins: [globalConfigMixin, alertMixin],
|
||||
data() {
|
||||
return {
|
||||
credentials: {
|
||||
accountName: '',
|
||||
fullName: '',
|
||||
email: '',
|
||||
password: '',
|
||||
hCaptchaClientResponse: '',
|
||||
},
|
||||
didCaptchaReset: false,
|
||||
isSignupInProgress: false,
|
||||
error: '',
|
||||
};
|
||||
},
|
||||
validations: {
|
||||
credentials: {
|
||||
accountName: {
|
||||
required,
|
||||
minLength: minLength(2),
|
||||
},
|
||||
fullName: {
|
||||
required,
|
||||
minLength: minLength(2),
|
||||
},
|
||||
email: {
|
||||
required,
|
||||
email,
|
||||
businessEmailValidator(value) {
|
||||
return CompanyEmailValidator.isCompanyEmail(value);
|
||||
},
|
||||
},
|
||||
password: {
|
||||
required,
|
||||
isValidPassword,
|
||||
minLength: minLength(6),
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({ globalConfig: 'globalConfig/get' }),
|
||||
termsLink() {
|
||||
return this.$t('REGISTER.TERMS_ACCEPT')
|
||||
.replace('https://www.chatwoot.com/terms', this.globalConfig.termsURL)
|
||||
.replace(
|
||||
'https://www.chatwoot.com/privacy-policy',
|
||||
this.globalConfig.privacyURL
|
||||
);
|
||||
},
|
||||
hasAValidCaptcha() {
|
||||
if (this.globalConfig.hCaptchaSiteKey) {
|
||||
return !!this.credentials.hCaptchaClientResponse;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
passwordErrorText() {
|
||||
const { password } = this.$v.credentials;
|
||||
if (!password.$error) {
|
||||
return '';
|
||||
}
|
||||
if (!password.minLength) {
|
||||
return this.$t('REGISTER.PASSWORD.ERROR');
|
||||
}
|
||||
if (!password.isValidPassword) {
|
||||
return this.$t('REGISTER.PASSWORD.IS_INVALID_PASSWORD');
|
||||
}
|
||||
return '';
|
||||
},
|
||||
showGoogleOAuth() {
|
||||
return Boolean(window.chatwootConfig.googleOAuthClientId);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async submit() {
|
||||
this.$v.$touch();
|
||||
if (this.$v.$invalid) {
|
||||
this.resetCaptcha();
|
||||
return;
|
||||
}
|
||||
this.isSignupInProgress = true;
|
||||
try {
|
||||
await register(this.credentials);
|
||||
window.location = DEFAULT_REDIRECT_URL;
|
||||
} catch (error) {
|
||||
let errorMessage =
|
||||
error?.message || this.$t('REGISTER.API.ERROR_MESSAGE');
|
||||
this.resetCaptcha();
|
||||
this.showAlert(errorMessage);
|
||||
} finally {
|
||||
this.isSignupInProgress = false;
|
||||
}
|
||||
},
|
||||
onRecaptchaVerified(token) {
|
||||
this.credentials.hCaptchaClientResponse = token;
|
||||
this.didCaptchaReset = false;
|
||||
},
|
||||
resetCaptcha() {
|
||||
if (!this.globalConfig.hCaptchaSiteKey) {
|
||||
return;
|
||||
}
|
||||
this.$refs.hCaptcha.reset();
|
||||
this.credentials.hCaptchaClientResponse = '';
|
||||
this.didCaptchaReset = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.h-captcha--box {
|
||||
&::v-deep .error {
|
||||
iframe {
|
||||
border: 1px solid var(--r-500);
|
||||
border-radius: var(--border-radius-normal);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="testimonials.length"
|
||||
class="hidden bg-woot-400 dark:bg-woot-800 overflow-hidden relative xl:flex flex-1"
|
||||
>
|
||||
<img
|
||||
src="/assets/images/auth/top-left.svg"
|
||||
class="left-0 absolute h-40 w-40 top-0"
|
||||
/>
|
||||
<img
|
||||
src="/assets/images/auth/bottom-right.svg"
|
||||
class="right-0 absolute h-40 w-40 bottom-0"
|
||||
/>
|
||||
<img
|
||||
src="/assets/images/auth/auth--bg.svg"
|
||||
class="h-[96%] left-[6%] top-[8%] w-[96%] absolute"
|
||||
/>
|
||||
<div class="flex items-center justify-center flex-col h-full w-full z-50">
|
||||
<div class="flex items-start justify-center p-6">
|
||||
<testimonial-card
|
||||
v-for="(testimonial, index) in testimonials"
|
||||
:key="testimonial.id"
|
||||
:review-content="testimonial.authorReview"
|
||||
:author-image="testimonial.authorImage"
|
||||
:author-name="testimonial.authorName"
|
||||
:author-designation="testimonial.authorCompany"
|
||||
:class="!index ? 'mt-[20%] -mr-4 z-50' : ''"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TestimonialCard from './TestimonialCard.vue';
|
||||
import { getTestimonialContent } from '../../../../../api/testimonials';
|
||||
export default {
|
||||
components: { TestimonialCard },
|
||||
data() {
|
||||
return { testimonials: [] };
|
||||
},
|
||||
beforeMount() {
|
||||
this.fetchTestimonials();
|
||||
},
|
||||
methods: {
|
||||
async fetchTestimonials() {
|
||||
try {
|
||||
const { data } = await getTestimonialContent();
|
||||
this.testimonials = data;
|
||||
} catch (error) {
|
||||
// Ignoring the error as the UI wouldn't break
|
||||
} finally {
|
||||
this.$emit('resize-containers', !!this.testimonials.length);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.center--img {
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col items-start justify-center p-6 w-80 bg-white rounded-lg drop-shadow-md dark:bg-slate-800"
|
||||
>
|
||||
<p class="text-sm text-slate-600 dark:text-woot-50 tracking-normal">
|
||||
{{ reviewContent }}
|
||||
</p>
|
||||
<div class="flex items-center mt-4 text-slate-700 dark:text-woot-50">
|
||||
<div class="bg-white rounded-full p-1">
|
||||
<img :src="authorImage" class="h-8 w-8 rounded-full" />
|
||||
</div>
|
||||
<div class="ml-2">
|
||||
<div class="text-sm font-medium">{{ authorName }}</div>
|
||||
<div class="text-xs">{{ authorDesignation }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
reviewContent: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
authorImage: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
authorName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
authorDesignation: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
Reference in New Issue
Block a user