feat: Contact Empty state

This commit is contained in:
iamsivin
2024-11-11 22:38:08 +05:30
parent ae804baf9b
commit fd4b1f2d21
7 changed files with 349 additions and 8 deletions
@@ -16,6 +16,10 @@ defineProps({
type: String,
default: '',
},
isEmptyState: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['filter', 'update:sort']);
@@ -24,7 +28,7 @@ const emit = defineEmits(['filter', 'update:sort']);
<template>
<div class="flex items-center gap-2">
<Button
v-if="!isDetailView"
v-if="!isDetailView && !isEmptyState"
icon="i-lucide-list-filter"
color="slate"
size="sm"
@@ -32,7 +36,7 @@ const emit = defineEmits(['filter', 'update:sort']);
@click="emit('filter')"
/>
<ContactSortMenu
v-if="!isDetailView"
v-if="!isDetailView && !isEmptyState"
:active-sort="activeSort"
:active-ordering="activeOrdering"
@update:sort="emit('update:sort', $event)"
@@ -10,6 +10,10 @@ import Breadcrumb from 'dashboard/components-next/breadcrumb/Breadcrumb.vue';
import ContactActions from 'dashboard/components-next/Contacts/ContactHeader/ContactActions.vue';
const props = defineProps({
searchValue: {
type: String,
default: '',
},
headerTitle: {
type: String,
default: '',
@@ -50,6 +54,10 @@ const props = defineProps({
type: String,
default: '',
},
isEmptyState: {
type: Boolean,
default: false,
},
});
const emit = defineEmits([
@@ -123,8 +131,12 @@ const updateCurrentPage = page => {
@click="handleBreadcrumbClick"
/>
<div class="flex items-center gap-4">
<div v-if="!isDetailView" class="flex items-center gap-2">
<div
v-if="!isDetailView && !isEmptyState"
class="flex items-center gap-2"
>
<Input
:model-value="searchValue"
:placeholder="$t('CONTACTS_LAYOUT.HEADER.SEARCH_PLACEHOLDER')"
:custom-input-class="[
'h-8 [&:not(.focus)]:!border-transparent bg-n-solid-1 ltr:!pl-8 !py-1 rtl:!pr-8',
@@ -140,6 +152,7 @@ const updateCurrentPage = page => {
</Input>
</div>
<ContactActions
:is-empty-state="isEmptyState"
:is-detail-view="isDetailView"
:active-sort="activeSort"
:active-ordering="activeOrdering"
@@ -0,0 +1,61 @@
<script setup>
import { ref } from 'vue';
import EmptyStateLayout from 'dashboard/components-next/EmptyStateLayout.vue';
import Button from 'dashboard/components-next/button/Button.vue';
import ContactsCard from 'dashboard/components-next/Contacts/ContactsCard/ContactsCard.vue';
import contactContent from 'dashboard/components-next/Contacts/EmptyState/contactEmptyStateContent';
import CreateNewContactDialog from 'dashboard/components-next/Contacts/ContactsForm/CreateNewContactDialog.vue';
defineProps({
title: {
type: String,
default: '',
},
subtitle: {
type: String,
default: '',
},
showButton: {
type: Boolean,
default: true,
},
buttonLabel: {
type: String,
default: '',
},
});
const createNewContactDialogRef = ref(null);
const onClick = () => {
createNewContactDialogRef.value?.dialogRef.open();
};
</script>
<template>
<EmptyStateLayout :title="title" :subtitle="subtitle">
<template #empty-state-item>
<div class="grid grid-cols-1 gap-4 p-px overflow-hidden">
<ContactsCard
v-for="contact in contactContent.slice(0, 5)"
:id="contact.id"
:key="contact.id"
:name="contact.name"
:email="contact.email"
:thumbnail="contact.thumbnail"
:phone-number="contact.phoneNumber"
:additional-attributes="contact.additionalAttributes"
:is-expanded="0 === contact.id"
@toggle="toggleExpanded(contact.id)"
/>
</div>
</template>
<template #actions>
<div v-if="showButton">
<Button :label="buttonLabel" icon="i-lucide-plus" @click="onClick" />
</div>
<CreateNewContactDialog ref="createNewContactDialogRef" />
</template>
</EmptyStateLayout>
</template>
@@ -0,0 +1,228 @@
export default [
{
additionalAttributes: {
city: 'Los Angeles',
country: 'United States',
description:
"I'm Candice, a developer focusing on building web solutions. Currently, Im working as a Product Developer at Chatwoot.",
companyName: 'Chatwoot',
countryCode: 'US',
socialProfiles: {
github: 'candice-dev',
twitter: 'candice_w_dev',
facebook: 'candice.dev',
linkedin: 'candice-matherson',
instagram: 'candice.codes',
},
},
availabilityStatus: 'offline',
email: 'candice.matherson@chatwoot.com',
id: 22,
name: 'Candice Matherson',
phoneNumber: '+14155552671',
identifier: null,
thumbnail: '',
customAttributes: {
dateContact: '2024-11-11T11:53:09.299Z',
linkContact: 'https://example.com',
listContact: 'Follow-Up',
textContact: 'Hi there!',
numberContact: '42',
checkboxContact: false,
},
lastActivityAt: 1712123233,
createdAt: 1712123233,
},
{
additionalAttributes: {
city: 'San Francisco',
country: 'United States',
description: 'Passionate about design and user experience.',
companyName: 'Designify',
countryCode: 'US',
socialProfiles: {
github: 'ophelia-folkard',
twitter: 'oph_designs',
facebook: 'ophelia.folkard',
linkedin: 'ophelia-folkard',
instagram: 'ophelia.design',
},
},
availabilityStatus: 'offline',
email: 'ophelia.folkard@designify.com',
id: 21,
name: 'Ophelia Folkard',
phoneNumber: '+14155552672',
identifier: null,
thumbnail: '',
customAttributes: {
dateContact: '2024-10-05T10:12:34.567Z',
linkContact: 'https://designify.com',
listContact: 'Prospects',
textContact: 'Looking forward to connecting!',
},
lastActivityAt: 1712123233,
createdAt: 1712123233,
},
{
additionalAttributes: {
city: 'Austin',
country: 'United States',
description: 'Avid coder and tech enthusiast.',
companyName: 'CodeHub',
countryCode: 'US',
socialProfiles: {
github: 'willy_castelot',
twitter: 'willy_code',
facebook: 'willy.castelot',
linkedin: 'willy-castelot',
instagram: 'willy.coder',
},
},
availabilityStatus: 'offline',
email: 'willy.castelot@codehub.io',
id: 20,
name: 'Willy Castelot',
phoneNumber: '+14155552673',
identifier: null,
thumbnail: '',
customAttributes: {
textContact: 'Lets collaborate!',
checkboxContact: true,
},
lastActivityAt: 1712123233,
createdAt: 1712123233,
},
{
additionalAttributes: {
city: 'Seattle',
country: 'United States',
description: 'Product manager with a love for innovation.',
companyName: 'InnovaTech',
countryCode: 'US',
socialProfiles: {
github: 'elisabeth-d',
twitter: 'elisabeth_innova',
facebook: 'elisabeth.derington',
linkedin: 'elisabeth-derington',
instagram: 'elisabeth.innovates',
},
},
availabilityStatus: 'offline',
email: 'elisabeth.derington@innova.com',
id: 19,
name: 'Elisabeth Derington',
phoneNumber: '+14155552674',
identifier: null,
thumbnail: '',
customAttributes: {
textContact: 'Lets schedule a call.',
},
lastActivityAt: 1712123232,
createdAt: 1712123232,
},
{
additionalAttributes: {
city: 'Chicago',
country: 'United States',
description: 'Marketing specialist and content creator.',
companyName: 'Contently',
countryCode: 'US',
socialProfiles: {
github: 'olia-olenchenko',
twitter: 'olia_content',
facebook: 'olia.olenchenko',
linkedin: 'olia-olenchenko',
instagram: 'olia.creates',
},
},
availabilityStatus: 'offline',
email: 'olia.olenchenko@contently.com',
id: 18,
name: 'Olia Olenchenko',
phoneNumber: '+14155552675',
identifier: null,
thumbnail: '',
customAttributes: {},
lastActivityAt: 1712123232,
createdAt: 1712123232,
},
{
additionalAttributes: {
city: 'Boston',
country: 'United States',
description: 'SEO expert and analytics enthusiast.',
companyName: 'OptiSearch',
countryCode: 'US',
socialProfiles: {
github: 'nate-vannuchi',
twitter: 'nate_seo',
facebook: 'nathaniel.vannuchi',
linkedin: 'nathaniel-vannuchi',
instagram: 'nate.optimizes',
},
},
availabilityStatus: 'offline',
email: 'nathaniel.vannuchi@optisearch.com',
id: 17,
name: 'Nathaniel Vannuchi',
phoneNumber: '+14155552676',
identifier: null,
thumbnail: '',
customAttributes: {},
lastActivityAt: 1712123232,
createdAt: 1712123232,
},
{
additionalAttributes: {
city: 'Denver',
country: 'United States',
description: 'UI/UX designer with a flair for minimalist designs.',
companyName: 'Minimal Designs',
countryCode: 'US',
socialProfiles: {
github: 'merrile-petruk',
twitter: 'merrile_ux',
facebook: 'merrile.petruk',
linkedin: 'merrile-petruk',
instagram: 'merrile.designs',
},
},
availabilityStatus: 'offline',
email: 'merrile.petruk@minimal.com',
id: 16,
name: 'Merrile Petruk',
phoneNumber: '+14155552677',
identifier: null,
thumbnail: '',
customAttributes: {},
lastActivityAt: 1712123232,
createdAt: 1712123232,
},
{
additionalAttributes: {
city: 'Miami',
country: 'United States',
description: 'Entrepreneur with a background in e-commerce.',
companyName: 'Ecom Solutions',
countryCode: 'US',
socialProfiles: {
github: 'cordell-d',
twitter: 'cordell_entrepreneur',
facebook: 'cordell.dalinder',
linkedin: 'cordell-dalinder',
instagram: 'cordell.ecom',
},
},
availabilityStatus: 'offline',
email: 'cordell.dalinder@ecomsolutions.com',
id: 15,
name: 'Cordell Dalinder',
phoneNumber: '+14155552678',
identifier: null,
thumbnail: '',
customAttributes: {},
lastActivityAt: 1712123232,
createdAt: 1712123232,
},
];
@@ -132,7 +132,7 @@ const iconStyles = computed(() => ({
}));
const initialsStyles = computed(() => ({
fontSize: `${props.size / 1.8}px`,
fontSize: `${props.size / 2}px`,
}));
const invalidateCurrentImage = () => {
@@ -580,6 +580,12 @@
"DELETE_ERROR": "Unable to delete attribute. Please try again later"
}
}
},
"EMPTY_STATE": {
"TITLE": "Your contacts list is empty",
"SUBTITLE": "Start building your customer relationships by adding contacts or importing them from your existing database.",
"BUTTON_LABEL": "Add contact",
"SEARCH_EMPTY_STATE_TITLE": "No contacts found"
}
}
}
@@ -8,6 +8,7 @@ import { useUISettings } from 'dashboard/composables/useUISettings';
import ContactsLayout from 'dashboard/components-next/Contacts/ContactsLayout.vue';
import ContactsList from 'dashboard/components-next/Contacts/Pages/ContactsList.vue';
import ContactEmptyState from 'dashboard/components-next/Contacts/EmptyState/ContactEmptyState.vue';
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
const DEFAULT_SORT_FIELD = 'last_activity_at';
@@ -55,6 +56,8 @@ const activeSegment = computed(() =>
: undefined
);
const hasContacts = computed(() => contacts.value.length > 0);
const headerTitle = computed(() => {
if (activeSegmentId.value) {
return activeSegment.value.name;
@@ -86,7 +89,7 @@ const fetchSavedFilteredContact = async (payload, page = 1) => {
}
};
const searchContacts = debounce(async value => {
const searchContacts = debounce(async (value, page = 1) => {
searchValue.value = value;
if (!value) {
if (activeSegmentId.value) {
@@ -99,7 +102,7 @@ const searchContacts = debounce(async value => {
await store.dispatch('contacts/search', {
search: encodeURIComponent(value),
page: 1,
page,
sortAttr: buildSortAttr(),
label: activeLabel.value,
});
@@ -132,6 +135,10 @@ watch(
);
const handlePageChange = async page => {
if (searchValue.value) {
await searchContacts(searchValue.value, page);
return;
}
if (activeSegmentId.value) {
await fetchSavedFilteredContact(activeSegment.value.query, page);
} else {
@@ -165,13 +172,15 @@ onMounted(async () => {
class="flex flex-col justify-between flex-1 h-full m-0 overflow-auto bg-n-background"
>
<ContactsLayout
:search-value="searchValue"
:header-title="headerTitle"
:button-label="$t('CONTACTS_LAYOUT.HEADER.MESSAGE_BUTTON')"
:current-page="currentPage"
:total-items="totalItems"
:show-pagination-footer="!isFetchingList && searchValue === ''"
:show-pagination-footer="!isFetchingList && hasContacts"
:active-sort="sortState.activeSort"
:active-ordering="sortState.activeOrdering"
:is-empty-state="!hasContacts && searchValue === ''"
@update:current-page="handlePageChange"
@search="searchContacts"
@sort="handleSort"
@@ -182,7 +191,27 @@ onMounted(async () => {
>
<Spinner />
</div>
<ContactsList v-else :contacts="contacts" />
<template v-else>
<div
v-if="searchValue && !hasContacts"
class="flex items-center justify-center py-10"
>
<span class="text-base text-n-slate-11">
{{ $t('CONTACTS_LAYOUT.EMPTY_STATE.SEARCH_EMPTY_STATE_TITLE') }}
</span>
</div>
<ContactEmptyState
v-else-if="!searchValue && !hasContacts"
class="pt-14"
:title="t('CONTACTS_LAYOUT.EMPTY_STATE.TITLE')"
:subtitle="t('CONTACTS_LAYOUT.EMPTY_STATE.SUBTITLE')"
:button-label="t('CONTACTS_LAYOUT.EMPTY_STATE.BUTTON_LABEL')"
/>
<ContactsList v-else :contacts="contacts" />
</template>
</ContactsLayout>
</div>
</template>