feat: split basic profile component

This commit is contained in:
Muhsin Keloth
2024-04-23 13:26:04 +05:30
parent d2cb9d4b23
commit 99e5c389ca
5 changed files with 261 additions and 204 deletions
@@ -10,6 +10,7 @@
"PASSWORD_UPDATE_SUCCESS": "Your password has been changed successfully",
"AFTER_EMAIL_CHANGED": "Your profile has been updated successfully, please login again as your login credentials are changed",
"FORM": {
"PICTURE": "Profile Picture",
"AVATAR": "Profile Image",
"ERROR": "Please fix form errors",
"REMOVE_IMAGE": "Remove",
@@ -6,66 +6,22 @@
<div class="flex flex-col gap-16 sm:max-w-[720px]">
<div class="flex flex-col gap-6">
<h2 class="mt-4 text-2xl font-medium text-ash-900">
Profile Settings
{{ $t('PROFILE_SETTINGS.TITLE') }}
</h2>
<user-basic-profile
<user-profile-picture
:src="avatarUrl"
:name="name"
size="72px"
@change="handleImageUpload"
@update="updateUser"
@delete="deleteAvatar"
/>
<form
class="flex flex-col gap-6"
@submit.prevent="updateUser('profile')"
>
<form-input
v-model="name"
type="text"
name="name"
:class="{ error: $v.name.$error }"
:label="$t('PROFILE_SETTINGS.FORM.NAME.LABEL')"
:placeholder="$t('PROFILE_SETTINGS.FORM.NAME.PLACEHOLDER')"
:has-error="$v.name.$error"
:error-message="$t('PROFILE_SETTINGS.FORM.NAME.ERROR')"
@blur="$v.name.$touch"
/>
<form-input
v-model="displayName"
type="text"
name="displayName"
:class="{ error: $v.displayName.$error }"
:label="$t('PROFILE_SETTINGS.FORM.DISPLAY_NAME.LABEL')"
:placeholder="
$t('PROFILE_SETTINGS.FORM.DISPLAY_NAME.PLACEHOLDER')
"
:has-error="$v.displayName.$error"
:error-message="$t('PROFILE_SETTINGS.FORM.DISPLAY_NAME.ERROR')"
@blur="$v.displayName.$touch"
/>
<form-input
v-if="!globalConfig.disableUserProfileUpdate"
v-model="email"
type="email"
name="email"
:class="{ error: $v.email.$error }"
:label="$t('PROFILE_SETTINGS.FORM.EMAIL.LABEL')"
:placeholder="$t('PROFILE_SETTINGS.FORM.EMAIL.PLACEHOLDER')"
:has-error="$v.email.$error"
:error-message="$t('PROFILE_SETTINGS.FORM.EMAIL.ERROR')"
@blur="$v.email.$touch"
/>
<v3-button
type="submit"
color-scheme="primary"
variant="solid"
size="large"
>
{{ $t('PROFILE_SETTINGS.BTN_TEXT') }}
</v3-button>
</form>
<user-basic-details
:name="name"
:display-name="displayName"
:email="email"
:email-enabled="!globalConfig.disableUserProfileUpdate"
@update-user="updateUser"
/>
</div>
<personal-wrapper
@@ -142,7 +98,6 @@
</div>
</template>
<script>
import UserBasicProfile from './UserBasicProfile.vue';
import MessageSignature from './MessageSignature.vue';
import PersonalWrapper from './PersonalWrapper.vue';
import HotKeyCard from './HotKeyCard.vue';
@@ -153,27 +108,25 @@ import uiSettingsMixin, {
isEditorHotKeyEnabled,
} from 'dashboard/mixins/uiSettings';
import alertMixin from 'shared/mixins/alertMixin';
import { required, minLength, email } from 'vuelidate/lib/validators';
import { mapGetters } from 'vuex';
import { clearCookiesOnLogout } from '../../../../store/utils/api';
import { hasValidAvatarUrl } from 'dashboard/helper/URLHelper';
import NotificationSettings from './NotificationSettings.vue';
import NotificationPreferences from './NotificationPreferences.vue';
import FormInput from 'v3/components/Form/Input.vue';
import V3Button from 'v3/components/Form/Button.vue';
import UserProfilePicture from './UserProfilePicture.vue';
import UserBasicDetails from './UserBasicDetails.vue';
export default {
components: {
UserBasicProfile,
MessageSignature,
PersonalWrapper,
HotKeyCard,
ChangePassword,
NotificationSettings,
AccessToken,
FormInput,
V3Button,
NotificationPreferences,
UserProfilePicture,
UserBasicDetails,
},
mixins: [alertMixin, globalConfigMixin, uiSettingsMixin],
data() {
@@ -203,17 +156,6 @@ export default {
],
};
},
validations: {
name: {
required,
minLength: minLength(1),
},
displayName: {},
email: {
required,
email,
},
},
computed: {
...mapGetters({
currentUser: 'getCurrentUser',
@@ -245,12 +187,6 @@ export default {
},
isEditorHotKeyEnabled,
async updateUser() {
this.$v.$touch();
if (this.$v.$invalid) {
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.ERROR'));
return;
}
this.isProfileUpdating = true;
const hasEmailChanged = this.currentUser.email !== this.email;
try {
@@ -0,0 +1,129 @@
<template>
<form class="flex flex-col gap-6" @submit.prevent="updateUser('profile')">
<form-input
v-model="userName"
type="text"
name="name"
:class="{ error: $v.userName.$error }"
:label="$t('PROFILE_SETTINGS.FORM.NAME.LABEL')"
:placeholder="$t('PROFILE_SETTINGS.FORM.NAME.PLACEHOLDER')"
:has-error="$v.userName.$error"
:error-message="$t('PROFILE_SETTINGS.FORM.NAME.ERROR')"
@blur="$v.userName.$touch"
/>
<form-input
v-model="userDisplayName"
type="text"
name="displayName"
:class="{ error: $v.userDisplayName.$error }"
:label="$t('PROFILE_SETTINGS.FORM.DISPLAY_NAME.LABEL')"
:placeholder="$t('PROFILE_SETTINGS.FORM.DISPLAY_NAME.PLACEHOLDER')"
:has-error="$v.userDisplayName.$error"
:error-message="$t('PROFILE_SETTINGS.FORM.DISPLAY_NAME.ERROR')"
@blur="$v.userDisplayName.$touch"
/>
<form-input
v-if="emailEnabled"
v-model="userEmail"
type="email"
name="email"
:class="{ error: $v.userEmail.$error }"
:label="$t('PROFILE_SETTINGS.FORM.EMAIL.LABEL')"
:placeholder="$t('PROFILE_SETTINGS.FORM.EMAIL.PLACEHOLDER')"
:has-error="$v.userEmail.$error"
:error-message="$t('PROFILE_SETTINGS.FORM.EMAIL.ERROR')"
@blur="$v.userEmail.$touch"
/>
<v3-button
type="submit"
color-scheme="primary"
variant="solid"
size="large"
>
{{ $t('PROFILE_SETTINGS.BTN_TEXT') }}
</v3-button>
</form>
</template>
<script>
import V3Button from 'v3/components/Form/Button.vue';
import FormInput from 'v3/components/Form/Input.vue';
import { required, minLength, email } from 'vuelidate/lib/validators';
import alertMixin from 'shared/mixins/alertMixin';
export default {
components: {
V3Button,
FormInput,
},
mixins: [alertMixin],
props: {
name: {
type: String,
default: '',
},
email: {
type: String,
default: '',
},
displayName: {
type: String,
default: '',
},
emailEnabled: {
type: Boolean,
default: false,
},
},
data() {
return {
userName: this.name,
userDisplayName: this.displayName,
userEmail: this.email,
};
},
validations: {
userName: {
required,
minLength: minLength(1),
},
userDisplayName: {},
userEmail: {
required,
email,
},
},
watch: {
name: {
handler(value) {
this.userName = value;
},
immediate: true,
},
displayName: {
handler(value) {
this.userDisplayName = value;
},
immediate: true,
},
email: {
handler(value) {
this.userEmail = value;
},
immediate: true,
},
},
methods: {
async updateUser() {
this.$v.$touch();
if (this.$v.$invalid) {
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.ERROR'));
return;
}
this.$emit('update-user', {
name: this.userName,
displayName: this.userDisplayName,
email: this.userEmail,
});
},
},
};
</script>
@@ -1,127 +0,0 @@
<template>
<div>
<div class="flex flex-col gap-2">
<span class="text-sm font-medium text-ash-900"> Profile picture </span>
<div
class="relative rounded-xl h-[72px] w-[72px] cursor-pointer"
:title="name"
@mouseover="showUpload"
@mouseleave="hideUpload"
>
<img
v-if="shouldShowImage"
class="rounded-xl h-[72px] w-[72px]"
:src="src"
draggable="false"
@load="onImgLoad"
@error="onImgError"
/>
<Avatar
v-show="!shouldShowImage"
:username="userNameWithoutEmoji"
:size="72"
/>
<input
ref="file"
type="file"
accept="image/png, image/jpeg, image/jpg, image/gif, image/webp"
style="display: none"
@change="handleImageUpload"
/>
<button
v-if="src && showUploadIcon"
class="absolute z-10 flex items-center justify-center w-6 h-6 p-1 border border-white rounded-full select-none reset-base -top-2 -right-2 dark:border-white bg-slate-200 dark:bg-slate-700"
@click="onAvatarDelete"
>
<fluent-icon
icon="dismiss"
size="16"
class="text-slate-800 dark:text-slate-200"
/>
</button>
<button
v-if="showUploadIcon"
class="reset-base absolute h-[72px] w-[72px] top-0 left-0 rounded-xl select-none flex items-center justify-center bg-modal-backdrop-dark dark:bg-modal-backdrop-dark"
@click="openFileInput"
>
<fluent-icon icon="avatar-upload" size="32" class="text-white" />
</button>
</div>
</div>
</div>
</template>
<script>
import Avatar from 'v3/components/Form/Avatar.vue';
import { removeEmoji } from 'shared/helpers/emoji';
export default {
components: {
Avatar,
},
props: {
src: {
type: String,
default: '',
},
name: {
type: String,
default: '',
},
},
data() {
return {
hasImageLoaded: false,
imgError: false,
showUploadIcon: false,
};
},
computed: {
userNameWithoutEmoji() {
return removeEmoji(this.name);
},
shouldShowImage() {
if (!this.src) {
return false;
}
if (this.hasImageLoaded) {
return !this.imgError;
}
return true;
},
},
watch: {
src(value, oldValue) {
if (value !== oldValue && this.imgError) {
this.imgError = false;
}
},
},
methods: {
onImgError() {
this.imgError = true;
},
onImgLoad() {
this.hasImageLoaded = true;
},
showUpload() {
this.showUploadIcon = true;
},
hideUpload() {
this.showUploadIcon = false;
},
openFileInput() {
// Trigger click event on the hidden file input
this.$refs.file.click();
},
handleImageUpload(event) {
const [file] = event.target.files;
this.$emit('change', {
file,
url: file ? URL.createObjectURL(file) : null,
});
},
onAvatarDelete() {
this.$emit('delete');
},
},
};
</script>
@@ -0,0 +1,118 @@
<template>
<div>
<div class="flex flex-col gap-2">
<span class="text-sm font-medium text-ash-900">
{{ $t('PROFILE_SETTINGS.FORM.PICTURE') }}
</span>
<div
class="relative rounded-xl h-[72px] w-[72px] cursor-pointer"
@mouseover="onImageOver"
@mouseleave="onImageOut"
>
<img
v-if="shouldShowImage"
class="rounded-xl h-[72px] w-[72px]"
:src="src"
draggable="false"
@load="onImageLoad"
@error="onImageLoadError"
/>
<Avatar
v-show="!shouldShowImage"
:username="userNameWithoutEmoji"
:size="72"
/>
<input
ref="fileInputRef"
type="file"
accept="image/png, image/jpeg, image/jpg, image/gif, image/webp"
style="display: none"
@change="onImageUpload"
/>
<button
v-if="src && shouldShowUploadIcon"
class="absolute z-10 flex items-center justify-center w-6 h-6 p-1 border border-white rounded-full select-none reset-base -top-2 -right-2 dark:border-white bg-slate-200 dark:bg-slate-700"
@click="onAvatarDelete"
>
<fluent-icon
icon="dismiss"
size="16"
class="text-slate-800 dark:text-slate-200"
/>
</button>
<button
v-if="shouldShowUploadIcon"
class="reset-base absolute h-[72px] w-[72px] top-0 left-0 rounded-xl select-none flex items-center justify-center bg-modal-backdrop-dark dark:bg-modal-backdrop-dark"
@click="openFileInput"
>
<fluent-icon icon="avatar-upload" size="32" class="text-white" />
</button>
</div>
</div>
</div>
</template>
<script setup>
import { computed, ref } from 'vue';
import Avatar from 'v3/components/Form/Avatar.vue';
import { removeEmoji } from 'shared/helpers/emoji';
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 shouldShowUploadIcon = ref(false);
const fileInputRef = ref(null);
const userNameWithoutEmoji = computed(() => removeEmoji(props.name));
const shouldShowImage = computed(() => {
if (!props.src) {
return false;
}
if (hasImageLoaded.value) {
return !imageLoadedError.value;
}
return true;
});
const onImageLoadError = () => {
imageLoadedError.value = true;
};
const onImageLoad = () => {
hasImageLoaded.value = true;
};
const onImageOver = () => {
shouldShowUploadIcon.value = true;
};
const onImageOut = () => {
shouldShowUploadIcon.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>