chore: Replace thumbnail component with avatar

This commit is contained in:
iamsivin
2024-11-05 23:56:11 +05:30
parent 3c58d6af59
commit ae1c6e3f98
9 changed files with 185 additions and 214 deletions
@@ -1,7 +1,8 @@
<script setup>
import Thumbnail from 'dashboard/components-next/thumbnail/Thumbnail.vue';
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
import Icon from 'dashboard/components-next/icon/Icon.vue';
const props = defineProps({
@@ -33,10 +34,11 @@ const senderThumbnailSrc = computed(() => props.sender?.thumbnail);
{{ t('CAMPAIGN.LIVE_CHAT.CARD.CAMPAIGN_DETAILS.SENT_BY') }}
</span>
<div class="flex items-center gap-1.5 flex-shrink-0">
<Thumbnail
:author="sender || { name: senderName }"
<Avatar
:name="senderName"
:src="senderThumbnailSrc"
:size="16"
rounded-full
/>
<span class="text-sm font-medium text-n-slate-12">
{{ senderName }}
@@ -8,7 +8,7 @@ import { debounce } from '@chatwoot/utils';
import CardLayout from 'dashboard/components-next/CardLayout.vue';
import ContactsForm from 'dashboard/components-next/Contacts/ContactsForm/ContactsForm.vue';
import Button from 'dashboard/components-next/button/Button.vue';
import Thumbnail from 'dashboard/components-next/thumbnail/Thumbnail.vue';
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
const props = defineProps({
id: { type: Number, required: true },
@@ -27,11 +27,6 @@ const router = useRouter();
const { t } = useI18n();
const store = useStore();
const author = computed(() => ({
name: props.name,
thumbnail: props.thumbnail,
}));
const updateContact = async updatedData => {
await store.dispatch('contacts/update', updatedData);
await store.dispatch('contacts/fetchContactableInbox', props.id);
@@ -58,7 +53,7 @@ const onClickViewDetails = () => {
<CardLayout :key="id" layout="row">
<template #header>
<div class="flex items-center justify-between gap-4">
<Thumbnail :author="author" :name="name" :src="thumbnail" :size="48" />
<Avatar :name="name" :src="thumbnail" :size="48" rounded-full />
<div class="flex flex-col gap-1">
<div class="flex items-center gap-2">
<span class="text-sm font-medium truncate text-n-slate-12">{{
@@ -7,7 +7,7 @@ import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
import { dynamicTime } from 'shared/helpers/timeHelper';
import Editor from 'dashboard/components-next/Editor/Editor.vue';
import Thumbnail from 'dashboard/components-next/thumbnail/Thumbnail.vue';
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
import Button from 'dashboard/components-next/Button/Button.vue';
@@ -84,23 +84,24 @@ const onDelete = noteId => {
class="flex flex-col gap-2 px-6 py-2 border-b border-n-strong group/note"
>
<div class="flex items-center justify-between">
<div class="flex items-center gap-1.5 py-2.5">
<Thumbnail
:author="note.user || { name: note.user.name }"
<div class="flex items-center gap-1.5 py-2.5 min-w-0">
<Avatar
:name="note.user.name"
:src="note.user.thumbnail"
:size="16"
rounded-full
/>
<span
class="inline-flex items-center gap-1 text-sm text-n-slate-11"
>
<span class="text-sm font-medium text-n-slate-11">
{{ getWrittenBy(note) }}
<div class="min-w-0 truncate">
<span
class="inline-flex items-center gap-1 text-sm text-n-slate-11"
>
<span class="font-medium">{{ getWrittenBy(note) }}</span>
{{ $t('CONTACTS_LAYOUT.SIDEBAR.NOTES.WROTE') }}
<span class="font-medium">{{
dynamicTime(note.createdAt)
}}</span>
</span>
{{ $t('CONTACTS_LAYOUT.SIDEBAR.NOTES.WROTE') }}
<span class="text-sm font-medium text-n-slate-11">
{{ dynamicTime(note.createdAt) }}
</span>
</span>
</div>
</div>
<Button
variant="faded"
@@ -13,7 +13,7 @@ import Icon from 'dashboard/components-next/icon/Icon.vue';
import CardLayout from 'dashboard/components-next/CardLayout.vue';
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
import Button from 'dashboard/components-next/button/Button.vue';
import Thumbnail from 'dashboard/components-next/thumbnail/Thumbnail.vue';
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
const props = defineProps({
id: {
@@ -101,7 +101,7 @@ const categoryName = computed(() => {
});
const authorName = computed(() => {
return props.author?.name || props.author?.availableName || '-';
return props.author?.name || props.author?.availableName || '';
});
const authorThumbnailSrc = computed(() => {
@@ -164,13 +164,14 @@ const handleClick = id => {
<div class="flex items-center justify-between w-full gap-4">
<div class="flex items-center gap-4">
<div class="flex items-center gap-1">
<Thumbnail
:author="author"
<Avatar
:name="authorName"
:src="authorThumbnailSrc"
:size="16"
rounded-full
/>
<span class="text-sm truncate text-n-slate-11">
{{ authorName }}
{{ authorName || '-' }}
</span>
</div>
<span class="block text-sm whitespace-nowrap text-n-slate-11">
@@ -6,7 +6,7 @@ import { OnClickOutside } from '@vueuse/components';
import { useMapGetter } from 'dashboard/composables/store';
import Button from 'dashboard/components-next/button/Button.vue';
import Thumbnail from 'dashboard/components-next/thumbnail/Thumbnail.vue';
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
import ArticleEditorProperties from 'dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditorProperties.vue';
@@ -50,7 +50,7 @@ const author = computed(() => {
});
const authorName = computed(
() => author.value?.name || author.value?.available_name || '-'
() => author.value?.name || author.value?.available_name || ''
);
const authorThumbnailSrc = computed(() => author.value?.thumbnail);
@@ -186,17 +186,14 @@ onMounted(() => {
text-variant="info"
@click="openAgentsList = !openAgentsList"
>
<Thumbnail
:author="author"
<Avatar
:name="authorName"
:size="20"
:src="authorThumbnailSrc"
:size="20"
rounded-full
/>
<span
v-if="author"
class="text-sm text-n-slate-12 hover:text-n-slate-11"
>
{{ author.available_name }}
<span class="text-sm text-n-slate-12 hover:text-n-slate-11">
{{ authorName || '-' }}
</span>
</Button>
<DropdownMenu
@@ -6,7 +6,7 @@ import { useMapGetter, useStore } from 'dashboard/composables/store.js';
import { buildPortalURL } from 'dashboard/helper/portalHelper';
import Button from 'dashboard/components-next/button/Button.vue';
import Thumbnail from 'dashboard/components-next/thumbnail/Thumbnail.vue';
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
const emit = defineEmits(['close', 'createPortal']);
@@ -148,14 +148,13 @@ const redirectToPortalHomePage = () => {
<span class="text-sm font-medium truncate text-n-slate-12">
{{ portal.name || '' }}
</span>
<Thumbnail
<Avatar
v-if="portal"
:author="portal"
:name="portal.name"
:size="20"
:src="getPortalThumbnailSrc(portal)"
:show-author-name="false"
:size="20"
icon-name="i-lucide-building-2"
rounded-full
/>
</Button>
</div>
@@ -1,6 +1,9 @@
<script setup>
import Icon from 'dashboard/components-next/icon/Icon.vue';
import { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { removeEmoji } from 'shared/helpers/emoji';
import Icon from 'dashboard/components-next/icon/Icon.vue';
import wootConstants from 'dashboard/constants/globals';
const props = defineProps({
@@ -27,34 +30,109 @@ const props = defineProps({
status: {
type: String,
default: null,
validator: value => {
if (!value) return true;
return wootConstants.AVAILABILITY_STATUS_KEYS.includes(value);
},
validator: value =>
!value || wootConstants.AVAILABILITY_STATUS_KEYS.includes(value),
},
iconName: {
type: String,
default: null,
},
});
const emit = defineEmits(['upload']);
const { t } = useI18n();
const isImageValid = ref(true);
function invalidateCurrentImage() {
isImageValid.value = false;
}
const AVATAR_COLORS = {
dark: [
['#4B143D', '#FF8DCC'],
['#3F220D', '#FFA366'],
['#2A2A2A', '#ADB1B8'],
['#023B37', '#0BD8B6'],
['#27264D', '#A19EFF'],
['#1D2E62', '#9EB1FF'],
],
light: [
['#FBDCEF', '#C2298A'],
['#FFE0BB', '#99543A'],
['#E8E8E8', '#60646C'],
['#CCF3EA', '#008573'],
['#EBEBFE', '#4747C2'],
['#E1E9FF', '#3A5BC7'],
],
default: { bg: '#E8E8E8', text: '#60646C' },
};
const STATUS_CLASSES = {
online: 'bg-n-teal-10',
busy: 'bg-n-amber-10',
offline: 'bg-n-slate-10',
};
const showDefaultAvatar = computed(() => !props.src && !props.name);
const initials = computed(() => {
const splitNames = props.name.split(' ');
if (splitNames.length > 1) {
const firstName = splitNames[0];
const lastName = splitNames[splitNames.length - 1];
return firstName[0] + lastName[0];
}
const firstName = splitNames[0];
return firstName[0];
if (!props.name) return '';
const words = removeEmoji(props.name).split(/\s+/);
return words.length === 1
? words[0].charAt(0).toUpperCase()
: words
.slice(0, 2)
.map(word => word.charAt(0))
.join('')
.toUpperCase();
});
const getColorsByNameLength = computed(() => {
if (!props.name) return AVATAR_COLORS.default;
const index = props.name.length % AVATAR_COLORS.light.length;
return {
bg: AVATAR_COLORS.light[index][0],
darkBg: AVATAR_COLORS.dark[index][0],
text: AVATAR_COLORS.light[index][1],
darkText: AVATAR_COLORS.dark[index][1],
};
});
const containerStyles = computed(() => ({
width: `${props.size}px`,
height: `${props.size}px`,
}));
const avatarStyles = computed(() => ({
...containerStyles.value,
backgroundColor:
!showDefaultAvatar.value && (!props.src || !isImageValid.value)
? getColorsByNameLength.value.bg
: undefined,
color:
!showDefaultAvatar.value && (!props.src || !isImageValid.value)
? getColorsByNameLength.value.text
: undefined,
'--dark-bg': getColorsByNameLength.value.darkBg,
'--dark-text': getColorsByNameLength.value.darkText,
}));
const badgeStyles = computed(() => ({
top: `${props.size - 10}px`,
left: `${props.size - 10}px`,
}));
const iconStyles = computed(() => ({
fontSize: `${props.size / 1.6}px`,
}));
const initialsStyles = computed(() => ({
fontSize: `${props.size / 1.8}px`,
}));
const invalidateCurrentImage = () => {
isImageValid.value = false;
};
watch(
() => props.src,
() => {
@@ -64,48 +142,63 @@ watch(
</script>
<template>
<span
class="relative inline"
:style="{
width: `${size}px`,
height: `${size}px`,
}"
>
<slot name="badge" :size>
<span class="relative inline-flex" :style="containerStyles">
<!-- Status Badge -->
<slot name="badge" :size="size">
<div
v-if="status"
class="rounded-full w-2.5 h-2.5 absolute z-20"
:style="{
top: `${size - 10}px`,
left: `${size - 10}px`,
}"
:class="{
'bg-n-teal-10': status === 'online',
'bg-n-amber-10': status === 'busy',
'bg-n-slate-10': status === 'offline',
}"
:style="badgeStyles"
:class="STATUS_CLASSES[status]"
/>
</slot>
<!-- Avatar Container -->
<span
role="img"
class="inline-flex relative items-center justify-center object-cover overflow-hidden font-medium bg-woot-50 text-woot-500 group/avatar"
:class="{
'rounded-full': roundedFull,
'rounded-xl': !roundedFull,
}"
:style="{
width: `${size}px`,
height: `${size}px`,
}"
class="relative inline-flex items-center justify-center object-cover overflow-hidden font-medium"
:class="[
roundedFull ? 'rounded-full' : 'rounded-xl',
{
'dark:!bg-[var(--dark-bg)] dark:!text-[var(--dark-text)]':
!showDefaultAvatar && (!src || !isImageValid),
'bg-n-slate-3 dark:bg-n-slate-4': showDefaultAvatar,
},
]"
:style="avatarStyles"
>
<img
<!-- Avatar Content -->
<component
:is="src && isImageValid ? 'img' : 'div'"
v-if="src && isImageValid"
:src="src"
:alt="name"
@error="invalidateCurrentImage"
/>
<span v-else>
{{ initials }}
</span>
<template v-else>
<!-- Custom Icon -->
<Icon v-if="iconName" :icon="iconName" :style="iconStyles" />
<!-- Initials -->
<span
v-else-if="!showDefaultAvatar"
:style="initialsStyles"
class="select-none"
>
{{ initials }}
</span>
<!-- Fallback Icon if no name or image -->
<Icon
v-else
v-tooltip.top-start="t('THUMBNAIL.AUTHOR.NOT_AVAILABLE')"
icon="i-lucide-user"
:style="iconStyles"
/>
</template>
<!-- Upload Overlay -->
<div
v-if="allowUpload"
role="button"
@@ -2,7 +2,7 @@
import { defineProps, defineEmits } from 'vue';
import Icon from 'dashboard/components-next/icon/Icon.vue';
import Thumbnail from 'dashboard/components-next/thumbnail/Thumbnail.vue';
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
defineProps({
menuItems: {
@@ -41,12 +41,12 @@ const handleAction = (action, value) => {
:disabled="item.disabled"
@click="handleAction(item.action, item.value)"
>
<Thumbnail
<Avatar
v-if="item.thumbnail"
:author="item.thumbnail"
:name="item.thumbnail.name"
:size="thumbnailSize"
:src="item.thumbnail.src"
:size="thumbnailSize"
rounded-full
/>
<Icon v-if="item.icon" :icon="item.icon" class="flex-shrink-0" />
<span v-if="item.emoji" class="flex-shrink-0">{{ item.emoji }}</span>
@@ -1,117 +0,0 @@
<script setup>
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { removeEmoji } from 'shared/helpers/emoji';
const props = defineProps({
author: {
type: Object,
default: null,
},
name: {
type: String,
default: '',
},
src: {
type: String,
default: '',
},
size: {
type: Number,
default: 16,
},
showAuthorName: {
type: Boolean,
default: true,
},
iconName: {
type: String,
default: '',
},
});
const { t } = useI18n();
const hasImageLoaded = ref(false);
const imgError = ref(false);
const authorInitial = computed(() => {
if (!props.name) return '';
const name = removeEmoji(props.name);
const words = name.split(/\s+/);
if (words.length === 1) {
return name.substring(0, 2).toUpperCase();
}
return words
.slice(0, 2)
.map(word => word[0])
.join('')
.toUpperCase();
});
const fontSize = computed(() => {
return props.size / 2;
});
const iconSize = computed(() => {
return Math.round(props.size / 1.8);
});
const shouldShowImage = computed(() => {
return props.src && !imgError.value;
});
const onImgError = () => {
imgError.value = true;
};
const onImgLoad = () => {
hasImageLoaded.value = true;
};
</script>
<template>
<div
class="flex items-center justify-center rounded-full bg-n-slate-3 dark:bg-n-slate-4"
:style="{ width: `${size}px`, height: `${size}px` }"
>
<div v-if="author" class="flex items-center justify-center">
<img
v-if="shouldShowImage"
:src="src"
:alt="name"
class="w-full h-full rounded-full"
@load="onImgLoad"
@error="onImgError"
/>
<template v-else>
<span
v-if="showAuthorName"
class="flex items-center justify-center font-medium text-n-slate-11"
:style="{ fontSize: `${fontSize}px` }"
>
{{ authorInitial }}
</span>
<div
v-else
class="flex items-center justify-center w-full h-full rounded-xl"
>
<span
v-if="iconName"
:class="`${iconName} text-n-brand/70`"
:style="{ width: `${iconSize}px`, height: `${iconSize}px` }"
/>
</div>
</template>
</div>
<div
v-else
v-tooltip.top-start="t('THUMBNAIL.AUTHOR.NOT_AVAILABLE')"
class="flex items-center justify-center w-4 h-4 rounded-full bg-n-slate-3 dark:bg-n-slate-4"
>
<span class="i-lucide-user size-2.5 text-n-brand" />
</div>
</div>
</template>