mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-01 03:27:52 +00:00
feat: Show popular articles on widget home (#7604)
This commit is contained in:
committed by
GitHub
parent
9efadf8804
commit
e052a061f4
@@ -280,7 +280,7 @@ export const SDK_CSS = `
|
|||||||
.woot-widget-holder {
|
.woot-widget-holder {
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
bottom: 104px;
|
bottom: 104px;
|
||||||
height: calc(85% - 64px - 20px);
|
height: calc(90% - 64px - 20px);
|
||||||
max-height: 590px !important;
|
max-height: 590px !important;
|
||||||
min-height: 250px !important;
|
min-height: 250px !important;
|
||||||
width: 400px !important;
|
width: 400px !important;
|
||||||
|
|||||||
18
app/javascript/shared/components/ArticleSkeletonLoader.vue
Normal file
18
app/javascript/shared/components/ArticleSkeletonLoader.vue
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<template>
|
||||||
|
<div class="p-6 space-y-6 bg-white">
|
||||||
|
<div class="space-y-2 animate-pulse ">
|
||||||
|
<div class="h-6 bg-slate-100 rounded w-1/5" />
|
||||||
|
<div class="h-10 bg-slate-100 rounded w-3/5" />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2 animate-pulse ">
|
||||||
|
<div class="h-5 bg-slate-100 rounded" />
|
||||||
|
<div class="h-5 bg-slate-100 rounded" />
|
||||||
|
<div class="h-5 bg-slate-100 rounded" />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2 animate-pulse ">
|
||||||
|
<div class="h-5 bg-slate-100 rounded" />
|
||||||
|
<div class="h-5 bg-slate-100 rounded" />
|
||||||
|
<div class="h-5 bg-slate-100 rounded w-7/10" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
55
app/javascript/shared/components/IframeLoader.vue
Normal file
55
app/javascript/shared/components/IframeLoader.vue
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative overflow-hidden pb-1/2 h-full">
|
||||||
|
<iframe
|
||||||
|
v-if="url"
|
||||||
|
:src="url"
|
||||||
|
class="absolute w-full h-full top-0 left-0"
|
||||||
|
@load="handleIframeLoad"
|
||||||
|
@error="handleIframeError"
|
||||||
|
/>
|
||||||
|
<article-skeleton-loader
|
||||||
|
v-if="isLoading"
|
||||||
|
class="absolute w-full h-full top-0 left-0"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="showEmptyState"
|
||||||
|
class="absolute w-full h-full top-0 left-0 flex justify-center items-center"
|
||||||
|
>
|
||||||
|
<p>{{ $t('PORTAL.IFRAME_ERROR') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ArticleSkeletonLoader from 'shared/components/ArticleSkeletonLoader';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'IframeRenderer',
|
||||||
|
components: {
|
||||||
|
ArticleSkeletonLoader,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
url: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isLoading: true,
|
||||||
|
showEmptyState: !this.url,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleIframeLoad() {
|
||||||
|
// Once loaded, the loading state is hidden
|
||||||
|
this.isLoading = false;
|
||||||
|
},
|
||||||
|
handleIframeError() {
|
||||||
|
// Hide the loading state and show the empty state when an error occurs
|
||||||
|
this.isLoading = false;
|
||||||
|
this.showEmptyState = true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -97,6 +97,7 @@ const getMostReadArticles = (slug, locale) => ({
|
|||||||
url: `/hc/${slug}/${locale}/articles.json`,
|
url: `/hc/${slug}/${locale}/articles.json`,
|
||||||
params: {
|
params: {
|
||||||
page: 1,
|
page: 1,
|
||||||
|
sort: 'views',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<div
|
<div
|
||||||
v-dompurify-html="formatMessage(message, false)"
|
v-dompurify-html="formatMessage(message, false)"
|
||||||
class="message-content"
|
class="message-content"
|
||||||
:class="$dm('text-black-900', 'dark:text-slate-50')"
|
:class="$dm('text-slate-900', 'dark:text-slate-50')"
|
||||||
/>
|
/>
|
||||||
<email-input
|
<email-input
|
||||||
v-if="isTemplateEmail"
|
v-if="isTemplateEmail"
|
||||||
|
|||||||
@@ -1,21 +1,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="py-2">
|
<div>
|
||||||
<h3 class="text-sm font-semibold text-slate-900 mb-0">{{ title }}</h3>
|
<h3 class="text-base font-medium text-slate-900 dark:text-slate-50 mb-0">
|
||||||
<article-list :articles="articles" />
|
{{ title }}
|
||||||
|
</h3>
|
||||||
|
<article-list :articles="articles" @click="onArticleClick" />
|
||||||
<button
|
<button
|
||||||
class="inline-flex text-sm font-medium rounded-md px-2 py-1 -ml-2 leading-6 text-slate-800 justify-between items-center hover:bg-slate-25 see-articles"
|
class="inline-flex text-sm font-medium rounded-md px-2 py-1 -ml-2 leading-6 text-slate-800 dark:text-slate-50 justify-between items-center hover:bg-slate-25 see-articles"
|
||||||
@click="$emit('view-all-articles')"
|
:style="{ color: widgetColor }"
|
||||||
|
@click="$emit('view-all')"
|
||||||
>
|
>
|
||||||
<span class="pr-2">{{ $t('PORTAL.VIEW_ALL_ARTICLES') }}</span>
|
<span class="pr-2 text-sm">{{ $t('PORTAL.VIEW_ALL_ARTICLES') }}</span>
|
||||||
<fluent-icon icon="arrow-right" size="14" />
|
<fluent-icon icon="arrow-right" size="14" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
import ArticleList from './ArticleList.vue';
|
import ArticleList from './ArticleList.vue';
|
||||||
|
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { ArticleList },
|
components: { FluentIcon, ArticleList },
|
||||||
props: {
|
props: {
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -25,9 +31,13 @@ export default {
|
|||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
categoryPath: {
|
},
|
||||||
type: String,
|
computed: {
|
||||||
default: '',
|
...mapGetters({ widgetColor: 'appConfig/getWidgetColor' }),
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onArticleClick(link) {
|
||||||
|
this.$emit('view', link);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
|
||||||
<h2 class="text-base font-bold leading-6 text-slate-800 mb-0">
|
|
||||||
{{ $t('PORTAL.POPULAR_ARTICLES') }}
|
|
||||||
</h2>
|
|
||||||
<category-card
|
<category-card
|
||||||
|
:title="$t('PORTAL.POPULAR_ARTICLES')"
|
||||||
:articles="articles.slice(0, 4)"
|
:articles="articles.slice(0, 4)"
|
||||||
@view-all-articles="$emit('view-all-articles')"
|
@view-all="$emit('view-all')"
|
||||||
|
@view="onArticleClick"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -24,6 +21,11 @@ export default {
|
|||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
|
onArticleClick(link) {
|
||||||
|
this.$emit('view', link);
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<ul role="list" class="py-2">
|
<ul role="list" class="py-2">
|
||||||
<article-list-item
|
<article-list-item
|
||||||
v-for="article in articles"
|
v-for="article in articles"
|
||||||
:key="article.id"
|
:key="article.slug"
|
||||||
:link="article.link"
|
:link="article.link"
|
||||||
:title="article.title"
|
:title="article.title"
|
||||||
@click="onClick"
|
@click="onClick"
|
||||||
|
|||||||
@@ -1,20 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<li
|
<li
|
||||||
class="py-1 flex items-center justify-between -mx-1 px-1 hover:bg-slate-25"
|
class="py-1 flex items-center justify-between -mx-1 px-1 hover:bg-slate-75 dark:hover:bg-slate-600 rounded cursor-pointer text-slate-700 dark:text-slate-50 dark:hover:text-slate-25 hover:text-slate-900 "
|
||||||
|
@click="onClick"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="text-slate-700 hover:text-slate-900 underline-offset-2 text-sm leading-6"
|
class="underline-offset-2 text-sm leading-6 text-left"
|
||||||
@click="onClick"
|
@click="onClick"
|
||||||
>
|
>
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</button>
|
</button>
|
||||||
<span class="pl-1 text-slate-700 arrow">
|
<span class="pl-1 first-letter arrow">
|
||||||
<fluent-icon icon="arrow-right" size="14" />
|
<fluent-icon icon="arrow-right" size="14" />
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
|
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: { FluentIcon },
|
||||||
props: {
|
props: {
|
||||||
link: {
|
link: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|||||||
@@ -4,7 +4,11 @@
|
|||||||
:class="$dm('bg-white', 'dark:bg-slate-900')"
|
:class="$dm('bg-white', 'dark:bg-slate-900')"
|
||||||
>
|
>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<button v-if="showBackButton" @click="onBackButtonClick">
|
<button
|
||||||
|
v-if="showBackButton"
|
||||||
|
class="-ml-3 px-2"
|
||||||
|
@click="onBackButtonClick"
|
||||||
|
>
|
||||||
<fluent-icon
|
<fluent-icon
|
||||||
icon="chevron-left"
|
icon="chevron-left"
|
||||||
size="24"
|
size="24"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<header
|
<header
|
||||||
class="header-expanded py-6 px-5 relative box-border w-full"
|
class="header-expanded py-6 px-5 relative box-border w-full"
|
||||||
:class="$dm('bg-white', 'dark:bg-slate-900')"
|
:class="showBg ? 'bg-transparent' : 'bg-white dark:bg-slate-900'"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex items-start"
|
class="flex items-start"
|
||||||
@@ -13,23 +13,25 @@
|
|||||||
:src="avatarUrl"
|
:src="avatarUrl"
|
||||||
alt="Avatar"
|
alt="Avatar"
|
||||||
/>
|
/>
|
||||||
<header-actions :show-popout-button="showPopoutButton" />
|
<header-actions
|
||||||
|
:show-popout-button="showPopoutButton"
|
||||||
|
:show-end-conversation-button="false"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<h2
|
<h2
|
||||||
v-dompurify-html="introHeading"
|
v-dompurify-html="introHeading"
|
||||||
class="mt-5 text-3xl mb-3 leading-8 font-normal"
|
class="mt-4 text-2xl mb-2 font-normal"
|
||||||
:class="$dm('text-slate-900', 'dark:text-slate-50')"
|
:class="$dm('text-slate-900', 'dark:text-slate-50')"
|
||||||
/>
|
/>
|
||||||
<p
|
<p
|
||||||
v-dompurify-html="introBody"
|
v-dompurify-html="introBody"
|
||||||
class="text-lg leading-normal"
|
class="text-base leading-normal"
|
||||||
:class="$dm('text-slate-700', 'dark:text-slate-200')"
|
:class="$dm('text-slate-700', 'dark:text-slate-200')"
|
||||||
/>
|
/>
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
import HeaderActions from './HeaderActions';
|
import HeaderActions from './HeaderActions';
|
||||||
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
|
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
|
||||||
|
|
||||||
@@ -56,11 +58,10 @@ export default {
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
showBg: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
},
|
},
|
||||||
computed: {
|
|
||||||
...mapGetters({
|
|
||||||
widgetColor: 'appConfig/getWidgetColor',
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="showHeaderActions" class="actions flex items-center">
|
<div v-if="showHeaderActions" class="actions flex items-center">
|
||||||
<button
|
<button
|
||||||
v-if="canLeaveConversation && hasEndConversationEnabled"
|
v-if="
|
||||||
|
canLeaveConversation &&
|
||||||
|
hasEndConversationEnabled &&
|
||||||
|
showEndConversationButton
|
||||||
|
"
|
||||||
class="button transparent compact"
|
class="button transparent compact"
|
||||||
:title="$t('END_CONVERSATION')"
|
:title="$t('END_CONVERSATION')"
|
||||||
@click="resolveConversation"
|
@click="resolveConversation"
|
||||||
@@ -56,6 +60,10 @@ export default {
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
showEndConversationButton: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="px-5">
|
<div class="p-4 shadow rounded-md bg-white dark:bg-slate-700">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<div
|
<div
|
||||||
class="max-w-xs"
|
class="max-w-xs"
|
||||||
:class="$dm('text-black-700', 'dark:text-slate-50')"
|
:class="$dm('text-slate-700', 'dark:text-slate-50')"
|
||||||
>
|
>
|
||||||
<div class="text-base leading-5 font-medium mb-1">
|
<div class="text-sm font-medium mb-1">
|
||||||
{{
|
{{
|
||||||
isOnline
|
isOnline
|
||||||
? $t('TEAM_AVAILABILITY.ONLINE')
|
? $t('TEAM_AVAILABILITY.ONLINE')
|
||||||
: $t('TEAM_AVAILABILITY.OFFLINE')
|
: $t('TEAM_AVAILABILITY.OFFLINE')
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs leading-3 mt-1">
|
<div class="text-xs mt-1">
|
||||||
{{ replyWaitMessage }}
|
{{ replyWaitMessage }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="w-full h-full flex flex-col"
|
class="w-full h-full flex flex-col relative bg-slate-50 dark:bg-slate-800"
|
||||||
:class="$dm('bg-slate-50', 'dark:bg-slate-800')"
|
:class="{ 'overflow-auto': isOnHomeView }"
|
||||||
|
:style="portal ? { backgroundColor: backgroundColor } : {}"
|
||||||
@keydown.esc="closeWindow"
|
@keydown.esc="closeWindow"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="header-wrap"
|
class="header-wrap sticky top-0 z-40"
|
||||||
:class="{
|
:class="{
|
||||||
expanded: !isHeaderCollapsed,
|
expanded: !isHeaderCollapsed,
|
||||||
collapsed: isHeaderCollapsed,
|
collapsed: isHeaderCollapsed,
|
||||||
|
'custom-header-shadow': (isOnHomeView && !portal) || !isOnArticleViewer,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<transition
|
<transition
|
||||||
@@ -25,6 +27,7 @@
|
|||||||
:intro-body="channelConfig.welcomeTagline"
|
:intro-body="channelConfig.welcomeTagline"
|
||||||
:avatar-url="channelConfig.avatarUrl"
|
:avatar-url="channelConfig.avatarUrl"
|
||||||
:show-popout-button="appConfig.showPopoutButton"
|
:show-popout-button="appConfig.showPopoutButton"
|
||||||
|
:show-bg="!!portal"
|
||||||
/>
|
/>
|
||||||
<chat-header
|
<chat-header
|
||||||
v-if="isHeaderCollapsed"
|
v-if="isHeaderCollapsed"
|
||||||
@@ -32,6 +35,7 @@
|
|||||||
:avatar-url="channelConfig.avatarUrl"
|
:avatar-url="channelConfig.avatarUrl"
|
||||||
:show-popout-button="appConfig.showPopoutButton"
|
:show-popout-button="appConfig.showPopoutButton"
|
||||||
:available-agents="availableAgents"
|
:available-agents="availableAgents"
|
||||||
|
:show-back-button="showBackButton"
|
||||||
/>
|
/>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,7 +50,7 @@
|
|||||||
>
|
>
|
||||||
<router-view />
|
<router-view />
|
||||||
</transition>
|
</transition>
|
||||||
<branding :disable-branding="disableBranding" />
|
<branding v-if="!isOnArticleViewer" :disable-branding="disableBranding" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
@@ -75,20 +79,42 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
availableAgents: 'agent/availableAgents',
|
|
||||||
appConfig: 'appConfig/getAppConfig',
|
appConfig: 'appConfig/getAppConfig',
|
||||||
|
availableAgents: 'agent/availableAgents',
|
||||||
|
widgetColor: 'appConfig/getWidgetColor',
|
||||||
}),
|
}),
|
||||||
|
portal() {
|
||||||
|
return window.chatwootWebChannel.portal;
|
||||||
|
},
|
||||||
isHeaderCollapsed() {
|
isHeaderCollapsed() {
|
||||||
if (!this.hasIntroText) {
|
if (!this.hasIntroText) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return this.$route.name !== 'home';
|
return !this.isOnHomeView;
|
||||||
|
},
|
||||||
|
backgroundColor() {
|
||||||
|
const color = this.widgetColor.replace('#', '');
|
||||||
|
const r = parseInt(color.slice(0, 2), 16);
|
||||||
|
const g = parseInt(color.slice(2, 4), 16);
|
||||||
|
const b = parseInt(color.slice(4, 6), 16);
|
||||||
|
return `rgba(${r},${g},${b}, 0.02)`;
|
||||||
},
|
},
|
||||||
hasIntroText() {
|
hasIntroText() {
|
||||||
return (
|
return (
|
||||||
this.channelConfig.welcomeTitle || this.channelConfig.welcomeTagline
|
this.channelConfig.welcomeTitle || this.channelConfig.welcomeTagline
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
showBackButton() {
|
||||||
|
return ['article-viewer', 'messages', 'prechat-form'].includes(
|
||||||
|
this.$route.name
|
||||||
|
);
|
||||||
|
},
|
||||||
|
isOnArticleViewer() {
|
||||||
|
return ['article-viewer'].includes(this.$route.name);
|
||||||
|
},
|
||||||
|
isOnHomeView() {
|
||||||
|
return ['home'].includes(this.$route.name);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
closeWindow() {
|
closeWindow() {
|
||||||
@@ -102,11 +128,13 @@ export default {
|
|||||||
@import '~widget/assets/scss/variables';
|
@import '~widget/assets/scss/variables';
|
||||||
@import '~widget/assets/scss/mixins';
|
@import '~widget/assets/scss/mixins';
|
||||||
|
|
||||||
|
.custom-header-shadow {
|
||||||
|
@include shadow-large;
|
||||||
|
}
|
||||||
|
|
||||||
.header-wrap {
|
.header-wrap {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
transition: max-height 300ms;
|
transition: max-height 300ms;
|
||||||
z-index: 99;
|
|
||||||
@include shadow-large;
|
|
||||||
|
|
||||||
&.expanded {
|
&.expanded {
|
||||||
max-height: 16rem;
|
max-height: 16rem;
|
||||||
|
|||||||
@@ -108,6 +108,7 @@
|
|||||||
},
|
},
|
||||||
"PORTAL": {
|
"PORTAL": {
|
||||||
"POPULAR_ARTICLES": "Popular Articles",
|
"POPULAR_ARTICLES": "Popular Articles",
|
||||||
"VIEW_ALL_ARTICLES": "View all articles"
|
"VIEW_ALL_ARTICLES": "View all articles",
|
||||||
|
"IFRAME_LOAD_ERROR": "There was an error loading the article, please refresh the page and try again."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,12 @@ export default new Router({
|
|||||||
name: 'messages',
|
name: 'messages',
|
||||||
component: () => import('./views/Messages.vue'),
|
component: () => import('./views/Messages.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/article',
|
||||||
|
name: 'article-viewer',
|
||||||
|
props: true,
|
||||||
|
component: () => import('./views/ArticleViewer.vue'),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ export const actions = {
|
|||||||
try {
|
try {
|
||||||
const { data } = await getMostReadArticles(slug, locale);
|
const { data } = await getMostReadArticles(slug, locale);
|
||||||
const { payload = [] } = data;
|
const { payload = [] } = data;
|
||||||
|
|
||||||
if (payload.length) {
|
if (payload.length) {
|
||||||
commit('setArticles', payload);
|
commit('setArticles', payload);
|
||||||
}
|
}
|
||||||
|
|||||||
34
app/javascript/widget/views/ArticleViewer.vue
Normal file
34
app/javascript/widget/views/ArticleViewer.vue
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<template>
|
||||||
|
<div class="bg-white h-full">
|
||||||
|
<iframe-loader :url="link" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { IFrameHelper } from 'widget/helpers/utils';
|
||||||
|
import IframeLoader from 'shared/components/IframeLoader';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Campaigns',
|
||||||
|
components: {
|
||||||
|
IframeLoader,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
link: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
closeFullView() {
|
||||||
|
if (IFrameHelper.isIFrame()) {
|
||||||
|
IFrameHelper.sendMessage({
|
||||||
|
event: 'setCampaignReadOn',
|
||||||
|
});
|
||||||
|
IFrameHelper.sendMessage({ event: 'toggleBubble' });
|
||||||
|
bus.$emit('snooze-campaigns');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -1,24 +1,54 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-1 flex-col justify-end">
|
<div
|
||||||
<div class="flex flex-1 overflow-auto">
|
class="z-50 rounded-md border-t border-slate-50 w-full flex flex-1 flex-col justify-end"
|
||||||
<!-- Load Conversation List Components Here -->
|
:class="{ 'pb-2': showArticles }"
|
||||||
|
>
|
||||||
|
<div v-if="false" class="px-4 py-2 w-full">
|
||||||
|
<div class="p-4 rounded-md bg-white dark:bg-slate-700 shadow w-full">
|
||||||
|
<article-hero
|
||||||
|
v-if="
|
||||||
|
!articleUiFlags.isFetching &&
|
||||||
|
!articleUiFlags.isError &&
|
||||||
|
popularArticles.length
|
||||||
|
"
|
||||||
|
:articles="popularArticles"
|
||||||
|
@view="openArticleInArticleViewer"
|
||||||
|
@view-all="viewAllArticles"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="articleUiFlags.isFetching"
|
||||||
|
class="flex flex-col items-center justify-center py-8"
|
||||||
|
>
|
||||||
|
<div class="inline-block p-4 rounded-lg bg-slate-200">
|
||||||
|
<spinner size="small" />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="px-4 pt-2 w-full sticky bottom-4">
|
||||||
<team-availability
|
<team-availability
|
||||||
:available-agents="availableAgents"
|
:available-agents="availableAgents"
|
||||||
:has-conversation="!!conversationSize"
|
:has-conversation="!!conversationSize"
|
||||||
@start-conversation="startConversation"
|
@start-conversation="startConversation"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import configMixin from '../mixins/configMixin';
|
|
||||||
import TeamAvailability from 'widget/components/TeamAvailability';
|
import TeamAvailability from 'widget/components/TeamAvailability';
|
||||||
|
import ArticleHero from 'widget/components/ArticleHero';
|
||||||
|
import Spinner from 'shared/components/Spinner.vue';
|
||||||
|
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import routerMixin from 'widget/mixins/routerMixin';
|
import routerMixin from 'widget/mixins/routerMixin';
|
||||||
|
import configMixin from 'widget/mixins/configMixin';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Home',
|
name: 'Home',
|
||||||
components: {
|
components: {
|
||||||
|
Spinner,
|
||||||
|
ArticleHero,
|
||||||
TeamAvailability,
|
TeamAvailability,
|
||||||
},
|
},
|
||||||
mixins: [configMixin, routerMixin],
|
mixins: [configMixin, routerMixin],
|
||||||
@@ -32,15 +62,31 @@ export default {
|
|||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data() {
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
availableAgents: 'agent/availableAgents',
|
availableAgents: 'agent/availableAgents',
|
||||||
activeCampaign: 'campaign/getActiveCampaign',
|
activeCampaign: 'campaign/getActiveCampaign',
|
||||||
conversationSize: 'conversation/getConversationSize',
|
conversationSize: 'conversation/getConversationSize',
|
||||||
|
popularArticles: 'article/popularArticles',
|
||||||
|
articleUiFlags: 'article/uiFlags',
|
||||||
}),
|
}),
|
||||||
|
portal() {
|
||||||
|
return window.chatwootWebChannel.portal;
|
||||||
|
},
|
||||||
|
showArticles() {
|
||||||
|
return (
|
||||||
|
this.portal &&
|
||||||
|
(this.articleUiFlags.isFetching || this.popularArticles.length)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (this.portal && this.popularArticles.length === 0) {
|
||||||
|
this.$store.dispatch('article/fetch', {
|
||||||
|
slug: this.portal.slug,
|
||||||
|
locale: 'en',
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
startConversation() {
|
startConversation() {
|
||||||
@@ -49,6 +95,19 @@ export default {
|
|||||||
}
|
}
|
||||||
return this.replaceRoute('messages');
|
return this.replaceRoute('messages');
|
||||||
},
|
},
|
||||||
|
openArticleInArticleViewer(link) {
|
||||||
|
this.$router.push({
|
||||||
|
name: 'article-viewer',
|
||||||
|
params: { link: `${link}?show_plain_layout=true` },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
viewAllArticles() {
|
||||||
|
const {
|
||||||
|
portal: { slug },
|
||||||
|
locale = 'en',
|
||||||
|
} = window.chatwootWebChannel;
|
||||||
|
this.openArticleInArticleViewer(`/hc/${slug}/${locale}`);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -38,3 +38,5 @@ json.associated_articles do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
json.link "hc/#{article.portal.slug}/articles/#{article.slug}"
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
welcomeTagline: '<%= @web_widget.welcome_tagline %>',
|
welcomeTagline: '<%= @web_widget.welcome_tagline %>',
|
||||||
welcomeTitle: '<%= @web_widget.welcome_title %>',
|
welcomeTitle: '<%= @web_widget.welcome_title %>',
|
||||||
widgetColor: '<%= @web_widget.widget_color %>',
|
widgetColor: '<%= @web_widget.widget_color %>',
|
||||||
|
portal: <%= @web_widget.inbox.portal.to_json.html_safe %>,
|
||||||
enabledFeatures: <%= @web_widget.selected_feature_flags.to_json.html_safe %>,
|
enabledFeatures: <%= @web_widget.selected_feature_flags.to_json.html_safe %>,
|
||||||
enabledLanguages: <%= available_locales_with_name.to_json.html_safe %>,
|
enabledLanguages: <%= available_locales_with_name.to_json.html_safe %>,
|
||||||
replyTime: '<%= @web_widget.reply_time %>',
|
replyTime: '<%= @web_widget.reply_time %>',
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
fontSize: {
|
fontSize: {
|
||||||
xxs: '0.625rem',
|
|
||||||
...defaultTheme.fontSize,
|
...defaultTheme.fontSize,
|
||||||
|
xxs: '0.625rem',
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
transparent: 'transparent',
|
transparent: 'transparent',
|
||||||
@@ -113,6 +113,7 @@ module.exports = {
|
|||||||
body: '#2f3b49',
|
body: '#2f3b49',
|
||||||
},
|
},
|
||||||
keyframes: {
|
keyframes: {
|
||||||
|
...defaultTheme.keyframes,
|
||||||
wiggle: {
|
wiggle: {
|
||||||
'0%': { transform: 'translateX(0)' },
|
'0%': { transform: 'translateX(0)' },
|
||||||
'15%': { transform: 'translateX(0.375rem)' },
|
'15%': { transform: 'translateX(0.375rem)' },
|
||||||
@@ -125,6 +126,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
animation: {
|
animation: {
|
||||||
|
...defaultTheme.animation,
|
||||||
wiggle: 'wiggle 0.5s ease-in-out',
|
wiggle: 'wiggle 0.5s ease-in-out',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user