mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 02:57:57 +00:00 
			
		
		
		
	feat: Show popular articles on widget home (#7604)
This commit is contained in:
		 Nithin David Thomas
					Nithin David Thomas
				
			
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			 GitHub
						GitHub
					
				
			
						parent
						
							9efadf8804
						
					
				
				
					commit
					e052a061f4
				
			| @@ -280,7 +280,7 @@ export const SDK_CSS = ` | ||||
|   .woot-widget-holder { | ||||
|     border-radius: 16px; | ||||
|     bottom: 104px; | ||||
|     height: calc(85% - 64px - 20px); | ||||
|     height: calc(90% - 64px - 20px); | ||||
|     max-height: 590px !important; | ||||
|     min-height: 250px !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`, | ||||
|   params: { | ||||
|     page: 1, | ||||
|     sort: 'views', | ||||
|   }, | ||||
| }); | ||||
|  | ||||
|   | ||||
| @@ -10,7 +10,7 @@ | ||||
|       <div | ||||
|         v-dompurify-html="formatMessage(message, false)" | ||||
|         class="message-content" | ||||
|         :class="$dm('text-black-900', 'dark:text-slate-50')" | ||||
|         :class="$dm('text-slate-900', 'dark:text-slate-50')" | ||||
|       /> | ||||
|       <email-input | ||||
|         v-if="isTemplateEmail" | ||||
|   | ||||
| @@ -1,21 +1,27 @@ | ||||
| <template> | ||||
|   <div class="py-2"> | ||||
|     <h3 class="text-sm font-semibold text-slate-900 mb-0">{{ title }}</h3> | ||||
|     <article-list :articles="articles" /> | ||||
|   <div> | ||||
|     <h3 class="text-base font-medium text-slate-900 dark:text-slate-50 mb-0"> | ||||
|       {{ title }} | ||||
|     </h3> | ||||
|     <article-list :articles="articles" @click="onArticleClick" /> | ||||
|     <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" | ||||
|       @click="$emit('view-all-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" | ||||
|       :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" /> | ||||
|     </button> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import { mapGetters } from 'vuex'; | ||||
| import ArticleList from './ArticleList.vue'; | ||||
| import FluentIcon from 'shared/components/FluentIcon/Index.vue'; | ||||
|  | ||||
| export default { | ||||
|   components: { ArticleList }, | ||||
|   components: { FluentIcon, ArticleList }, | ||||
|   props: { | ||||
|     title: { | ||||
|       type: String, | ||||
| @@ -25,9 +31,13 @@ export default { | ||||
|       type: Array, | ||||
|       default: () => [], | ||||
|     }, | ||||
|     categoryPath: { | ||||
|       type: String, | ||||
|       default: '', | ||||
|   }, | ||||
|   computed: { | ||||
|     ...mapGetters({ widgetColor: 'appConfig/getWidgetColor' }), | ||||
|   }, | ||||
|   methods: { | ||||
|     onArticleClick(link) { | ||||
|       this.$emit('view', link); | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
|   | ||||
| @@ -1,13 +1,10 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <h2 class="text-base font-bold leading-6 text-slate-800 mb-0"> | ||||
|       {{ $t('PORTAL.POPULAR_ARTICLES') }} | ||||
|     </h2> | ||||
|     <category-card | ||||
|       :articles="articles.slice(0, 4)" | ||||
|       @view-all-articles="$emit('view-all-articles')" | ||||
|     /> | ||||
|   </div> | ||||
|   <category-card | ||||
|     :title="$t('PORTAL.POPULAR_ARTICLES')" | ||||
|     :articles="articles.slice(0, 4)" | ||||
|     @view-all="$emit('view-all')" | ||||
|     @view="onArticleClick" | ||||
|   /> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| @@ -24,6 +21,11 @@ export default { | ||||
|       default: '', | ||||
|     }, | ||||
|   }, | ||||
|   methods: { | ||||
|     onArticleClick(link) { | ||||
|       this.$emit('view', link); | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|  | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
|   <ul role="list" class="py-2"> | ||||
|     <article-list-item | ||||
|       v-for="article in articles" | ||||
|       :key="article.id" | ||||
|       :key="article.slug" | ||||
|       :link="article.link" | ||||
|       :title="article.title" | ||||
|       @click="onClick" | ||||
|   | ||||
| @@ -1,20 +1,24 @@ | ||||
| <template> | ||||
|   <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 | ||||
|       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" | ||||
|     > | ||||
|       {{ title }} | ||||
|     </button> | ||||
|     <span class="pl-1 text-slate-700 arrow"> | ||||
|     <span class="pl-1 first-letter arrow"> | ||||
|       <fluent-icon icon="arrow-right" size="14" /> | ||||
|     </span> | ||||
|   </li> | ||||
| </template> | ||||
| <script> | ||||
| import FluentIcon from 'shared/components/FluentIcon/Index.vue'; | ||||
|  | ||||
| export default { | ||||
|   components: { FluentIcon }, | ||||
|   props: { | ||||
|     link: { | ||||
|       type: String, | ||||
|   | ||||
| @@ -4,7 +4,11 @@ | ||||
|     :class="$dm('bg-white', 'dark:bg-slate-900')" | ||||
|   > | ||||
|     <div class="flex items-center"> | ||||
|       <button v-if="showBackButton" @click="onBackButtonClick"> | ||||
|       <button | ||||
|         v-if="showBackButton" | ||||
|         class="-ml-3 px-2" | ||||
|         @click="onBackButtonClick" | ||||
|       > | ||||
|         <fluent-icon | ||||
|           icon="chevron-left" | ||||
|           size="24" | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| <template> | ||||
|   <header | ||||
|     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 | ||||
|       class="flex items-start" | ||||
| @@ -13,23 +13,25 @@ | ||||
|         :src="avatarUrl" | ||||
|         alt="Avatar" | ||||
|       /> | ||||
|       <header-actions :show-popout-button="showPopoutButton" /> | ||||
|       <header-actions | ||||
|         :show-popout-button="showPopoutButton" | ||||
|         :show-end-conversation-button="false" | ||||
|       /> | ||||
|     </div> | ||||
|     <h2 | ||||
|       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')" | ||||
|     /> | ||||
|     <p | ||||
|       v-dompurify-html="introBody" | ||||
|       class="text-lg leading-normal" | ||||
|       class="text-base leading-normal" | ||||
|       :class="$dm('text-slate-700', 'dark:text-slate-200')" | ||||
|     /> | ||||
|   </header> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import { mapGetters } from 'vuex'; | ||||
| import HeaderActions from './HeaderActions'; | ||||
| import darkModeMixin from 'widget/mixins/darkModeMixin.js'; | ||||
|  | ||||
| @@ -56,11 +58,10 @@ export default { | ||||
|       type: Boolean, | ||||
|       default: false, | ||||
|     }, | ||||
|   }, | ||||
|   computed: { | ||||
|     ...mapGetters({ | ||||
|       widgetColor: 'appConfig/getWidgetColor', | ||||
|     }), | ||||
|     showBg: { | ||||
|       type: Boolean, | ||||
|       default: true, | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
|   | ||||
| @@ -1,7 +1,11 @@ | ||||
| <template> | ||||
|   <div v-if="showHeaderActions" class="actions flex items-center"> | ||||
|     <button | ||||
|       v-if="canLeaveConversation && hasEndConversationEnabled" | ||||
|       v-if=" | ||||
|         canLeaveConversation && | ||||
|           hasEndConversationEnabled && | ||||
|           showEndConversationButton | ||||
|       " | ||||
|       class="button transparent compact" | ||||
|       :title="$t('END_CONVERSATION')" | ||||
|       @click="resolveConversation" | ||||
| @@ -56,6 +60,10 @@ export default { | ||||
|       type: Boolean, | ||||
|       default: false, | ||||
|     }, | ||||
|     showEndConversationButton: { | ||||
|       type: Boolean, | ||||
|       default: true, | ||||
|     }, | ||||
|   }, | ||||
|   computed: { | ||||
|     ...mapGetters({ | ||||
|   | ||||
| @@ -1,18 +1,18 @@ | ||||
| <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="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 | ||||
|               ? $t('TEAM_AVAILABILITY.ONLINE') | ||||
|               : $t('TEAM_AVAILABILITY.OFFLINE') | ||||
|           }} | ||||
|         </div> | ||||
|         <div class="text-xs leading-3 mt-1"> | ||||
|         <div class="text-xs mt-1"> | ||||
|           {{ replyWaitMessage }} | ||||
|         </div> | ||||
|       </div> | ||||
|   | ||||
| @@ -1,14 +1,16 @@ | ||||
| <template> | ||||
|   <div | ||||
|     class="w-full h-full flex flex-col" | ||||
|     :class="$dm('bg-slate-50', 'dark:bg-slate-800')" | ||||
|     class="w-full h-full flex flex-col relative bg-slate-50 dark:bg-slate-800" | ||||
|     :class="{ 'overflow-auto': isOnHomeView }" | ||||
|     :style="portal ? { backgroundColor: backgroundColor } : {}" | ||||
|     @keydown.esc="closeWindow" | ||||
|   > | ||||
|     <div | ||||
|       class="header-wrap" | ||||
|       class="header-wrap sticky top-0 z-40" | ||||
|       :class="{ | ||||
|         expanded: !isHeaderCollapsed, | ||||
|         collapsed: isHeaderCollapsed, | ||||
|         'custom-header-shadow': (isOnHomeView && !portal) || !isOnArticleViewer, | ||||
|       }" | ||||
|     > | ||||
|       <transition | ||||
| @@ -25,6 +27,7 @@ | ||||
|           :intro-body="channelConfig.welcomeTagline" | ||||
|           :avatar-url="channelConfig.avatarUrl" | ||||
|           :show-popout-button="appConfig.showPopoutButton" | ||||
|           :show-bg="!!portal" | ||||
|         /> | ||||
|         <chat-header | ||||
|           v-if="isHeaderCollapsed" | ||||
| @@ -32,6 +35,7 @@ | ||||
|           :avatar-url="channelConfig.avatarUrl" | ||||
|           :show-popout-button="appConfig.showPopoutButton" | ||||
|           :available-agents="availableAgents" | ||||
|           :show-back-button="showBackButton" | ||||
|         /> | ||||
|       </transition> | ||||
|     </div> | ||||
| @@ -46,7 +50,7 @@ | ||||
|     > | ||||
|       <router-view /> | ||||
|     </transition> | ||||
|     <branding :disable-branding="disableBranding" /> | ||||
|     <branding v-if="!isOnArticleViewer" :disable-branding="disableBranding" /> | ||||
|   </div> | ||||
| </template> | ||||
| <script> | ||||
| @@ -75,20 +79,42 @@ export default { | ||||
|   }, | ||||
|   computed: { | ||||
|     ...mapGetters({ | ||||
|       availableAgents: 'agent/availableAgents', | ||||
|       appConfig: 'appConfig/getAppConfig', | ||||
|       availableAgents: 'agent/availableAgents', | ||||
|       widgetColor: 'appConfig/getWidgetColor', | ||||
|     }), | ||||
|     portal() { | ||||
|       return window.chatwootWebChannel.portal; | ||||
|     }, | ||||
|     isHeaderCollapsed() { | ||||
|       if (!this.hasIntroText) { | ||||
|         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() { | ||||
|       return ( | ||||
|         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: { | ||||
|     closeWindow() { | ||||
| @@ -102,11 +128,13 @@ export default { | ||||
| @import '~widget/assets/scss/variables'; | ||||
| @import '~widget/assets/scss/mixins'; | ||||
|  | ||||
| .custom-header-shadow { | ||||
|   @include shadow-large; | ||||
| } | ||||
|  | ||||
| .header-wrap { | ||||
|   flex-shrink: 0; | ||||
|   transition: max-height 300ms; | ||||
|   z-index: 99; | ||||
|   @include shadow-large; | ||||
|  | ||||
|   &.expanded { | ||||
|     max-height: 16rem; | ||||
|   | ||||
| @@ -108,6 +108,7 @@ | ||||
|   }, | ||||
|   "PORTAL": { | ||||
|     "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', | ||||
|           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 { | ||||
|       const { data } = await getMostReadArticles(slug, locale); | ||||
|       const { payload = [] } = data; | ||||
|  | ||||
|       if (payload.length) { | ||||
|         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> | ||||
|   <div class="flex flex-1 flex-col justify-end"> | ||||
|     <div class="flex flex-1 overflow-auto"> | ||||
|       <!-- Load Conversation List Components Here --> | ||||
|   <div | ||||
|     class="z-50 rounded-md border-t border-slate-50 w-full flex flex-1 flex-col justify-end" | ||||
|     :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 class="px-4 pt-2 w-full sticky bottom-4"> | ||||
|       <team-availability | ||||
|         :available-agents="availableAgents" | ||||
|         :has-conversation="!!conversationSize" | ||||
|         @start-conversation="startConversation" | ||||
|       /> | ||||
|     </div> | ||||
|     <team-availability | ||||
|       :available-agents="availableAgents" | ||||
|       :has-conversation="!!conversationSize" | ||||
|       @start-conversation="startConversation" | ||||
|     /> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import configMixin from '../mixins/configMixin'; | ||||
| import TeamAvailability from 'widget/components/TeamAvailability'; | ||||
| import ArticleHero from 'widget/components/ArticleHero'; | ||||
| import Spinner from 'shared/components/Spinner.vue'; | ||||
|  | ||||
| import { mapGetters } from 'vuex'; | ||||
| import routerMixin from 'widget/mixins/routerMixin'; | ||||
| import configMixin from 'widget/mixins/configMixin'; | ||||
|  | ||||
| export default { | ||||
|   name: 'Home', | ||||
|   components: { | ||||
|     Spinner, | ||||
|     ArticleHero, | ||||
|     TeamAvailability, | ||||
|   }, | ||||
|   mixins: [configMixin, routerMixin], | ||||
| @@ -32,15 +62,31 @@ export default { | ||||
|       default: false, | ||||
|     }, | ||||
|   }, | ||||
|   data() { | ||||
|     return {}; | ||||
|   }, | ||||
|   computed: { | ||||
|     ...mapGetters({ | ||||
|       availableAgents: 'agent/availableAgents', | ||||
|       activeCampaign: 'campaign/getActiveCampaign', | ||||
|       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: { | ||||
|     startConversation() { | ||||
| @@ -49,6 +95,19 @@ export default { | ||||
|       } | ||||
|       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> | ||||
|   | ||||
| @@ -38,3 +38,5 @@ json.associated_articles do | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| json.link "hc/#{article.portal.slug}/articles/#{article.slug}" | ||||
|   | ||||
| @@ -14,6 +14,7 @@ | ||||
|         welcomeTagline: '<%= @web_widget.welcome_tagline %>', | ||||
|         welcomeTitle: '<%= @web_widget.welcome_title %>', | ||||
|         widgetColor: '<%= @web_widget.widget_color %>', | ||||
|         portal: <%= @web_widget.inbox.portal.to_json.html_safe %>, | ||||
|         enabledFeatures: <%= @web_widget.selected_feature_flags.to_json.html_safe %>, | ||||
|         enabledLanguages: <%= available_locales_with_name.to_json.html_safe %>, | ||||
|         replyTime: '<%= @web_widget.reply_time %>', | ||||
|   | ||||
| @@ -12,8 +12,8 @@ module.exports = { | ||||
|   ], | ||||
|   theme: { | ||||
|     fontSize: { | ||||
|       xxs: '0.625rem', | ||||
|       ...defaultTheme.fontSize, | ||||
|       xxs: '0.625rem', | ||||
|     }, | ||||
|     colors: { | ||||
|       transparent: 'transparent', | ||||
| @@ -113,6 +113,7 @@ module.exports = { | ||||
|       body: '#2f3b49', | ||||
|     }, | ||||
|     keyframes: { | ||||
|       ...defaultTheme.keyframes, | ||||
|       wiggle: { | ||||
|         '0%': { transform: 'translateX(0)' }, | ||||
|         '15%': { transform: 'translateX(0.375rem)' }, | ||||
| @@ -125,6 +126,7 @@ module.exports = { | ||||
|       }, | ||||
|     }, | ||||
|     animation: { | ||||
|       ...defaultTheme.animation, | ||||
|       wiggle: 'wiggle 0.5s ease-in-out', | ||||
|     }, | ||||
|   }, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user