feat: Eslint rules (#9839)
# Pull Request Template ## Description This PR adds new eslint rules to the code base. **Error rules** | Rule name | Type | Files updated | | ----------------- | --- | - | | `vue/block-order` | error | ✅ | | `vue/component-name-in-template-casing` | error | ✅ | | `vue/component-options-name-casing` | error | ✅ | | `vue/custom-event-name-casing` | error | ✅ | | `vue/define-emits-declaration` | error | ✅ | | `vue/no-unused-properties` | error | ✅ | | `vue/define-macros-order` | error | ✅ | | `vue/define-props-declaration` | error | ✅ | | `vue/match-component-import-name` | error | ✅ | | `vue/next-tick-style` | error | ✅ | | `vue/no-bare-strings-in-template` | error | ✅ | | `vue/no-empty-component-block` | error | ✅ | | `vue/no-multiple-objects-in-class` | error | ✅ | | `vue/no-required-prop-with-default` | error | ✅ | | `vue/no-static-inline-styles` | error | ✅ | | `vue/no-template-target-blank` | error | ✅ | | `vue/no-this-in-before-route-enter` | error | ✅ | | `vue/no-undef-components` | error | ✅ | | `vue/no-unused-emit-declarations` | error | ✅ | | `vue/no-unused-refs` | error | ✅ | | `vue/no-use-v-else-with-v-for` | error | ✅ | | `vue/no-useless-v-bind` | error | ✅ | | `vue/no-v-text` | error | ✅ | | `vue/padding-line-between-blocks` | error | ✅ | | ~`vue/prefer-prop-type-boolean-first`~ | ~error~ | ❌ (removed this rule, cause a bug in displaying custom attributes) | | `vue/prefer-separate-static-class` | error | ✅ | | `vue/prefer-true-attribute-shorthand` | error | ✅ | | `vue/require-explicit-slots` | error | ✅ | | `vue/require-macro-variable-name` | error | ✅ | **Warn rules** | Rule name | Type | Files updated | | ---- | ------------- | ------------- | | `vue/no-root-v-if` | warn | ❎ | Fixes https://linear.app/chatwoot/issue/CW-3492/vue-eslint-rules ## Type of change - [x] New feature (non-breaking change which adds functionality) ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Fayaz Ahmed <fayazara@gmail.com> Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> Co-authored-by: Shivam Mishra <scm.mymail@gmail.com> Co-authored-by: Pranav <pranav@chatwoot.com>
This commit is contained in:
@@ -1,9 +1,3 @@
|
||||
<template>
|
||||
<div class="h-full w-full antialiased" :class="theme">
|
||||
<router-view />
|
||||
<snackbar-container />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import SnackbarContainer from './components/SnackBar/Container.vue';
|
||||
|
||||
@@ -42,6 +36,14 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full w-full antialiased" :class="theme">
|
||||
<router-view />
|
||||
<SnackbarContainer />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
|
||||
@@ -1,18 +1,3 @@
|
||||
<template>
|
||||
<button
|
||||
:type="type"
|
||||
data-testid="submit_button"
|
||||
:disabled="disabled"
|
||||
:class="computedClass"
|
||||
class="flex items-center w-full justify-center rounded-md bg-woot-500 py-3 px-3 text-base font-medium text-white shadow-sm hover:bg-woot-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-woot-500 cursor-pointer"
|
||||
@click="onClick"
|
||||
>
|
||||
<span>{{ buttonText }}</span>
|
||||
<fluent-icon v-if="!!iconClass" :icon="iconClass" class="icon" />
|
||||
<spinner v-if="loading" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
|
||||
@@ -61,3 +46,18 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
:type="type"
|
||||
data-testid="submit_button"
|
||||
:disabled="disabled"
|
||||
:class="computedClass"
|
||||
class="flex items-center w-full justify-center rounded-md bg-woot-500 py-3 px-3 text-base font-medium text-white shadow-sm hover:bg-woot-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-woot-500 cursor-pointer"
|
||||
@click="onClick"
|
||||
>
|
||||
<span>{{ buttonText }}</span>
|
||||
<fluent-icon v-if="!!iconClass" :icon="iconClass" class="icon" />
|
||||
<Spinner v-if="loading" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative my-4 section-separator">
|
||||
<div class="absolute inset-0 flex items-center" aria-hidden="true">
|
||||
@@ -12,13 +23,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,30 +1,3 @@
|
||||
<template>
|
||||
<button
|
||||
class="inline-flex items-center gap-1 text-sm font-medium reset-base rounded-xl w-fit"
|
||||
:class="buttonClasses"
|
||||
v-bind="$attrs"
|
||||
@click="onClick"
|
||||
>
|
||||
<fluent-icon
|
||||
v-if="icon && !trailingIcon"
|
||||
size="1.16em"
|
||||
:icon="icon"
|
||||
class="flex-shrink-0"
|
||||
/>
|
||||
<span
|
||||
v-if="$slots.default"
|
||||
class="text-sm font-medium truncate ltr:text-left rtl:text-right"
|
||||
>
|
||||
<slot />
|
||||
</span>
|
||||
<fluent-icon
|
||||
v-if="icon && trailingIcon"
|
||||
size="1.16em"
|
||||
:icon="icon"
|
||||
class="flex-shrink-0"
|
||||
/>
|
||||
</button>
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed, useAttrs } from 'vue';
|
||||
|
||||
@@ -54,6 +27,8 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['click']);
|
||||
|
||||
const attrs = useAttrs();
|
||||
|
||||
const baseClasses = {
|
||||
@@ -99,8 +74,6 @@ const sizeClass = computed(() => {
|
||||
|
||||
const buttonClasses = computed(() => [colorClass.value, sizeClass.value]);
|
||||
|
||||
const emit = defineEmits(['click']);
|
||||
|
||||
const onClick = () => {
|
||||
if (props.disabled) {
|
||||
return;
|
||||
@@ -108,3 +81,31 @@ const onClick = () => {
|
||||
emit('click');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="inline-flex items-center gap-1 text-sm font-medium reset-base rounded-xl w-fit"
|
||||
:class="buttonClasses"
|
||||
v-bind="$attrs"
|
||||
@click="onClick"
|
||||
>
|
||||
<fluent-icon
|
||||
v-if="icon && !trailingIcon"
|
||||
size="1.16em"
|
||||
:icon="icon"
|
||||
class="flex-shrink-0"
|
||||
/>
|
||||
<span
|
||||
v-if="$slots.default"
|
||||
class="text-sm font-medium truncate ltr:text-left rtl:text-right"
|
||||
>
|
||||
<slot />
|
||||
</span>
|
||||
<fluent-icon
|
||||
v-if="icon && trailingIcon"
|
||||
size="1.16em"
|
||||
:icon="icon"
|
||||
class="flex-shrink-0"
|
||||
/>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
@@ -1,13 +1,3 @@
|
||||
<template>
|
||||
<input
|
||||
:id="value"
|
||||
v-model="checked"
|
||||
type="checkbox"
|
||||
:value="value"
|
||||
class="flex-shrink-0 mt-0.5 border-ash-200 border bg-ash-50 checked:border-none checked:bg-primary-600 dark:checked:bg-primary-600 shadow-sm appearance-none rounded-[4px] w-4 h-4 focus:ring-1 after:content-[''] after:text-white checked:after:content-['✓'] after:flex after:items-center after:justify-center after:text-center after:text-xs after:font-bold after:relative"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
@@ -22,10 +12,20 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['input']);
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const checked = computed({
|
||||
get: () => props.isChecked,
|
||||
set: value => emit('update', props.value, value),
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input
|
||||
:id="value"
|
||||
v-model="checked"
|
||||
type="checkbox"
|
||||
:value="value"
|
||||
class="flex-shrink-0 mt-0.5 border-ash-200 border bg-ash-50 checked:border-none checked:bg-primary-600 dark:checked:bg-primary-600 shadow-sm appearance-none rounded-[4px] w-4 h-4 focus:ring-1 after:content-[''] after:text-white checked:after:content-['✓'] after:flex after:items-center after:justify-center after:text-center after:text-xs after:font-bold after:relative"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -1,27 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="rounded-xl flex leading-[100%] font-medium items-center justify-center text-center cursor-default"
|
||||
:class="`h-[${size}px] w-[${size}px] ${colorClass}`"
|
||||
:style="style"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<slot>{{ initial }}</slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { userInitial } from 'v3/helpers/CommonHelper';
|
||||
const colors = {
|
||||
1: 'bg-ash-200 text-ash-900',
|
||||
2: 'bg-amber-200 text-amber-900',
|
||||
3: 'bg-pink-100 text-pink-800',
|
||||
4: 'bg-purple-100 text-purple-800',
|
||||
5: 'bg-indigo-100 text-indigo-800',
|
||||
6: 'bg-grass-100 text-grass-800',
|
||||
7: 'bg-mint-100 text-mint-800',
|
||||
8: 'bg-orange-100 text-orange-800',
|
||||
};
|
||||
const props = defineProps({
|
||||
name: {
|
||||
type: String,
|
||||
@@ -32,6 +11,16 @@ const props = defineProps({
|
||||
default: 72,
|
||||
},
|
||||
});
|
||||
const colors = {
|
||||
1: 'bg-ash-200 text-ash-900',
|
||||
2: 'bg-amber-200 text-amber-900',
|
||||
3: 'bg-pink-100 text-pink-800',
|
||||
4: 'bg-purple-100 text-purple-800',
|
||||
5: 'bg-indigo-100 text-indigo-800',
|
||||
6: 'bg-grass-100 text-grass-800',
|
||||
7: 'bg-mint-100 text-mint-800',
|
||||
8: 'bg-orange-100 text-orange-800',
|
||||
};
|
||||
const style = computed(() => ({
|
||||
fontSize: `${Math.floor(props.size / 2.5)}px`,
|
||||
}));
|
||||
@@ -41,3 +30,14 @@ const colorClass = computed(() => {
|
||||
|
||||
const initial = computed(() => userInitial(props.name));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="rounded-xl flex leading-[100%] font-medium items-center justify-center text-center cursor-default"
|
||||
:class="`h-[${size}px] w-[${size}px] ${colorClass}`"
|
||||
:style="style"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<slot>{{ initial }}</slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,39 +1,3 @@
|
||||
<template>
|
||||
<with-label
|
||||
:label="label"
|
||||
:icon="icon"
|
||||
:name="name"
|
||||
:has-error="hasError"
|
||||
:error-message="errorMessage"
|
||||
>
|
||||
<template #rightOfLabel>
|
||||
<slot />
|
||||
</template>
|
||||
<input
|
||||
:id="name"
|
||||
:name="name"
|
||||
:type="type"
|
||||
autocomplete="off"
|
||||
:tabindex="tabindex"
|
||||
:required="required"
|
||||
:placeholder="placeholder"
|
||||
:data-testid="dataTestid"
|
||||
:value="value"
|
||||
:class="{
|
||||
'focus:outline-red-600 outline-red-600 dark:focus:outline-red-600 dark:outline-red-600':
|
||||
hasError,
|
||||
'outline-slate-200 dark:outline-slate-600 dark:focus:outline-woot-500 focus:outline-woot-500':
|
||||
!hasError,
|
||||
'px-3 py-3': spacing === 'base',
|
||||
'px-3 py-2 mb-0': spacing === 'compact',
|
||||
'pl-9': icon,
|
||||
}"
|
||||
class="block w-full border-none rounded-md shadow-sm appearance-none outline outline-1 focus:outline-2 text-slate-900 dark:text-slate-100 placeholder:text-slate-400 sm:text-sm sm:leading-6 dark:bg-slate-800"
|
||||
@input="onInput"
|
||||
@blur="$emit('blur')"
|
||||
/>
|
||||
</with-label>
|
||||
</template>
|
||||
<script>
|
||||
import WithLabel from './WithLabel.vue';
|
||||
export default {
|
||||
@@ -98,3 +62,40 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WithLabel
|
||||
:label="label"
|
||||
:icon="icon"
|
||||
:name="name"
|
||||
:has-error="hasError"
|
||||
:error-message="errorMessage"
|
||||
>
|
||||
<template #rightOfLabel>
|
||||
<slot />
|
||||
</template>
|
||||
<input
|
||||
:id="name"
|
||||
:name="name"
|
||||
:type="type"
|
||||
autocomplete="off"
|
||||
:tabindex="tabindex"
|
||||
:required="required"
|
||||
:placeholder="placeholder"
|
||||
:data-testid="dataTestid"
|
||||
:value="value"
|
||||
:class="{
|
||||
'focus:outline-red-600 outline-red-600 dark:focus:outline-red-600 dark:outline-red-600':
|
||||
hasError,
|
||||
'outline-slate-200 dark:outline-slate-600 dark:focus:outline-woot-500 focus:outline-woot-500':
|
||||
!hasError,
|
||||
'px-3 py-3': spacing === 'base',
|
||||
'px-3 py-2 mb-0': spacing === 'compact',
|
||||
'pl-9': icon,
|
||||
}"
|
||||
class="block w-full border-none rounded-md shadow-sm appearance-none outline outline-1 focus:outline-2 text-slate-900 dark:text-slate-100 placeholder:text-slate-400 sm:text-sm sm:leading-6 dark:bg-slate-800"
|
||||
@input="onInput"
|
||||
@blur="$emit('blur')"
|
||||
/>
|
||||
</WithLabel>
|
||||
</template>
|
||||
|
||||
@@ -1,3 +1,51 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import InitialsAvatar from './InitialsAvatar.vue';
|
||||
const props = defineProps({
|
||||
src: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['change', 'delete']);
|
||||
|
||||
const hasImageLoaded = ref(false);
|
||||
const imageLoadedError = ref(false);
|
||||
const fileInputRef = ref(null);
|
||||
|
||||
const shouldShowImage = computed(() => props.src && !imageLoadedError.value);
|
||||
|
||||
const onImageLoadError = () => {
|
||||
imageLoadedError.value = true;
|
||||
};
|
||||
|
||||
const onImageLoad = () => {
|
||||
hasImageLoaded.value = true;
|
||||
imageLoadedError.value = false;
|
||||
};
|
||||
|
||||
const openFileInput = () => {
|
||||
fileInputRef.value.click();
|
||||
};
|
||||
|
||||
const onImageUpload = event => {
|
||||
const [file] = event.target.files;
|
||||
emit('change', {
|
||||
file,
|
||||
url: file ? URL.createObjectURL(file) : null,
|
||||
});
|
||||
};
|
||||
|
||||
const onAvatarDelete = () => {
|
||||
emit('delete');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative rounded-xl h-[72px] w-[72px] cursor-pointe group">
|
||||
<img
|
||||
@@ -9,7 +57,7 @@
|
||||
@load="onImageLoad"
|
||||
@error="onImageLoadError"
|
||||
/>
|
||||
<initials-avatar v-else-if="!shouldShowImage" :name="name" :size="72" />
|
||||
<InitialsAvatar v-else-if="!shouldShowImage" :name="name" :size="72" />
|
||||
|
||||
<input
|
||||
ref="fileInputRef"
|
||||
@@ -35,50 +83,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import InitialsAvatar from './InitialsAvatar.vue';
|
||||
const props = defineProps({
|
||||
src: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(['change', 'delete']);
|
||||
|
||||
const hasImageLoaded = ref(false);
|
||||
const imageLoadedError = ref(false);
|
||||
const fileInputRef = ref(null);
|
||||
|
||||
const shouldShowImage = computed(() => props.src && !imageLoadedError.value);
|
||||
|
||||
const onImageLoadError = () => {
|
||||
imageLoadedError.value = true;
|
||||
};
|
||||
|
||||
const onImageLoad = () => {
|
||||
hasImageLoaded.value = true;
|
||||
imageLoadedError.value = false;
|
||||
};
|
||||
|
||||
const openFileInput = () => {
|
||||
fileInputRef.value.click();
|
||||
};
|
||||
|
||||
const onImageUpload = event => {
|
||||
const [file] = event.target.files;
|
||||
emits('change', {
|
||||
file,
|
||||
url: file ? URL.createObjectURL(file) : null,
|
||||
});
|
||||
};
|
||||
|
||||
const onAvatarDelete = () => {
|
||||
emits('delete');
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,25 +1,3 @@
|
||||
<template>
|
||||
<with-label
|
||||
:label="label"
|
||||
:name="name"
|
||||
:has-error="hasError"
|
||||
:error-message="errorMessage"
|
||||
>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<woot-button
|
||||
v-for="option in options"
|
||||
:key="option.value"
|
||||
:variant="value === option.value ? '' : 'hollow'"
|
||||
:color-scheme="value === option.value ? 'primary' : 'secondary'"
|
||||
size="small"
|
||||
@click="$emit('input', option.value)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</with-label>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WithLabel from './WithLabel.vue';
|
||||
export default {
|
||||
@@ -27,10 +5,6 @@ export default {
|
||||
WithLabel,
|
||||
},
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
@@ -58,3 +32,25 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WithLabel
|
||||
:label="label"
|
||||
:name="name"
|
||||
:has-error="hasError"
|
||||
:error-message="errorMessage"
|
||||
>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<woot-button
|
||||
v-for="option in options"
|
||||
:key="option.value"
|
||||
:variant="value === option.value ? '' : 'hollow'"
|
||||
:color-scheme="value === option.value ? 'primary' : 'secondary'"
|
||||
size="small"
|
||||
@click="$emit('input', option.value)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</WithLabel>
|
||||
</template>
|
||||
|
||||
@@ -1,35 +1,3 @@
|
||||
<template>
|
||||
<with-label
|
||||
:label="label"
|
||||
:icon="icon"
|
||||
:name="name"
|
||||
:has-error="hasError"
|
||||
:error-message="errorMessage"
|
||||
>
|
||||
<select
|
||||
:id="id"
|
||||
:selected="value"
|
||||
:name="name"
|
||||
:class="{
|
||||
'text-ash-400': !value,
|
||||
'text-ash-900': value,
|
||||
'pl-9': icon,
|
||||
}"
|
||||
class="block w-full px-3 py-2 pr-6 mb-0 border-0 shadow-sm outline-none appearance-none rounded-xl select-caret ring-ash-200 ring-1 ring-inset placeholder:text-ash-900 focus:ring-2 focus:ring-inset focus:ring-primary-500 sm:text-sm sm:leading-6"
|
||||
@input="onInput"
|
||||
>
|
||||
<option value="" disabled selected class="hidden">
|
||||
{{ placeholder }}
|
||||
</option>
|
||||
<slot>
|
||||
<option v-for="opt in options" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</option>
|
||||
</slot>
|
||||
</select>
|
||||
</with-label>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WithLabel from './WithLabel.vue';
|
||||
export default {
|
||||
@@ -81,6 +49,39 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WithLabel
|
||||
:label="label"
|
||||
:icon="icon"
|
||||
:name="name"
|
||||
:has-error="hasError"
|
||||
:error-message="errorMessage"
|
||||
>
|
||||
<select
|
||||
:id="id"
|
||||
:selected="value"
|
||||
:name="name"
|
||||
:class="{
|
||||
'text-ash-400': !value,
|
||||
'text-ash-900': value,
|
||||
'pl-9': icon,
|
||||
}"
|
||||
class="block w-full px-3 py-2 pr-6 mb-0 border-0 shadow-sm outline-none appearance-none rounded-xl select-caret ring-ash-200 ring-1 ring-inset placeholder:text-ash-900 focus:ring-2 focus:ring-inset focus:ring-primary-500 sm:text-sm sm:leading-6"
|
||||
@input="onInput"
|
||||
>
|
||||
<option value="" disabled selected class="hidden">
|
||||
{{ placeholder }}
|
||||
</option>
|
||||
<slot>
|
||||
<option v-for="opt in options" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</option>
|
||||
</slot>
|
||||
</select>
|
||||
</WithLabel>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.select-caret {
|
||||
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='32' height='24' viewBox='0 0 32 24'><polygon points='0,0 32,0 16,24' style='fill: rgb%28110, 111, 115%29'></polygon></svg>");
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: { type: Boolean, default: false },
|
||||
},
|
||||
methods: {
|
||||
onClick() {
|
||||
this.$emit('input', !this.value);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
@@ -20,16 +33,3 @@
|
||||
/>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: { type: Boolean, default: false },
|
||||
},
|
||||
methods: {
|
||||
onClick() {
|
||||
this.$emit('input', !this.value);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,31 +1,3 @@
|
||||
<template>
|
||||
<with-label
|
||||
:label="label"
|
||||
:name="name"
|
||||
:has-error="hasError"
|
||||
:error-message="errorMessage"
|
||||
>
|
||||
<textarea
|
||||
:id="name"
|
||||
:name="name"
|
||||
autocomplete="off"
|
||||
:required="required"
|
||||
:placeholder="placeholder"
|
||||
:data-testid="dataTestid"
|
||||
:value="value"
|
||||
:rows="rows"
|
||||
:class="{
|
||||
'focus:outline-red-600 outline-red-600': hasError,
|
||||
'dark:outline-slate-600 dark:focus:outline-woot-500 outline-slate-200 focus:outline-woot-500':
|
||||
!hasError,
|
||||
'resize-none': !allowResize,
|
||||
}"
|
||||
class="block w-full p-3 bg-white border-none rounded-md shadow-sm appearance-none outline outline-1 focus:outline focus:outline-2 text-slate-900 dark:text-slate-100 placeholder:text-slate-400 dark:bg-slate-800"
|
||||
@input="onInput"
|
||||
@blur="$emit('blur')"
|
||||
/>
|
||||
</with-label>
|
||||
</template>
|
||||
<script>
|
||||
import WithLabel from './WithLabel.vue';
|
||||
export default {
|
||||
@@ -81,3 +53,32 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WithLabel
|
||||
:label="label"
|
||||
:name="name"
|
||||
:has-error="hasError"
|
||||
:error-message="errorMessage"
|
||||
>
|
||||
<textarea
|
||||
:id="name"
|
||||
:name="name"
|
||||
autocomplete="off"
|
||||
:required="required"
|
||||
:placeholder="placeholder"
|
||||
:data-testid="dataTestid"
|
||||
:value="value"
|
||||
:rows="rows"
|
||||
:class="{
|
||||
'focus:outline-red-600 outline-red-600': hasError,
|
||||
'dark:outline-slate-600 dark:focus:outline-woot-500 outline-slate-200 focus:outline-woot-500':
|
||||
!hasError,
|
||||
'resize-none': !allowResize,
|
||||
}"
|
||||
class="block w-full p-3 bg-white border-none rounded-md shadow-sm appearance-none outline outline-1 focus:outline focus:outline-2 text-slate-900 dark:text-slate-100 placeholder:text-slate-400 dark:bg-slate-800"
|
||||
@input="onInput"
|
||||
@blur="$emit('blur')"
|
||||
/>
|
||||
</WithLabel>
|
||||
</template>
|
||||
|
||||
@@ -1,3 +1,30 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
hasError: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
errorMessage: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-1">
|
||||
<label
|
||||
@@ -30,29 +57,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
hasError: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
errorMessage: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,23 +1,3 @@
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<a
|
||||
:href="getGoogleAuthUrl()"
|
||||
class="inline-flex w-full justify-center rounded-md bg-white py-3 px-4 shadow-sm ring-1 ring-inset ring-slate-200 dark:ring-slate-600 hover:bg-slate-50 focus:outline-offset-0 dark:bg-slate-700 dark:hover:bg-slate-700"
|
||||
>
|
||||
<img src="/assets/images/auth/google.svg" alt="Google Logo" class="h-6" />
|
||||
<span class="text-base font-medium ml-2 text-slate-600 dark:text-white">
|
||||
{{ $t('LOGIN.OAUTH.GOOGLE_LOGIN') }}
|
||||
</span>
|
||||
</a>
|
||||
<simple-divider
|
||||
v-if="showSeparator"
|
||||
ref="divider"
|
||||
:label="$t('COMMON.OR')"
|
||||
class="uppercase"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SimpleDivider from '../Divider/SimpleDivider.vue';
|
||||
|
||||
@@ -58,3 +38,25 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable vue/no-unused-refs -->
|
||||
<!-- Added ref for writing specs -->
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<a
|
||||
:href="getGoogleAuthUrl()"
|
||||
class="inline-flex justify-center w-full px-4 py-3 bg-white rounded-md shadow-sm ring-1 ring-inset ring-slate-200 dark:ring-slate-600 hover:bg-slate-50 focus:outline-offset-0 dark:bg-slate-700 dark:hover:bg-slate-700"
|
||||
>
|
||||
<img src="/assets/images/auth/google.svg" alt="Google Logo" class="h-6" />
|
||||
<span class="ml-2 text-base font-medium text-slate-600 dark:text-white">
|
||||
{{ $t('LOGIN.OAUTH.GOOGLE_LOGIN') }}
|
||||
</span>
|
||||
</a>
|
||||
<SimpleDivider
|
||||
v-if="showSeparator"
|
||||
ref="divider"
|
||||
:label="$t('COMMON.OR')"
|
||||
class="uppercase"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,18 +1,3 @@
|
||||
<template>
|
||||
<transition-group
|
||||
name="toast-fade"
|
||||
tag="div"
|
||||
class="fixed left-0 right-0 mx-auto overflow-hidden text-center top-10 z-50 max-w-[40rem]"
|
||||
>
|
||||
<snackbar-item
|
||||
v-for="snackbarAlertMessage in snackbarAlertMessages"
|
||||
:key="snackbarAlertMessage.key"
|
||||
:message="snackbarAlertMessage.message"
|
||||
:action="snackbarAlertMessage.action"
|
||||
/>
|
||||
</transition-group>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import SnackbarItem from './Item.vue';
|
||||
@@ -52,3 +37,18 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<transition-group
|
||||
name="toast-fade"
|
||||
tag="div"
|
||||
class="fixed left-0 right-0 mx-auto overflow-hidden text-center top-10 z-50 max-w-[40rem]"
|
||||
>
|
||||
<SnackbarItem
|
||||
v-for="snackbarAlertMessage in snackbarAlertMessages"
|
||||
:key="snackbarAlertMessage.key"
|
||||
:message="snackbarAlertMessage.message"
|
||||
:action="snackbarAlertMessage.action"
|
||||
/>
|
||||
</transition-group>
|
||||
</template>
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
message: { type: String, default: '' },
|
||||
action: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isActionPresent() {
|
||||
return this.action && this.action.message;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="bg-slate-900 dark:bg-slate-800 rounded-md drop-shadow-md mb-4 max-w-[40rem] inline-flex items-center min-w-[22rem] py-3 px-4"
|
||||
@@ -13,25 +30,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
message: { type: String, default: '' },
|
||||
action: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
showButton: Boolean,
|
||||
duration: {
|
||||
type: [String, Number],
|
||||
default: 3000,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isActionPresent() {
|
||||
return this.action && this.action.message;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
<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';
|
||||
@@ -34,3 +28,10 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<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>
|
||||
|
||||
@@ -1,52 +1,3 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col justify-center w-full min-h-full py-12 bg-woot-25 sm:px-6 lg:px-8 dark:bg-slate-900"
|
||||
>
|
||||
<form
|
||||
class="bg-white shadow sm:mx-auto sm:w-full sm:max-w-lg dark:bg-slate-800 p-11 sm:shadow-lg sm:rounded-lg"
|
||||
@submit.prevent="submitForm"
|
||||
>
|
||||
<h1
|
||||
class="mb-1 text-2xl font-medium tracking-tight text-left text-slate-900 dark:text-white"
|
||||
>
|
||||
{{ $t('SET_NEW_PASSWORD.TITLE') }}
|
||||
</h1>
|
||||
|
||||
<div class="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 { useVuelidate } from '@vuelidate/core';
|
||||
import { required, minLength } from '@vuelidate/validators';
|
||||
@@ -63,8 +14,6 @@ export default {
|
||||
},
|
||||
props: {
|
||||
resetPasswordToken: { type: String, default: '' },
|
||||
redirectUrl: { type: String, default: '' },
|
||||
config: { type: String, default: '' },
|
||||
},
|
||||
setup() {
|
||||
return { v$: useVuelidate() };
|
||||
@@ -135,3 +84,52 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col justify-center w-full min-h-full py-12 bg-woot-25 sm:px-6 lg:px-8 dark:bg-slate-900"
|
||||
>
|
||||
<form
|
||||
class="bg-white shadow sm:mx-auto sm:w-full sm:max-w-lg dark:bg-slate-800 p-11 sm:shadow-lg sm:rounded-lg"
|
||||
@submit.prevent="submitForm"
|
||||
>
|
||||
<h1
|
||||
class="mb-1 text-2xl font-medium tracking-tight text-left text-slate-900 dark:text-white"
|
||||
>
|
||||
{{ $t('SET_NEW_PASSWORD.TITLE') }}
|
||||
</h1>
|
||||
|
||||
<div class="space-y-5">
|
||||
<FormInput
|
||||
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"
|
||||
/>
|
||||
<FormInput
|
||||
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"
|
||||
/>
|
||||
<SubmitButton
|
||||
: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>
|
||||
|
||||
@@ -1,51 +1,3 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col justify-center w-full min-h-full py-12 bg-woot-25 sm:px-6 lg:px-8 dark:bg-slate-900"
|
||||
>
|
||||
<form
|
||||
class="bg-white shadow sm:mx-auto sm:w-full sm:max-w-lg dark:bg-slate-800 p-11 sm:shadow-lg sm:rounded-lg"
|
||||
@submit.prevent="submit"
|
||||
>
|
||||
<h1
|
||||
class="mb-1 text-2xl font-medium tracking-tight text-left text-slate-900 dark:text-white"
|
||||
>
|
||||
{{ $t('RESET_PASSWORD.TITLE') }}
|
||||
</h1>
|
||||
<p
|
||||
class="mb-4 text-sm font-normal leading-6 tracking-normal text-slate-600 dark:text-woot-50"
|
||||
>
|
||||
{{
|
||||
useInstallationName(
|
||||
$t('RESET_PASSWORD.DESCRIPTION'),
|
||||
globalConfig.installationName
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<div class="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="mt-4 -mb-1 text-sm text-slate-600 dark:text-woot-50">
|
||||
{{ $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 { useVuelidate } from '@vuelidate/core';
|
||||
import { mapGetters } from 'vuex';
|
||||
@@ -111,3 +63,51 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col justify-center w-full min-h-full py-12 bg-woot-25 sm:px-6 lg:px-8 dark:bg-slate-900"
|
||||
>
|
||||
<form
|
||||
class="bg-white shadow sm:mx-auto sm:w-full sm:max-w-lg dark:bg-slate-800 p-11 sm:shadow-lg sm:rounded-lg"
|
||||
@submit.prevent="submit"
|
||||
>
|
||||
<h1
|
||||
class="mb-1 text-2xl font-medium tracking-tight text-left text-slate-900 dark:text-white"
|
||||
>
|
||||
{{ $t('RESET_PASSWORD.TITLE') }}
|
||||
</h1>
|
||||
<p
|
||||
class="mb-4 text-sm font-normal leading-6 tracking-normal text-slate-600 dark:text-woot-50"
|
||||
>
|
||||
{{
|
||||
useInstallationName(
|
||||
$t('RESET_PASSWORD.DESCRIPTION'),
|
||||
globalConfig.installationName
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<div class="space-y-5">
|
||||
<FormInput
|
||||
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="mt-4 -mb-1 text-sm text-slate-600 dark:text-woot-50">
|
||||
{{ $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>
|
||||
|
||||
@@ -1,57 +1,3 @@
|
||||
<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';
|
||||
@@ -85,3 +31,57 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-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="block w-auto h-8 dark:hidden"
|
||||
/>
|
||||
<img
|
||||
v-if="globalConfig.logoDark"
|
||||
:src="globalConfig.logoDark"
|
||||
:alt="globalConfig.installationName"
|
||||
class="hidden w-auto h-8 dark:block"
|
||||
/>
|
||||
<h2
|
||||
class="mt-6 text-3xl font-medium text-left mb-7 text-slate-900 dark:text-woot-50"
|
||||
>
|
||||
{{ $t('REGISTER.TRY_WOOT') }}
|
||||
</h2>
|
||||
</div>
|
||||
<SignupForm />
|
||||
<div class="px-1 text-sm text-slate-800 dark:text-woot-50">
|
||||
<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"
|
||||
@resizeContainers="resizeContainers"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-show="isLoading"
|
||||
class="flex items-center justify-center w-full h-full"
|
||||
>
|
||||
<Spinner color-scheme="primary" size="" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,83 +1,3 @@
|
||||
<template>
|
||||
<div class="flex-1 px-1 overflow-auto">
|
||||
<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 { useVuelidate } from '@vuelidate/core';
|
||||
import { required, minLength, email } from '@vuelidate/validators';
|
||||
@@ -210,6 +130,87 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-1 px-1 overflow-auto">
|
||||
<form class="space-y-3" @submit.prevent="submit">
|
||||
<div class="flex">
|
||||
<FormInput
|
||||
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"
|
||||
/>
|
||||
<FormInput
|
||||
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>
|
||||
<FormInput
|
||||
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"
|
||||
/>
|
||||
<FormInput
|
||||
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">
|
||||
<VueHcaptcha
|
||||
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>
|
||||
<SubmitButton
|
||||
: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>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.h-captcha--box {
|
||||
&::v-deep .error {
|
||||
|
||||
@@ -1,36 +1,3 @@
|
||||
<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';
|
||||
@@ -50,13 +17,46 @@ export default {
|
||||
} catch (error) {
|
||||
// Ignoring the error as the UI wouldn't break
|
||||
} finally {
|
||||
this.$emit('resize-containers', !!this.testimonials.length);
|
||||
this.$emit('resizeContainers', !!this.testimonials.length);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="testimonials.length"
|
||||
class="relative flex-1 hidden overflow-hidden bg-woot-400 dark:bg-woot-800 xl:flex"
|
||||
>
|
||||
<img
|
||||
src="/assets/images/auth/top-left.svg"
|
||||
class="absolute top-0 left-0 w-40 h-40"
|
||||
/>
|
||||
<img
|
||||
src="/assets/images/auth/bottom-right.svg"
|
||||
class="absolute bottom-0 right-0 w-40 h-40"
|
||||
/>
|
||||
<img
|
||||
src="/assets/images/auth/auth--bg.svg"
|
||||
class="h-[96%] left-[6%] top-[8%] w-[96%] absolute"
|
||||
/>
|
||||
<div class="z-50 flex flex-col items-center justify-center w-full h-full">
|
||||
<div class="flex items-start justify-center p-6">
|
||||
<TestimonialCard
|
||||
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>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.center--img {
|
||||
}
|
||||
|
||||
@@ -1,21 +1,3 @@
|
||||
<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: {
|
||||
@@ -38,3 +20,22 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<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>
|
||||
|
||||
@@ -1,95 +1,3 @@
|
||||
<template>
|
||||
<main
|
||||
class="flex flex-col w-full min-h-screen py-20 bg-woot-25 sm:px-6 lg:px-8 dark:bg-slate-900"
|
||||
>
|
||||
<section class="max-w-5xl mx-auto">
|
||||
<img
|
||||
:src="globalConfig.logo"
|
||||
:alt="globalConfig.installationName"
|
||||
class="block w-auto h-8 mx-auto dark:hidden"
|
||||
/>
|
||||
<img
|
||||
v-if="globalConfig.logoDark"
|
||||
:src="globalConfig.logoDark"
|
||||
:alt="globalConfig.installationName"
|
||||
class="hidden w-auto h-8 mx-auto dark:block"
|
||||
/>
|
||||
<h2
|
||||
class="mt-6 text-3xl font-medium text-center text-slate-900 dark:text-woot-50"
|
||||
>
|
||||
{{
|
||||
useInstallationName($t('LOGIN.TITLE'), globalConfig.installationName)
|
||||
}}
|
||||
</h2>
|
||||
<p
|
||||
v-if="showSignupLink"
|
||||
class="mt-3 text-sm text-center text-slate-600 dark:text-slate-400"
|
||||
>
|
||||
{{ $t('COMMON.OR') }}
|
||||
<router-link to="auth/signup" class="lowercase text-link">
|
||||
{{ $t('LOGIN.CREATE_NEW_ACCOUNT') }}
|
||||
</router-link>
|
||||
</p>
|
||||
</section>
|
||||
<section
|
||||
class="bg-white shadow sm:mx-auto mt-11 sm:w-full sm:max-w-lg dark:bg-slate-800 p-11 sm:shadow-lg sm:rounded-lg"
|
||||
:class="{
|
||||
'mb-8 mt-15': !showGoogleOAuth,
|
||||
'animate-wiggle': loginApi.hasErrored,
|
||||
}"
|
||||
>
|
||||
<div v-if="!email">
|
||||
<GoogleOAuthButton v-if="showGoogleOAuth" />
|
||||
<form class="space-y-5" @submit.prevent="submitFormLogin">
|
||||
<form-input
|
||||
v-model.trim="credentials.email"
|
||||
name="email_address"
|
||||
type="text"
|
||||
data-testid="email_input"
|
||||
:tabindex="1"
|
||||
required
|
||||
:label="$t('LOGIN.EMAIL.LABEL')"
|
||||
:placeholder="$t('LOGIN.EMAIL.PLACEHOLDER')"
|
||||
:has-error="v$.credentials.email.$error"
|
||||
@input="v$.credentials.email.$touch"
|
||||
/>
|
||||
<form-input
|
||||
v-model.trim="credentials.password"
|
||||
type="password"
|
||||
name="password"
|
||||
data-testid="password_input"
|
||||
required
|
||||
:tabindex="2"
|
||||
:label="$t('LOGIN.PASSWORD.LABEL')"
|
||||
:placeholder="$t('LOGIN.PASSWORD.PLACEHOLDER')"
|
||||
:has-error="v$.credentials.password.$error"
|
||||
@input="v$.credentials.password.$touch"
|
||||
>
|
||||
<p v-if="!globalConfig.disableUserProfileUpdate">
|
||||
<router-link
|
||||
to="auth/reset/password"
|
||||
class="text-sm text-link"
|
||||
tabindex="4"
|
||||
>
|
||||
{{ $t('LOGIN.FORGOT_PASSWORD') }}
|
||||
</router-link>
|
||||
</p>
|
||||
</form-input>
|
||||
<submit-button
|
||||
:disabled="loginApi.showLoading"
|
||||
:tabindex="3"
|
||||
:button-text="$t('LOGIN.SUBMIT')"
|
||||
:loading="loginApi.showLoading"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
<div v-else class="flex items-center justify-center">
|
||||
<spinner color-scheme="primary" size="" />
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required, email } from '@vuelidate/validators';
|
||||
@@ -119,7 +27,6 @@ export default {
|
||||
ssoAuthToken: { type: String, default: '' },
|
||||
ssoAccountId: { type: String, default: '' },
|
||||
ssoConversationId: { type: String, default: '' },
|
||||
config: { type: String, default: '' },
|
||||
email: { type: String, default: '' },
|
||||
authError: { type: String, default: '' },
|
||||
},
|
||||
@@ -237,3 +144,95 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main
|
||||
class="flex flex-col w-full min-h-screen py-20 bg-woot-25 sm:px-6 lg:px-8 dark:bg-slate-900"
|
||||
>
|
||||
<section class="max-w-5xl mx-auto">
|
||||
<img
|
||||
:src="globalConfig.logo"
|
||||
:alt="globalConfig.installationName"
|
||||
class="block w-auto h-8 mx-auto dark:hidden"
|
||||
/>
|
||||
<img
|
||||
v-if="globalConfig.logoDark"
|
||||
:src="globalConfig.logoDark"
|
||||
:alt="globalConfig.installationName"
|
||||
class="hidden w-auto h-8 mx-auto dark:block"
|
||||
/>
|
||||
<h2
|
||||
class="mt-6 text-3xl font-medium text-center text-slate-900 dark:text-woot-50"
|
||||
>
|
||||
{{
|
||||
useInstallationName($t('LOGIN.TITLE'), globalConfig.installationName)
|
||||
}}
|
||||
</h2>
|
||||
<p
|
||||
v-if="showSignupLink"
|
||||
class="mt-3 text-sm text-center text-slate-600 dark:text-slate-400"
|
||||
>
|
||||
{{ $t('COMMON.OR') }}
|
||||
<router-link to="auth/signup" class="lowercase text-link">
|
||||
{{ $t('LOGIN.CREATE_NEW_ACCOUNT') }}
|
||||
</router-link>
|
||||
</p>
|
||||
</section>
|
||||
<section
|
||||
class="bg-white shadow sm:mx-auto mt-11 sm:w-full sm:max-w-lg dark:bg-slate-800 p-11 sm:shadow-lg sm:rounded-lg"
|
||||
:class="{
|
||||
'mb-8 mt-15': !showGoogleOAuth,
|
||||
'animate-wiggle': loginApi.hasErrored,
|
||||
}"
|
||||
>
|
||||
<div v-if="!email">
|
||||
<GoogleOAuthButton v-if="showGoogleOAuth" />
|
||||
<form class="space-y-5" @submit.prevent="submitFormLogin">
|
||||
<FormInput
|
||||
v-model.trim="credentials.email"
|
||||
name="email_address"
|
||||
type="text"
|
||||
data-testid="email_input"
|
||||
:tabindex="1"
|
||||
required
|
||||
:label="$t('LOGIN.EMAIL.LABEL')"
|
||||
:placeholder="$t('LOGIN.EMAIL.PLACEHOLDER')"
|
||||
:has-error="v$.credentials.email.$error"
|
||||
@input="v$.credentials.email.$touch"
|
||||
/>
|
||||
<FormInput
|
||||
v-model.trim="credentials.password"
|
||||
type="password"
|
||||
name="password"
|
||||
data-testid="password_input"
|
||||
required
|
||||
:tabindex="2"
|
||||
:label="$t('LOGIN.PASSWORD.LABEL')"
|
||||
:placeholder="$t('LOGIN.PASSWORD.PLACEHOLDER')"
|
||||
:has-error="v$.credentials.password.$error"
|
||||
@input="v$.credentials.password.$touch"
|
||||
>
|
||||
<p v-if="!globalConfig.disableUserProfileUpdate">
|
||||
<router-link
|
||||
to="auth/reset/password"
|
||||
class="text-sm text-link"
|
||||
tabindex="4"
|
||||
>
|
||||
{{ $t('LOGIN.FORGOT_PASSWORD') }}
|
||||
</router-link>
|
||||
</p>
|
||||
</FormInput>
|
||||
<SubmitButton
|
||||
:disabled="loginApi.showLoading"
|
||||
:tabindex="3"
|
||||
:button-text="$t('LOGIN.SUBMIT')"
|
||||
:loading="loginApi.showLoading"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
<div v-else class="flex items-center justify-center">
|
||||
<Spinner color-scheme="primary" size="" />
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
@@ -1,3 +1,27 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'OnboardingStep',
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isComplete: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<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"
|
||||
@@ -24,29 +48,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'OnboardingStep',
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isComplete: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.hide-before-of-last:last-child::before {
|
||||
display: none;
|
||||
|
||||
Reference in New Issue
Block a user