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 { |   .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> |   <category-card | ||||||
|     <h2 class="text-base font-bold leading-6 text-slate-800 mb-0"> |     :title="$t('PORTAL.POPULAR_ARTICLES')" | ||||||
|       {{ $t('PORTAL.POPULAR_ARTICLES') }} |     :articles="articles.slice(0, 4)" | ||||||
|     </h2> |     @view-all="$emit('view-all')" | ||||||
|     <category-card |     @view="onArticleClick" | ||||||
|       :articles="articles.slice(0, 4)" |   /> | ||||||
|       @view-all-articles="$emit('view-all-articles')" |  | ||||||
|     /> |  | ||||||
|   </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: { | ||||||
|   computed: { |       type: Boolean, | ||||||
|     ...mapGetters({ |       default: true, | ||||||
|       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 class="px-4 pt-2 w-full sticky bottom-4"> | ||||||
|  |       <team-availability | ||||||
|  |         :available-agents="availableAgents" | ||||||
|  |         :has-conversation="!!conversationSize" | ||||||
|  |         @start-conversation="startConversation" | ||||||
|  |       /> | ||||||
|     </div> |     </div> | ||||||
|     <team-availability |  | ||||||
|       :available-agents="availableAgents" |  | ||||||
|       :has-conversation="!!conversationSize" |  | ||||||
|       @start-conversation="startConversation" |  | ||||||
|     /> |  | ||||||
|   </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