Restructure omni services and add Chatwoot research snapshot
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
<script setup>
|
||||
import ArticleCard from './ArticleCard.vue';
|
||||
|
||||
const articles = [
|
||||
{
|
||||
id: 1,
|
||||
title: "How to get an SSL certificate for your Help Center's custom domain",
|
||||
status: 'draft',
|
||||
updatedAt: 1729048936,
|
||||
author: {
|
||||
name: 'John',
|
||||
thumbnail: 'https://i.pravatar.cc/300',
|
||||
},
|
||||
category: {
|
||||
title: 'Marketing',
|
||||
slug: 'marketing',
|
||||
icon: '📈',
|
||||
},
|
||||
views: 400,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Setting up your first Help Center portal',
|
||||
status: '',
|
||||
updatedAt: 1729048936,
|
||||
author: {
|
||||
name: 'John',
|
||||
thumbnail: 'https://i.pravatar.cc/300',
|
||||
},
|
||||
category: {
|
||||
title: 'Development',
|
||||
slug: 'development',
|
||||
icon: '🛠️',
|
||||
},
|
||||
views: 1400,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Best practices for organizing your Help Center content',
|
||||
status: 'archived',
|
||||
updatedAt: 1729048936,
|
||||
author: {
|
||||
name: 'Fernando',
|
||||
thumbnail: 'https://i.pravatar.cc/300',
|
||||
},
|
||||
category: {
|
||||
title: 'Finance',
|
||||
slug: 'finance',
|
||||
icon: '💰',
|
||||
},
|
||||
views: 4300,
|
||||
},
|
||||
];
|
||||
|
||||
const category = {
|
||||
name: 'Marketing',
|
||||
slug: 'marketing',
|
||||
icon: '📈',
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable vue/no-bare-strings-in-template -->
|
||||
<!-- eslint-disable vue/no-undef-components -->
|
||||
<template>
|
||||
<Story
|
||||
title="Components/HelpCenter/ArticleCard"
|
||||
:layout="{ type: 'grid', width: '700px' }"
|
||||
>
|
||||
<Variant title="Article Card">
|
||||
<div
|
||||
v-for="(article, index) in articles"
|
||||
:key="index"
|
||||
class="px-20 py-4 bg-n-background"
|
||||
>
|
||||
<ArticleCard
|
||||
:id="article.id"
|
||||
:title="article.title"
|
||||
:status="article.status"
|
||||
:author="article.author"
|
||||
:category="category"
|
||||
:views="article.views"
|
||||
:updated-at="article.updatedAt"
|
||||
/>
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
</template>
|
||||
@@ -0,0 +1,195 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useToggle } from '@vueuse/core';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||
import {
|
||||
ARTICLE_MENU_ITEMS,
|
||||
ARTICLE_MENU_OPTIONS,
|
||||
ARTICLE_STATUSES,
|
||||
} from 'dashboard/helper/portalHelper';
|
||||
|
||||
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 Avatar from 'dashboard/components-next/avatar/Avatar.vue';
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
author: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
category: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
views: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
updatedAt: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['openArticle', 'articleAction']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const [showActionsDropdown, toggleDropdown] = useToggle();
|
||||
|
||||
const articleMenuItems = computed(() => {
|
||||
const commonItems = Object.entries(ARTICLE_MENU_ITEMS).reduce(
|
||||
(acc, [key, item]) => {
|
||||
acc[key] = { ...item, label: t(item.label) };
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
const statusItems = (
|
||||
ARTICLE_MENU_OPTIONS[props.status] ||
|
||||
ARTICLE_MENU_OPTIONS[ARTICLE_STATUSES.PUBLISHED]
|
||||
).map(key => commonItems[key]);
|
||||
|
||||
return [...statusItems, commonItems.delete];
|
||||
});
|
||||
|
||||
const statusTextColor = computed(() => {
|
||||
switch (props.status) {
|
||||
case 'archived':
|
||||
return 'text-n-slate-12';
|
||||
case 'draft':
|
||||
return 'text-n-amber-11';
|
||||
default:
|
||||
return 'text-n-teal-11';
|
||||
}
|
||||
});
|
||||
|
||||
const statusText = computed(() => {
|
||||
switch (props.status) {
|
||||
case 'archived':
|
||||
return t('HELP_CENTER.ARTICLES_PAGE.ARTICLE_CARD.CARD.STATUS.ARCHIVED');
|
||||
case 'draft':
|
||||
return t('HELP_CENTER.ARTICLES_PAGE.ARTICLE_CARD.CARD.STATUS.DRAFT');
|
||||
default:
|
||||
return t('HELP_CENTER.ARTICLES_PAGE.ARTICLE_CARD.CARD.STATUS.PUBLISHED');
|
||||
}
|
||||
});
|
||||
|
||||
const categoryName = computed(() => {
|
||||
if (props.category?.slug) {
|
||||
return `${props.category.icon} ${props.category.name}`;
|
||||
}
|
||||
return t(
|
||||
'HELP_CENTER.ARTICLES_PAGE.ARTICLE_CARD.CARD.CATEGORY.UNCATEGORISED'
|
||||
);
|
||||
});
|
||||
|
||||
const authorName = computed(() => {
|
||||
return props.author?.name || props.author?.availableName || '';
|
||||
});
|
||||
|
||||
const authorThumbnailSrc = computed(() => {
|
||||
return props.author?.thumbnail;
|
||||
});
|
||||
|
||||
const lastUpdatedAt = computed(() => {
|
||||
return dynamicTime(props.updatedAt);
|
||||
});
|
||||
|
||||
const handleArticleAction = ({ action, value }) => {
|
||||
toggleDropdown(false);
|
||||
emit('articleAction', { action, value, id: props.id });
|
||||
};
|
||||
|
||||
const handleClick = id => {
|
||||
emit('openArticle', id);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CardLayout>
|
||||
<div class="flex justify-between w-full gap-1">
|
||||
<span
|
||||
class="text-base cursor-pointer hover:underline underline-offset-2 hover:text-n-blue-11 text-n-slate-12 line-clamp-1"
|
||||
@click="handleClick(id)"
|
||||
>
|
||||
{{ title }}
|
||||
</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="text-xs font-medium inline-flex items-center h-6 px-2 py-0.5 rounded-md bg-n-alpha-2"
|
||||
:class="statusTextColor"
|
||||
>
|
||||
{{ statusText }}
|
||||
</span>
|
||||
<div
|
||||
v-on-clickaway="() => toggleDropdown(false)"
|
||||
class="relative flex items-center group"
|
||||
>
|
||||
<Button
|
||||
icon="i-lucide-ellipsis-vertical"
|
||||
color="slate"
|
||||
size="xs"
|
||||
class="rounded-md group-hover:bg-n-alpha-2"
|
||||
@click="toggleDropdown()"
|
||||
/>
|
||||
<DropdownMenu
|
||||
v-if="showActionsDropdown"
|
||||
:menu-items="articleMenuItems"
|
||||
class="mt-1 ltr:right-0 rtl:left-0 xl:ltr:left-0 xl:rtl:right-0 top-full"
|
||||
@action="handleArticleAction($event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<Avatar
|
||||
:name="authorName"
|
||||
:src="authorThumbnailSrc"
|
||||
:size="16"
|
||||
rounded-full
|
||||
/>
|
||||
<span class="text-sm truncate text-n-slate-11">
|
||||
{{ authorName || '-' }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="block text-sm whitespace-nowrap text-n-slate-11">
|
||||
{{ categoryName }}
|
||||
</span>
|
||||
<div
|
||||
class="inline-flex items-center gap-1 text-n-slate-11 whitespace-nowrap"
|
||||
>
|
||||
<Icon icon="i-lucide-eye" class="size-4" />
|
||||
<span class="text-sm">
|
||||
{{
|
||||
t('HELP_CENTER.ARTICLES_PAGE.ARTICLE_CARD.CARD.VIEWS', {
|
||||
count: views,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-sm text-n-slate-11 line-clamp-1">
|
||||
{{ lastUpdatedAt }}
|
||||
</span>
|
||||
</div>
|
||||
</CardLayout>
|
||||
</template>
|
||||
Reference in New Issue
Block a user