feat: Update Signup screen (#6002)

* feat: Update Signup page designs

* feat: Update the signup page with dynamic testimonials

* Remove the images

* chore: Minor UI fixes

* chore: Form aligned to centre

* Update app/javascript/dashboard/routes/auth/components/Signup/Form.vue

* Design improvements

* Update company name key

* Revert "chore: Minor UI fixes"

This reverts commit 1556f4ca835d9aa0d9620fd6a3d52d259f0d7d65.

* Revert "Design improvements

This reverts commit dfb2364cf2f0cc93123698fde92e5f9e00536cc2.

* Remove footer

* Fix spacing

* Update app/views/installation/onboarding/index.html.erb

Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
This commit is contained in:
Pranav Raj S
2022-12-07 15:55:03 -08:00
committed by GitHub
parent 6064aad38f
commit 779bcf5e0d
17 changed files with 840 additions and 261 deletions

View File

@@ -0,0 +1,6 @@
/* global axios */
import wootConstants from 'dashboard/constants';
export const getTestimonialContent = () => {
return axios.get(wootConstants.TESTIMONIAL_URL);
};

View File

@@ -22,5 +22,6 @@ export default {
EXPANDED: 'expanded',
},
DOCS_URL: '//www.chatwoot.com/docs/product/',
TESTIMONIAL_URL: 'https://testimonials.cdn.chatwoot.com/content.json',
};
export const DEFAULT_REDIRECT_URL = '/app/';

View File

@@ -99,11 +99,7 @@
},
"AVAILABILITY": {
"LABEL": "Availability",
"STATUSES_LIST": [
"Online",
"Busy",
"Offline"
]
"STATUSES_LIST": ["Online", "Busy", "Offline"]
},
"EMAIL": {
"LABEL": "Your email address",
@@ -257,7 +253,7 @@
},
"FORM": {
"NAME": {
"LABEL": "Account Name",
"LABEL": "Company Name",
"PLACEHOLDER": "Wayne Enterprises"
},
"SUBMIT": "Submit"

View File

@@ -2,6 +2,8 @@
"REGISTER": {
"TRY_WOOT": "Register an account",
"TITLE": "Register",
"TESTIMONIAL_HEADER": "All it takes is one step to move forward",
"TESTIMONIAL_CONTENT": "You're one step away from engaging your customers, retaining them and finding new ones.",
"TERMS_ACCEPT": "By signing up, you agree to our <a href=\"https://www.chatwoot.com/terms\">T & C</a> and <a href=\"https://www.chatwoot.com/privacy-policy\">Privacy policy</a>",
"COMPANY_NAME": {
"LABEL": "Company name",

View File

@@ -1,283 +1,133 @@
<template>
<div class="medium-10 column signup">
<div class="text-center medium-12 signup--hero">
<img
:src="globalConfig.logo"
:alt="globalConfig.installationName"
class="hero--logo"
/>
<h2 class="hero--title">
{{ $t('REGISTER.TRY_WOOT') }}
</h2>
</div>
<div class="row align-center">
<div class="small-12 medium-6 large-5 column">
<form class="signup--box login-box" @submit.prevent="submit">
<woot-input
v-model="credentials.fullName"
:class="{ error: $v.credentials.fullName.$error }"
:label="$t('REGISTER.FULL_NAME.LABEL')"
:placeholder="$t('REGISTER.FULL_NAME.PLACEHOLDER')"
:error="
$v.credentials.fullName.$error
? $t('REGISTER.FULL_NAME.ERROR')
: ''
"
@blur="$v.credentials.fullName.$touch"
/>
<woot-input
v-model.trim="credentials.email"
type="email"
:class="{ error: $v.credentials.email.$error }"
:label="$t('REGISTER.EMAIL.LABEL')"
:placeholder="$t('REGISTER.EMAIL.PLACEHOLDER')"
:error="
$v.credentials.email.$error ? $t('REGISTER.EMAIL.ERROR') : ''
"
@blur="$v.credentials.email.$touch"
/>
<woot-input
v-model="credentials.accountName"
:class="{ error: $v.credentials.accountName.$error }"
:label="$t('REGISTER.COMPANY_NAME.LABEL')"
:placeholder="$t('REGISTER.COMPANY_NAME.PLACEHOLDER')"
:error="
$v.credentials.accountName.$error
? $t('REGISTER.COMPANY_NAME.ERROR')
: ''
"
@blur="$v.credentials.accountName.$touch"
/>
<woot-input
v-model.trim="credentials.password"
type="password"
:class="{ error: $v.credentials.password.$error }"
:label="$t('LOGIN.PASSWORD.LABEL')"
:placeholder="$t('SET_NEW_PASSWORD.PASSWORD.PLACEHOLDER')"
:error="passwordErrorText"
@blur="$v.credentials.password.$touch"
/>
<div v-if="globalConfig.hCaptchaSiteKey" class="h-captcha--box">
<vue-hcaptcha
ref="hCaptcha"
:class="{ error: !hasAValidCaptcha && didCaptchaReset }"
:sitekey="globalConfig.hCaptchaSiteKey"
@verify="onRecaptchaVerified"
<div class="h-full w-full">
<div v-show="!isLoading" class="row h-full">
<div
:class="
`${showTestimonials ? 'large-6' : 'large-12'} signup-form--container`
"
>
<div class="signup-form--content">
<div class="signup--hero">
<img
:src="globalConfig.logo"
:alt="globalConfig.installationName"
class="hero--logo"
/>
<span
v-if="!hasAValidCaptcha && didCaptchaReset"
class="captcha-error"
>
{{ $t('SET_NEW_PASSWORD.CAPTCHA.ERROR') }}
</span>
<h2 class="hero--title">
{{ $t('REGISTER.TRY_WOOT') }}
</h2>
</div>
<signup-form />
<div class="auth-screen--footer">
<span>{{ $t('REGISTER.HAVE_AN_ACCOUNT') }}</span>
<router-link to="/app/login">
{{
useInstallationName(
$t('LOGIN.TITLE'),
globalConfig.installationName
)
}}
</router-link>
</div>
<woot-submit-button
:disabled="isSignupInProgress || !hasAValidCaptcha"
:button-text="$t('REGISTER.SUBMIT')"
:loading="isSignupInProgress"
button-class="large expanded"
/>
<p v-dompurify-html="termsLink" class="accept--terms" />
</form>
<div class="column text-center sigin--footer">
<span>{{ $t('REGISTER.HAVE_AN_ACCOUNT') }}</span>
<router-link to="/app/login">
{{
useInstallationName(
$t('LOGIN.TITLE'),
globalConfig.installationName
)
}}
</router-link>
</div>
</div>
<testimonials
v-if="isAChatwootInstance"
class="medium-6 testimonial--container"
@resize-containers="resizeContainers"
/>
</div>
<div v-show="isLoading" class="spinner--container">
<spinner size="" />
</div>
</div>
</template>
<script>
import { required, minLength, email } from 'vuelidate/lib/validators';
import Auth from '../../api/auth';
import { mapGetters } from 'vuex';
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
import alertMixin from 'shared/mixins/alertMixin';
import { DEFAULT_REDIRECT_URL } from '../../constants';
import VueHcaptcha from '@hcaptcha/vue-hcaptcha';
import { isValidPassword } from 'shared/helpers/Validators';
const CompanyEmailValidator = require('company-email-validator');
import SignupForm from './components/Signup/Form.vue';
import Testimonials from './components/Testimonials/Index.vue';
import Spinner from 'shared/components/Spinner.vue';
export default {
components: {
VueHcaptcha,
SignupForm,
Spinner,
Testimonials,
},
mixins: [globalConfigMixin, alertMixin],
mixins: [globalConfigMixin],
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),
},
},
return { showTestimonials: false, isLoading: false };
},
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 '';
isAChatwootInstance() {
return this.globalConfig.installationName === 'Chatwoot';
},
},
beforeMount() {
this.isLoading = this.isAChatwootInstance;
},
methods: {
async submit() {
this.$v.$touch();
if (this.$v.$invalid) {
this.resetCaptcha();
return;
}
this.isSignupInProgress = true;
try {
const response = await Auth.register(this.credentials);
if (response.status === 200) {
window.location = DEFAULT_REDIRECT_URL;
}
} catch (error) {
let errorMessage = this.$t('REGISTER.API.ERROR_MESSAGE');
if (error.response && error.response.data.message) {
this.resetCaptcha();
errorMessage = error.response.data.message;
}
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;
resizeContainers(hasTestimonials) {
this.showTestimonials = hasTestimonials;
this.isLoading = false;
},
},
};
</script>
<style scoped lang="scss">
.signup {
.signup--hero {
margin-bottom: var(--space-larger);
.signup-form--container {
display: flex;
align-items: center;
height: 100%;
min-height: 640px;
overflow: auto;
justify-content: center;
}
.hero--logo {
width: 180px;
}
.signup-form--content {
padding: var(--space-jumbo);
max-width: 600px;
width: 100%;
}
.hero--title {
margin-top: var(--space-large);
font-weight: var(--font-weight-light);
}
.signup--hero {
margin-bottom: var(--space-normal);
.hero--logo {
width: 160px;
}
.signup--box {
padding: var(--space-large);
label {
font-size: var(--font-size-default);
color: var(--b-600);
input {
padding: var(--space-slab);
height: var(--space-larger);
font-size: var(--font-size-default);
}
}
}
.sigin--footer {
padding: var(--space-medium);
font-size: var(--font-size-default);
> a {
font-weight: var(--font-weight-bold);
}
}
.accept--terms {
font-size: var(--font-size-small);
text-align: center;
margin: var(--space-normal) 0 0 0;
}
.h-captcha--box {
margin-bottom: var(--space-one);
.captcha-error {
color: var(--r-400);
font-size: var(--font-size-small);
}
&::v-deep .error {
iframe {
border: 1px solid var(--r-500);
border-radius: var(--border-radius-normal);
}
}
.hero--title {
margin-top: var(--space-medium);
font-weight: var(--font-weight-light);
}
}
.auth-screen--footer {
font-size: var(--font-size-small);
}
@media screen and (max-width: 1200px) {
.testimonial--container {
display: none;
}
.signup-form--container {
width: 100%;
flex: 0 0 100%;
max-width: 100%;
}
.signup-form--content {
margin: 0 auto;
}
}
.spinner--container {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
</style>

View File

@@ -8,6 +8,12 @@ const Signup = () => import('./Signup');
export default {
routes: [
{
path: frontendURL('auth/signup'),
name: 'auth_signup',
component: Signup,
meta: { requireSignupEnabled: true },
},
{
path: frontendURL('auth'),
name: 'auth',
@@ -33,12 +39,6 @@ export default {
redirectUrl: route.query.route_url,
}),
},
{
path: 'signup',
name: 'auth_signup',
component: Signup,
meta: { requireSignupEnabled: true },
},
{
path: 'reset/password',
name: 'auth_reset_password',

View File

@@ -0,0 +1,93 @@
<template>
<label class="auth-input--wrap">
<div class="label-wrap">
<fluent-icon v-if="iconName" :icon="iconName" size="16" />
<span v-if="label">{{ label }}</span>
</div>
<div class="input--wrap">
<input
class="auth-input"
:value="value"
:type="type"
:placeholder="placeholder"
:readonly="readonly"
@input="onChange"
@blur="onBlur"
/>
<p v-if="helpText" class="help-text" />
<span v-if="error" class="message">
{{ error }}
</span>
</div>
</label>
</template>
<script>
export default {
props: {
label: {
type: String,
default: '',
},
value: {
type: [String, Number],
default: '',
},
type: {
type: String,
default: 'text',
},
placeholder: {
type: String,
default: '',
},
iconName: {
type: String,
default: '',
},
helpText: {
type: String,
default: '',
},
error: {
type: String,
default: '',
},
readonly: {
type: Boolean,
default: false,
},
},
methods: {
onChange(e) {
this.$emit('input', e.target.value);
},
onBlur(e) {
this.$emit('blur', e.target.value);
},
},
};
</script>
<style lang="scss" scoped>
.auth-input--wrap {
.label-wrap {
display: flex;
align-items: center;
color: var(--s-900);
margin-bottom: var(--space-smaller);
span {
margin-left: var(--space-smaller);
font-size: var(--font-size-small);
}
}
.auth-input {
font-size: var(--font-size-small) !important;
height: 4rem !important;
padding: var(--space-small) !important;
width: 100% !important;
background: var(--s-50) !important;
}
}
</style>

View File

@@ -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>

View File

@@ -0,0 +1,218 @@
<template>
<form @submit.prevent="submit">
<div class="input-wrap">
<auth-input
v-model="credentials.fullName"
:class="{ error: $v.credentials.fullName.$error }"
:label="$t('REGISTER.FULL_NAME.LABEL')"
icon-name="person"
:placeholder="$t('REGISTER.FULL_NAME.PLACEHOLDER')"
:error="
$v.credentials.fullName.$error ? $t('REGISTER.FULL_NAME.ERROR') : ''
"
@blur="$v.credentials.fullName.$touch"
/>
<auth-input
v-model.trim="credentials.email"
type="email"
:class="{ error: $v.credentials.email.$error }"
icon-name="mail"
:label="$t('REGISTER.EMAIL.LABEL')"
:placeholder="$t('REGISTER.EMAIL.PLACEHOLDER')"
:error="$v.credentials.email.$error ? $t('REGISTER.EMAIL.ERROR') : ''"
@blur="$v.credentials.email.$touch"
/>
<auth-input
v-model="credentials.accountName"
:class="{ error: $v.credentials.accountName.$error }"
icon-name="building-bank"
:label="$t('REGISTER.COMPANY_NAME.LABEL')"
:placeholder="$t('REGISTER.COMPANY_NAME.PLACEHOLDER')"
:error="
$v.credentials.accountName.$error
? $t('REGISTER.COMPANY_NAME.ERROR')
: ''
"
@blur="$v.credentials.accountName.$touch"
/>
<auth-input
v-model.trim="credentials.password"
type="password"
:class="{ error: $v.credentials.password.$error }"
icon-name="lock-closed"
:label="$t('LOGIN.PASSWORD.LABEL')"
:placeholder="$t('SET_NEW_PASSWORD.PASSWORD.PLACEHOLDER')"
:error="passwordErrorText"
@blur="$v.credentials.password.$touch"
/>
</div>
<div v-if="globalConfig.hCaptchaSiteKey" class="h-captcha--box">
<vue-hcaptcha
ref="hCaptcha"
:class="{ error: !hasAValidCaptcha && didCaptchaReset }"
:sitekey="globalConfig.hCaptchaSiteKey"
@verify="onRecaptchaVerified"
/>
<span v-if="!hasAValidCaptcha && didCaptchaReset" class="captcha-error">
{{ $t('SET_NEW_PASSWORD.CAPTCHA.ERROR') }}
</span>
</div>
<auth-submit-button
:label="$t('REGISTER.SUBMIT')"
:is-disabled="isSignupInProgress || !hasAValidCaptcha"
:is-loading="isSignupInProgress"
icon="arrow-chevron-right"
@click="submit"
/>
<p v-dompurify-html="termsLink" class="accept--terms" />
</form>
</template>
<script>
import { required, minLength, email } from 'vuelidate/lib/validators';
import Auth from '../../../../api/auth';
import { mapGetters } from 'vuex';
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
import alertMixin from 'shared/mixins/alertMixin';
import { DEFAULT_REDIRECT_URL } from '../../../../constants';
import VueHcaptcha from '@hcaptcha/vue-hcaptcha';
import AuthInput from '../AuthInput.vue';
import AuthSubmitButton from '../AuthSubmitButton.vue';
import { isValidPassword } from 'shared/helpers/Validators';
var CompanyEmailValidator = require('company-email-validator');
export default {
components: {
AuthInput,
AuthSubmitButton,
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 '';
},
},
methods: {
async submit() {
this.$v.$touch();
if (this.$v.$invalid) {
this.resetCaptcha();
return;
}
this.isSignupInProgress = true;
try {
const response = await Auth.register(this.credentials);
if (response.status === 200) {
window.location = DEFAULT_REDIRECT_URL;
}
} catch (error) {
let errorMessage = this.$t('REGISTER.API.ERROR_MESSAGE');
if (error.response && error.response.data.message) {
this.resetCaptcha();
errorMessage = error.response.data.message;
}
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 {
margin-bottom: var(--space-small);
.captcha-error {
color: var(--r-400);
font-size: var(--font-size-small);
}
&::v-deep .error {
iframe {
border: 1px solid var(--r-500);
border-radius: var(--border-radius-normal);
}
}
}
.accept--terms {
margin: var(--space-normal) 0 var(--space-smaller) 0;
}
</style>

View File

@@ -0,0 +1,119 @@
<template>
<div v-if="testimonials.length" class="testimonial--section">
<img src="/assets/images/auth/top-left.svg" class="top-left--img" />
<img src="/assets/images/auth/bottom-right.svg" class="bottom-right--img" />
<img src="/assets/images/auth/auth--bg.svg" class="center--img" />
<div class="testimonial--content">
<div class="testimonial--content-card">
<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="`testimonial-${index ? 'right' : 'left'}--card`"
/>
</div>
</div>
</div>
</template>
<script>
import TestimonialCard from './TestimonialCard.vue';
import { getTestimonialContent } from 'dashboard/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>
@import '~dashboard/assets/scss/woot';
.top-left--img {
left: 0;
height: 16rem;
position: absolute;
top: 0;
width: 16rem;
}
.bottom-right--img {
bottom: 0;
height: 16rem;
position: absolute;
right: 0;
width: 16rem;
}
.center--img {
left: 5%;
max-height: 86%;
max-width: 90%;
position: absolute;
top: 2%;
}
.center-container {
padding: var(--space-medium) 0;
}
.testimonial--section {
background: var(--w-400);
display: flex;
flex: 1 1;
position: relative;
}
.testimonial--content {
align-content: center;
display: flex;
flex-direction: column;
height: 100%;
justify-content: center;
width: 100%;
z-index: 1000;
}
.testimonial--content-card {
align-items: flex-start;
display: flex;
justify-content: center;
padding: var(--space-larger);
}
.testimonial-left--card {
--signup-testimonial-top: 20%;
margin-top: var(--signup-testimonial-top);
margin-right: var(--space-minus-normal);
z-index: 10000;
}
@media screen and (max-width: 1200px) {
.testimonial--section {
display: none;
}
}
</style>

View File

@@ -0,0 +1,93 @@
<template>
<div class="testimonial-card">
<div class="left-card--wrap absolute">
<div class="left-card--content">
<p class="card-content">
{{ reviewContent }}
</p>
<div class="content-author--details row">
<div class="author-image--wrap">
<img :src="authorImage" class="author-image" />
</div>
<div class="author-name-company--details">
<div class="author-name">{{ authorName }}</div>
<div class="author-company">{{ authorDesignation }}</div>
</div>
</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: '',
},
},
setup() {},
};
</script>
<style scoped lang="scss">
.testimonial-card {
align-items: center;
background: var(--white);
border-radius: var(--border-radius-normal);
box-shadow: var(--shadow-large);
display: flex;
justify-content: center;
padding: var(--space-medium) var(--space-large);
width: 32rem;
}
.content-author--details {
align-items: center;
display: flex;
margin-top: var(--space-small);
.author-image--wrap {
background: white;
border-radius: var(--border-radius-rounded);
padding: var(--space-smaller);
.author-image {
border-radius: var(--border-radius-rounded);
height: calc(var(--space-two) + var(--space-two));
width: calc(var(--space-two) + var(--space-two));
}
}
.author-name-company--details {
margin-left: var(--space-small);
.author-name {
font-size: var(--font-size-small);
font-weight: var(--font-weight-bold);
}
.author-company {
font-size: var(--font-size-mini);
}
}
}
.card-content {
color: var(--s-600);
// font-size: var(--font-size-default);
line-height: 1.7;
}
</style>

View File

@@ -0,0 +1,47 @@
<template>
<div class="testimonial--footer">
<h2 class="heading">
{{ title }}
</h2>
<span class="sub-block-title sub-heading">
{{ subTitle }}
</span>
</div>
</template>
<script>
export default {
props: {
title: {
type: String,
default: '',
},
subTitle: {
type: String,
default: '',
},
},
};
</script>
<style scoped lang="scss">
.testimonial--footer {
align-items: center;
bottom: var(--space-jumbo);
display: flex;
flex-direction: column;
text-align: center;
margin: 0 auto;
padding: 0 var(--space-jumbo);
position: absolute;
width: 100%;
.heading {
color: var(--white);
font-size: var(--font-size-bigger);
}
.sub-heading {
color: var(--white);
font-weight: var(--font-weight-medium);
}
}
</style>