[Feature] Website live chat (#187)
Co-authored-by: Nithin David Thomas <webofnithin@gmail.com> Co-authored-by: Sojan Jose <sojan@pepalo.com>
This commit is contained in:
25
app/javascript/widget/App.vue
Executable file
25
app/javascript/widget/App.vue
Executable file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<div id="app" class="woot-widget-wrap">
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
|
||||
methods: {
|
||||
...mapActions('conversation', ['fetchOldConversations']),
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.fetchOldConversations();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~widget/assets/scss/woot.scss';
|
||||
</style>
|
||||
12
app/javascript/widget/api/auth.js
Executable file
12
app/javascript/widget/api/auth.js
Executable file
@@ -0,0 +1,12 @@
|
||||
import authEndPoint from 'widget/api/endPoints';
|
||||
import { API } from 'widget/helpers/axios';
|
||||
|
||||
const createContact = async (inboxId, accountId) => {
|
||||
const urlData = authEndPoint.createContact(inboxId, accountId);
|
||||
const result = await API.post(urlData.url, urlData.params);
|
||||
return result;
|
||||
};
|
||||
|
||||
export default {
|
||||
createContact,
|
||||
};
|
||||
16
app/javascript/widget/api/conversation.js
Executable file
16
app/javascript/widget/api/conversation.js
Executable file
@@ -0,0 +1,16 @@
|
||||
import endPoints from 'widget/api/endPoints';
|
||||
import { API } from 'widget/helpers/axios';
|
||||
|
||||
const sendMessageAPI = async content => {
|
||||
const urlData = endPoints.sendMessage(content);
|
||||
const result = await API.post(urlData.url, urlData.params);
|
||||
return result;
|
||||
};
|
||||
|
||||
const getConversationAPI = async conversationId => {
|
||||
const urlData = endPoints.getConversation(conversationId);
|
||||
const result = await API.get(urlData.url);
|
||||
return result;
|
||||
};
|
||||
|
||||
export { sendMessageAPI, getConversationAPI };
|
||||
17
app/javascript/widget/api/endPoints.js
Executable file
17
app/javascript/widget/api/endPoints.js
Executable file
@@ -0,0 +1,17 @@
|
||||
const sendMessage = content => ({
|
||||
url: `/api/v1/widget/messages${window.location.search}`,
|
||||
params: {
|
||||
message: {
|
||||
content,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const getConversation = () => ({
|
||||
url: `/api/v1/widget/messages${window.location.search}`,
|
||||
});
|
||||
|
||||
export default {
|
||||
sendMessage,
|
||||
getConversation,
|
||||
};
|
||||
BIN
app/javascript/widget/assets/images/defaultUser.png
Executable file
BIN
app/javascript/widget/assets/images/defaultUser.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
11
app/javascript/widget/assets/images/message-send.svg
Executable file
11
app/javascript/widget/assets/images/message-send.svg
Executable file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="21px" height="21px" viewBox="0 0 21 21" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 53 (72520) - https://sketchapp.com -->
|
||||
<title>Untitled</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="message-send" fill="#FFFFFF" fill-rule="nonzero">
|
||||
<path d="M18.34,7.32 L4.34,0.32 C3.20803579,-0.243393454 1.84434515,-0.0365739638 0.930331262,0.837115781 C0.0163173744,1.71080553 -0.251780361,3.06378375 0.26,4.22 L2.66,9.59 L2.66,9.59 C2.77000426,9.8522654 2.77000426,10.1477346 2.66,10.41 L0.26,15.78 C-0.153051509,16.7079201 -0.0685371519,17.7818234 0.48458191,18.6337075 C1.03770097,19.4855916 1.98429967,19.9997529 3,20 C3.46823099,19.9953274 3.9294892,19.8859921 4.35,19.68 L18.35,12.68 C19.3627539,12.1705304 20.001816,11.1336797 20.001816,10 C20.001816,8.86632027 19.3627539,7.82946961 18.35,7.32 L18.34,7.32 Z M17.45,10.89 L3.45,17.89 C3.07351737,18.0707705 2.62434212,17.9985396 2.32351279,17.7088521 C2.02268345,17.4191646 1.93356002,16.9730338 2.1,16.59 L4.49,11.22 C4.5209392,11.1482915 4.54765161,11.0748324 4.57,11 L11.46,11 C12.0122847,11 12.46,10.5522847 12.46,10 C12.46,9.44771525 12.0122847,9 11.46,9 L4.57,9 C4.54765161,8.9251676 4.5209392,8.85170847 4.49,8.78 L2.1,3.41 C1.93356002,3.02696622 2.02268345,2.5808354 2.32351279,2.2911479 C2.62434212,2.00146039 3.07351737,1.92922952 3.45,2.11 L17.45,9.11 C17.7839662,9.28109597 17.9940395,9.62475706 17.9940395,10 C17.9940395,10.3752429 17.7839662,10.718904 17.45,10.89 Z" id="Shape"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
BIN
app/javascript/widget/assets/images/send.png
Executable file
BIN
app/javascript/widget/assets/images/send.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
57
app/javascript/widget/assets/scss/_buttons.scss
Executable file
57
app/javascript/widget/assets/scss/_buttons.scss
Executable file
@@ -0,0 +1,57 @@
|
||||
$button-border-width: 1px;
|
||||
// Buttons
|
||||
.button {
|
||||
appearance: none;
|
||||
background: $color-primary;
|
||||
border: $button-border-width solid $color-primary;
|
||||
border-radius: $border-radius;
|
||||
color: $color-white;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: $font-size-default;
|
||||
height: $space-two * 2;
|
||||
line-height: $line-height;
|
||||
outline: none;
|
||||
padding: $space-smaller $space-normal;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
transition: background .2s, border .2s, box-shadow .2s, color .2s;
|
||||
user-select: none;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
background: lighten($color-primary, 7%);
|
||||
border-color: $color-primary;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:active,
|
||||
&.active {
|
||||
background: $color-primary;
|
||||
border-color: darken($color-primary, 5%);
|
||||
color: lighten($color-primary, 20%);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&[disabled],
|
||||
&:disabled,
|
||||
&.disabled {
|
||||
cursor: default;
|
||||
opacity: .5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.small {
|
||||
font-size: $font-size-small;
|
||||
height: $space-medium;
|
||||
padding: $space-smaller $space-slab;
|
||||
}
|
||||
|
||||
&.large {
|
||||
font-size: $font-size-medium;
|
||||
height: $space-larger;
|
||||
padding: $space-small $space-medium;
|
||||
}
|
||||
}
|
||||
71
app/javascript/widget/assets/scss/_forms.scss
Executable file
71
app/javascript/widget/assets/scss/_forms.scss
Executable file
@@ -0,0 +1,71 @@
|
||||
// scss-lint:disable PropertySortOrder DeclarationOrder QualifyingElement
|
||||
$form-border-width: 1px;
|
||||
$input-height: $space-two * 2;
|
||||
|
||||
.form-input {
|
||||
@include placeholder {
|
||||
color: $color-gray;
|
||||
}
|
||||
|
||||
appearance: none;
|
||||
background: $color-white;
|
||||
border: $form-border-width solid $color-border;
|
||||
border-radius: $border-radius;
|
||||
box-sizing: border-box;
|
||||
color: $color-body;
|
||||
display: block;
|
||||
font-size: $font-size-default;
|
||||
height: $input-height;
|
||||
line-height: 1.3;
|
||||
max-width: 100%;
|
||||
outline: none;
|
||||
padding: $space-small $space-slab;
|
||||
position: relative;
|
||||
transition: background .2s, border .2s, box-shadow .2s, color .2s;
|
||||
width: 100%;
|
||||
|
||||
&:focus {
|
||||
border-color: $color-primary;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: $color-gray;
|
||||
}
|
||||
|
||||
// Input sizes
|
||||
&.small {
|
||||
font-size: $font-size-small;
|
||||
height: $space-large;
|
||||
padding: $space-small $space-slab;
|
||||
}
|
||||
|
||||
&.large {
|
||||
font-size: $font-size-medium;
|
||||
height: $space-larger;
|
||||
padding: $space-slab $space-two;
|
||||
}
|
||||
|
||||
&.input-inline {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
// Input types
|
||||
&[type="file"] {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// Form element: Textarea
|
||||
textarea.form-input {
|
||||
@include placeholder {
|
||||
color: $color-light-gray;
|
||||
}
|
||||
|
||||
&,
|
||||
&.large,
|
||||
&.small {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
20
app/javascript/widget/assets/scss/_mixins.scss
Executable file
20
app/javascript/widget/assets/scss/_mixins.scss
Executable file
@@ -0,0 +1,20 @@
|
||||
// scss-lint:disable PseudoElement SpaceBeforeBrace VendorPrefix
|
||||
$shadow-color-1: rgba(50, 50, 93, 0.2);
|
||||
$shadow-color-2: rgba(0, 0, 0, 0.07);
|
||||
$shadow-color-3: rgba(50, 50, 93, .08);
|
||||
$shadow-color-4: rgba(0, 0, 0, .05);
|
||||
|
||||
@mixin normal-shadow {
|
||||
box-shadow: 0 $space-small $space-normal $shadow-color-1, 0 $space-smaller $space-slab $shadow-color-2;
|
||||
}
|
||||
|
||||
@mixin light-shadow {
|
||||
box-shadow: 0 $space-smaller 6px $shadow-color-3, 0 1px 3px $shadow-color-4;
|
||||
}
|
||||
|
||||
@mixin placeholder {
|
||||
&::-webkit-input-placeholder {@content}
|
||||
&:-moz-placeholder {@content}
|
||||
&::-moz-placeholder {@content}
|
||||
&:-ms-input-placeholder {@content}
|
||||
}
|
||||
54
app/javascript/widget/assets/scss/_reset.scss
Executable file
54
app/javascript/widget/assets/scss/_reset.scss
Executable file
@@ -0,0 +1,54 @@
|
||||
// scss-lint:disable
|
||||
/* http://meyerweb.com/eric/tools/css/reset/
|
||||
v2.0 | 20110126
|
||||
License: none (public domain)
|
||||
*/
|
||||
|
||||
html, body, div, span, applet, object, iframe,
|
||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||
a, abbr, acronym, address, big, cite, code,
|
||||
del, dfn, em, img, ins, kbd, q, s, samp,
|
||||
small, strike, strong, sub, sup, tt, var,
|
||||
b, u, i, center,
|
||||
dl, dt, dd, ol, ul, li,
|
||||
fieldset, form, label, legend,
|
||||
table, caption, tbody, tfoot, thead, tr, th, td,
|
||||
article, aside, canvas, details, embed,
|
||||
figure, figcaption, footer, header, hgroup,
|
||||
menu, nav, output, ruby, section, summary,
|
||||
time, mark, audio, video {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/* HTML5 display-role reset for older browsers */
|
||||
article, aside, details, figcaption, figure,
|
||||
footer, header, hgroup, menu, nav, section {
|
||||
display: block;
|
||||
}
|
||||
|
||||
body {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
ol, ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
blockquote, q {
|
||||
quotes: none;
|
||||
}
|
||||
|
||||
blockquote:before, blockquote:after,
|
||||
q:before, q:after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
90
app/javascript/widget/assets/scss/_variables.scss
Executable file
90
app/javascript/widget/assets/scss/_variables.scss
Executable file
@@ -0,0 +1,90 @@
|
||||
// Font sizes
|
||||
$font-size-nano: 0.8rem;
|
||||
$font-size-micro: 0.8rem;
|
||||
$font-size-mini: 1rem;
|
||||
$font-size-small: 1.2rem;
|
||||
$font-size-default: 1.4rem;
|
||||
$font-size-medium: 1.6rem;
|
||||
$font-size-large: 2rem;
|
||||
$font-size-big: 2.4rem;
|
||||
$font-size-bigger: 3.2rem;
|
||||
$font-size-mega: 4rem;
|
||||
$font-size-giga: 5.6rem;
|
||||
|
||||
// spaces
|
||||
$zero: 0;
|
||||
$space-micro: 0.2rem;
|
||||
$space-smaller: 0.4rem;
|
||||
$space-small: 0.8rem;
|
||||
$space-one: 1rem;
|
||||
$space-slab: 1.2rem;
|
||||
$space-normal: 1.6rem;
|
||||
$space-two: 2rem;
|
||||
$space-medium: 2.4rem;
|
||||
$space-large: 3.2rem;
|
||||
$space-larger: 4.8rem;
|
||||
$space-big: 6.4rem;
|
||||
$space-jumbo: 8rem;
|
||||
$space-mega: 10rem;
|
||||
|
||||
// font-weight
|
||||
$font-weight-feather: 100;
|
||||
$font-weight-light: 300;
|
||||
$font-weight-normal: 400;
|
||||
$font-weight-medium: 500;
|
||||
$font-weight-bold: 600;
|
||||
$font-weight-black: 700;
|
||||
|
||||
//Navbar
|
||||
$nav-bar-width: 23rem;
|
||||
$header-height: 5.6rem;
|
||||
|
||||
// Woot Logo
|
||||
$woot-logo-width: 20rem;
|
||||
$woot-logo-height: 8rem;
|
||||
$woot-logo-padding: $space-large $space-large $space-large $space-large;
|
||||
|
||||
// Colors
|
||||
$color-woot: #1f93ff;
|
||||
$color-primary: $color-woot;
|
||||
$color-gray: #6e6f73;
|
||||
$color-light-gray: #999a9b;
|
||||
$color-border: #e0e6ed;
|
||||
$color-border-transparent: rgba(224, 230, 237, 0.5);
|
||||
$color-border-light: #f0f4f5;
|
||||
$color-background: #ecf3f9;
|
||||
$color-background-light: #fafafa;
|
||||
$color-white: #fff;
|
||||
$color-body: #3c4858;
|
||||
$color-heading: #1f2d3d;
|
||||
$color-modal-header: #f1f1f1;
|
||||
// Thumbnail
|
||||
$thumbnail-radius: 4rem;
|
||||
|
||||
// chat-header
|
||||
$conv-header-height: 4rem;
|
||||
|
||||
// login
|
||||
|
||||
// Inbox List
|
||||
|
||||
$inbox-thumb-size: 4.8rem;
|
||||
|
||||
|
||||
// Spinner
|
||||
$spinkit-spinner-color: $color-white !default;
|
||||
$spinkit-spinner-margin: 0 0 0 1.6rem !default;
|
||||
$spinkit-size: 1.6rem !default;
|
||||
|
||||
// Snackbar default
|
||||
$woot-snackbar-bg: #323232;
|
||||
$woot-snackbar-button: #ffeb3b;
|
||||
|
||||
$swift-ease-out-duration: .4s !default;
|
||||
$swift-ease-out-timing-function: cubic-bezier(.25, .8, .25, 1) !default;
|
||||
$swift-ease-out: all $swift-ease-out-duration $swift-ease-out-timing-function !default;
|
||||
|
||||
$border-radius: 3px;
|
||||
$line-height: 1;
|
||||
$footer-height: 11.2rem;
|
||||
$header-expanded-height: $space-medium * 10;
|
||||
65
app/javascript/widget/assets/scss/sdk.css
Normal file
65
app/javascript/widget/assets/scss/sdk.css
Normal file
@@ -0,0 +1,65 @@
|
||||
.woot-widget-holder {
|
||||
z-index: 2147483000!important;
|
||||
position: fixed!important;
|
||||
bottom: 104px;
|
||||
right: 20px;
|
||||
height: calc(85% - 64px - 20px);
|
||||
width: 370px!important;
|
||||
min-height: 250px!important;
|
||||
max-height: 590px!important;
|
||||
-moz-box-shadow: 0 5px 40px rgba(0,0,0,.16)!important;
|
||||
-o-box-shadow: 0 5px 40px rgba(0,0,0,.16)!important;
|
||||
-webkit-box-shadow: 0 5px 40px rgba(0,0,0,.16)!important;
|
||||
box-shadow: 0 5px 40px rgba(0,0,0,.16)!important;
|
||||
-o-border-radius: 8px!important;
|
||||
-moz-border-radius: 8px!important;
|
||||
-webkit-border-radius: 8px!important;
|
||||
border-radius: 8px!important;
|
||||
overflow: hidden!important;
|
||||
opacity: 1!important;
|
||||
}
|
||||
|
||||
.woot-widget-holder iframe { width: 100% !important; height: 100% !important; border: 0; }
|
||||
|
||||
.woot-widget-bubble {
|
||||
z-index: 2147483000!important;
|
||||
-moz-box-shadow: 0 8px 24px rgba(0,0,0,.16)!important;
|
||||
-o-box-shadow: 0 8px 24px rgba(0,0,0,.16)!important;
|
||||
-webkit-box-shadow: 0 8px 24px rgba(0,0,0,.16)!important;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,.16)!important;
|
||||
-o-border-radius: 100px!important;
|
||||
-moz-border-radius: 100px!important;
|
||||
-webkit-border-radius: 100px!important;
|
||||
border-radius: 100px!important;
|
||||
background: #1f93ff;
|
||||
position: fixed;
|
||||
cursor: pointer;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
width: 64px!important;
|
||||
height: 64px!important;
|
||||
}
|
||||
|
||||
.woot-widget-bubble:hover {
|
||||
background: #1f93ff;
|
||||
-moz-box-shadow: 0 8px 32px rgba(0,0,0,.4)!important;
|
||||
-o-box-shadow: 0 8px 32px rgba(0,0,0,.4)!important;
|
||||
-webkit-box-shadow: 0 8px 32px rgba(0,0,0,.4)!important;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,.4)!important;
|
||||
}
|
||||
|
||||
.woot-widget-bubble img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.woot-widget-bubble.woot--close img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 24px;
|
||||
}
|
||||
|
||||
.woot--hide {
|
||||
display: none !important;
|
||||
}
|
||||
16
app/javascript/widget/assets/scss/woot.scss
Executable file
16
app/javascript/widget/assets/scss/woot.scss
Executable file
@@ -0,0 +1,16 @@
|
||||
@import 'variables';
|
||||
@import 'buttons';
|
||||
@import 'mixins';
|
||||
@import 'forms';
|
||||
@import 'reset';
|
||||
|
||||
html,
|
||||
body {
|
||||
font-family: -apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
|
||||
font-size: 10px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.woot-widget-wrap {
|
||||
height: 100%;
|
||||
}
|
||||
83
app/javascript/widget/components/AgentMessage.vue
Executable file
83
app/javascript/widget/components/AgentMessage.vue
Executable file
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<div class="agent-message">
|
||||
<div class="avatar-wrap">
|
||||
<UserAvatar size="small" :src="avatarUrl" />
|
||||
</div>
|
||||
<div class="message-wrap">
|
||||
<h5 class="agent-name">
|
||||
{{ agentName }}
|
||||
</h5>
|
||||
<AgentMessageBubble :message="message" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import UserAvatar from 'widget/components/UserAvatar.vue';
|
||||
import AgentMessageBubble from 'widget/components/AgentMessageBubble.vue';
|
||||
|
||||
export default {
|
||||
name: 'AgentMessage',
|
||||
components: {
|
||||
UserAvatar,
|
||||
AgentMessageBubble,
|
||||
},
|
||||
props: {
|
||||
message: String,
|
||||
avatarUrl: String,
|
||||
agentName: String,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped lang="scss">
|
||||
@import '~widget/assets/scss/variables.scss';
|
||||
|
||||
.agent-message {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-end;
|
||||
margin: 0 $space-smaller $space-micro auto;
|
||||
|
||||
& + .agent-message {
|
||||
margin-bottom: $space-micro;
|
||||
|
||||
.chat-bubble {
|
||||
border-top-left-radius: $space-smaller;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.agent-name {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
& + .user-message {
|
||||
margin-bottom: $space-normal;
|
||||
}
|
||||
|
||||
.avatar-wrap {
|
||||
flex-shrink: 1;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.message-wrap {
|
||||
max-width: 90%;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 1;
|
||||
margin-left: $space-small;
|
||||
|
||||
.agent-name {
|
||||
font-weight: $font-weight-medium;
|
||||
margin-bottom: $space-smaller;
|
||||
margin-left: $space-two;
|
||||
color: $color-body;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
27
app/javascript/widget/components/AgentMessageBubble.vue
Executable file
27
app/javascript/widget/components/AgentMessageBubble.vue
Executable file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div class="chat-bubble agent">
|
||||
{{ message }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'AgentMessageBubble',
|
||||
props: {
|
||||
message: String,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style lang="scss">
|
||||
@import '~widget/assets/scss/variables.scss';
|
||||
|
||||
.chat-bubble {
|
||||
&.agent {
|
||||
background: $color-white;
|
||||
border-bottom-left-radius: $space-smaller;
|
||||
color: $color-body;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
33
app/javascript/widget/components/ChatFooter.vue
Executable file
33
app/javascript/widget/components/ChatFooter.vue
Executable file
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<footer class="footer">
|
||||
<ChatInputWrap :on-send-message="onSendMessage" />
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ChatInputWrap from 'widget/components/ChatInputWrap.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ChatInputWrap,
|
||||
},
|
||||
props: {
|
||||
msg: String,
|
||||
onSendMessage: Function,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped lang="scss">
|
||||
@import '~widget/assets/scss/variables.scss';
|
||||
|
||||
.footer {
|
||||
background: $color-white;
|
||||
box-shadow: 0 -$space-micro 3px rgba(50, 50, 93, 0.04),
|
||||
0 -1px 2px rgba(0, 0, 0, 0.03);
|
||||
box-sizing: border-box;
|
||||
padding: $space-small;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
51
app/javascript/widget/components/ChatHeaderExpanded.vue
Executable file
51
app/javascript/widget/components/ChatHeaderExpanded.vue
Executable file
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<header class="header-expanded">
|
||||
<div>
|
||||
<h2 class="title">
|
||||
{{ introHeading }}
|
||||
</h2>
|
||||
<p class="body">
|
||||
{{ introBody }}
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ChatHeaderExpanded',
|
||||
props: {
|
||||
introHeading: {
|
||||
type: String,
|
||||
default: 'Hi there ! 🙌🏼',
|
||||
},
|
||||
introBody: {
|
||||
type: String,
|
||||
default:
|
||||
'We make it simple to connect with us. Ask us anything, or share your feedback.',
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~widget/assets/scss/variables.scss';
|
||||
|
||||
.header-expanded {
|
||||
background: $color-woot;
|
||||
padding: $space-large;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
color: $color-white;
|
||||
|
||||
.title {
|
||||
font-size: $font-size-mega;
|
||||
margin-bottom: $space-two;
|
||||
}
|
||||
|
||||
.body {
|
||||
font-size: $font-size-medium;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
29
app/javascript/widget/components/ChatInputArea.vue
Executable file
29
app/javascript/widget/components/ChatInputArea.vue
Executable file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<textarea
|
||||
class="form-input user-message-input"
|
||||
:placeholder="placeholder"
|
||||
:value="value"
|
||||
@input="$emit('input', $event.target.value)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
placeholder: String,
|
||||
value: String,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped lang="scss">
|
||||
@import '~widget/assets/scss/variables.scss';
|
||||
|
||||
.user-message-input {
|
||||
border-color: $color-white;
|
||||
border-bottom-color: $color-border-light;
|
||||
height: $space-big;
|
||||
resize: none;
|
||||
}
|
||||
</style>
|
||||
65
app/javascript/widget/components/ChatInputWrap.vue
Executable file
65
app/javascript/widget/components/ChatInputWrap.vue
Executable file
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div class="input-wrap">
|
||||
<div>
|
||||
<ChatInputArea v-model="userInput" :placeholder="placeholder" />
|
||||
</div>
|
||||
<div class="message-button-wrap">
|
||||
<ChatSendButton
|
||||
:on-click="handleButtonClick"
|
||||
:disabled="!userInput.length"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ChatSendButton from 'widget/components/ChatSendButton.vue';
|
||||
import ChatInputArea from 'widget/components/ChatInputArea.vue';
|
||||
|
||||
export default {
|
||||
name: 'ChatInputWrap',
|
||||
components: {
|
||||
ChatSendButton,
|
||||
ChatInputArea,
|
||||
},
|
||||
|
||||
props: {
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: 'Type your message',
|
||||
},
|
||||
onSendMessage: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
userInput: '',
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
handleButtonClick() {
|
||||
if (this.userInput) {
|
||||
this.onSendMessage(this.userInput);
|
||||
}
|
||||
this.userInput = '';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~widget/assets/scss/variables.scss';
|
||||
|
||||
.input-wrap {
|
||||
.message-button-wrap {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
margin-top: $space-small;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
38
app/javascript/widget/components/ChatMessage.vue
Executable file
38
app/javascript/widget/components/ChatMessage.vue
Executable file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<UserMessage v-if="isUserMessage" :message="message.content" />
|
||||
<AgentMessage
|
||||
v-else
|
||||
:agent-name="message.sender_name"
|
||||
:message="message.content"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AgentMessage from 'widget/components/AgentMessage.vue';
|
||||
import UserMessage from 'widget/components/UserMessage.vue';
|
||||
import { MESSAGE_TYPE } from 'widget/helpers/constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AgentMessage,
|
||||
UserMessage,
|
||||
},
|
||||
props: {
|
||||
message: Object,
|
||||
},
|
||||
computed: {
|
||||
isUserMessage() {
|
||||
return this.message.message_type === MESSAGE_TYPE.INCOMING;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.message-wrap {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
max-width: 90%;
|
||||
}
|
||||
</style>
|
||||
63
app/javascript/widget/components/ChatSendButton.vue
Executable file
63
app/javascript/widget/components/ChatSendButton.vue
Executable file
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="disabled"
|
||||
class="button send-button"
|
||||
@click="onClick"
|
||||
>
|
||||
<span v-if="!loading" class="icon-holder">
|
||||
<img src="~widget/assets/images/message-send.svg" />
|
||||
<span>Send</span>
|
||||
</span>
|
||||
<spinner v-else size="small" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Spinner from 'widget/components/Spinner.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Spinner,
|
||||
},
|
||||
props: {
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
onClick: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped lang="scss">
|
||||
@import '~widget/assets/scss/variables.scss';
|
||||
|
||||
.send-button {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
min-width: $space-big;
|
||||
position: relative;
|
||||
|
||||
.icon-holder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
fill: $color-white;
|
||||
font-weight: $font-weight-medium;
|
||||
|
||||
img {
|
||||
margin-right: $space-small;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
33
app/javascript/widget/components/ConversationWrap.vue
Executable file
33
app/javascript/widget/components/ConversationWrap.vue
Executable file
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<section class="conversation">
|
||||
<ChatMessage
|
||||
v-for="message in messages"
|
||||
:key="message.id"
|
||||
:message="message"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ChatMessage from 'widget/components/ChatMessage.vue';
|
||||
|
||||
export default {
|
||||
name: 'ConversationWrap',
|
||||
components: {
|
||||
ChatMessage,
|
||||
},
|
||||
props: {
|
||||
messages: Object,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped lang="scss">
|
||||
@import '~widget/assets/scss/variables.scss';
|
||||
|
||||
.conversation {
|
||||
height: 100%;
|
||||
padding: $space-large $space-small $space-large $space-normal;
|
||||
}
|
||||
</style>
|
||||
31
app/javascript/widget/components/HelloWorld.vue
Executable file
31
app/javascript/widget/components/HelloWorld.vue
Executable file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div class="hello">
|
||||
<h1>{{ msg }}</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
msg: String,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped lang="scss">
|
||||
h3 {
|
||||
margin: 40px 0 0;
|
||||
}
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
li {
|
||||
display: inline-block;
|
||||
margin: 0 10px;
|
||||
}
|
||||
a {
|
||||
color: #42b983;
|
||||
}
|
||||
</style>
|
||||
52
app/javascript/widget/components/Spinner.vue
Executable file
52
app/javascript/widget/components/Spinner.vue
Executable file
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<span class="spinner" :class="size"></span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const SIZES = ['small', 'medium', 'large'];
|
||||
|
||||
export default {
|
||||
props: {
|
||||
size: {
|
||||
validator: value => SIZES.includes(value),
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped lang="scss">
|
||||
@import '~widget/assets/scss/variables.scss';
|
||||
|
||||
.spinner {
|
||||
@keyframes spinner {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
&:before {
|
||||
animation: spinner 0.7s linear infinite;
|
||||
border-radius: 50%;
|
||||
border-top-color: lighten($color-woot, 10%);
|
||||
border: 2px solid rgba(255, 255, 255, 0.7);
|
||||
box-sizing: border-box;
|
||||
content: '';
|
||||
height: $space-medium;
|
||||
left: 50%;
|
||||
margin-left: -$space-slab;
|
||||
margin-top: -$space-slab;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: $space-medium;
|
||||
}
|
||||
|
||||
&.small:before {
|
||||
border-width: 1px;
|
||||
height: $space-slab;
|
||||
margin-left: -$space-slab/2;
|
||||
margin-top: -$space-slab/2;
|
||||
width: $space-slab;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
46
app/javascript/widget/components/UserAvatar.vue
Executable file
46
app/javascript/widget/components/UserAvatar.vue
Executable file
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div class="user-avatar" :class="size" :style="getBgImage"></div>
|
||||
</template>
|
||||
<script>
|
||||
/**
|
||||
* Thumbnail Component
|
||||
* Src - source for round image
|
||||
*/
|
||||
export default {
|
||||
name: 'UserAvatar',
|
||||
props: {
|
||||
src: {
|
||||
type: String,
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
getBgImage() {
|
||||
if (this.src) return { 'background-image': `url(${this.src})` };
|
||||
return {};
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~widget/assets/scss/variables.scss';
|
||||
@import '~widget/assets/scss/mixins.scss';
|
||||
|
||||
.user-avatar {
|
||||
@include light-shadow;
|
||||
background: url('~widget/assets/images/defaultUser.png') center center
|
||||
no-repeat;
|
||||
background-size: cover;
|
||||
border-radius: 50%;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
|
||||
&.small {
|
||||
width: $space-medium;
|
||||
height: $space-medium;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
55
app/javascript/widget/components/UserMessage.vue
Executable file
55
app/javascript/widget/components/UserMessage.vue
Executable file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div class="user-message">
|
||||
<div class="message-wrap">
|
||||
<UserMessageBubble :message="message" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import UserMessageBubble from 'widget/components/UserMessageBubble.vue';
|
||||
|
||||
export default {
|
||||
name: 'UserMessage',
|
||||
components: {
|
||||
UserMessageBubble,
|
||||
},
|
||||
props: {
|
||||
message: String,
|
||||
avatarUrl: String,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped lang="scss">
|
||||
@import '~widget/assets/scss/variables.scss';
|
||||
|
||||
.user-message {
|
||||
align-items: flex-end;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
margin: 0 $space-smaller $space-micro auto;
|
||||
text-align: right;
|
||||
|
||||
& + .user-message {
|
||||
margin-bottom: $space-micro;
|
||||
.chat-bubble {
|
||||
border-top-right-radius: $space-smaller;
|
||||
}
|
||||
.user-avatar {
|
||||
visibility: hidden;
|
||||
}
|
||||
.agent-name {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
& + .agent-message {
|
||||
margin-bottom: $space-normal;
|
||||
}
|
||||
.message-wrap {
|
||||
margin-right: $space-small;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
36
app/javascript/widget/components/UserMessageBubble.vue
Executable file
36
app/javascript/widget/components/UserMessageBubble.vue
Executable file
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div class="chat-bubble user">
|
||||
{{ message }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'UserMessageBubble',
|
||||
props: {
|
||||
message: String,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style lang="scss">
|
||||
@import '~widget/assets/scss/variables.scss';
|
||||
@import '~widget/assets/scss/mixins.scss';
|
||||
|
||||
.chat-bubble {
|
||||
@include light-shadow;
|
||||
background: $color-woot;
|
||||
border-radius: $space-two;
|
||||
color: $color-white;
|
||||
display: inline-block;
|
||||
font-size: $font-size-default;
|
||||
line-height: 1.5;
|
||||
max-width: 80%;
|
||||
padding: $space-small $space-two;
|
||||
|
||||
&.user {
|
||||
border-bottom-right-radius: $space-smaller;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
16
app/javascript/widget/helpers/actionCable.js
Normal file
16
app/javascript/widget/helpers/actionCable.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import BaseActionCableConnector from '../../shared/helpers/BaseActionCableConnector';
|
||||
|
||||
class ActionCableConnector extends BaseActionCableConnector {
|
||||
constructor(app, pubsubToken) {
|
||||
super(app, pubsubToken);
|
||||
this.events = {
|
||||
'message.created': this.onMessageCreated,
|
||||
};
|
||||
}
|
||||
|
||||
onMessageCreated = data => {
|
||||
this.app.$store.dispatch('conversation/addMessage', data);
|
||||
};
|
||||
}
|
||||
|
||||
export default ActionCableConnector;
|
||||
15
app/javascript/widget/helpers/axios.js
Executable file
15
app/javascript/widget/helpers/axios.js
Executable file
@@ -0,0 +1,15 @@
|
||||
import axios from 'axios';
|
||||
import { APP_BASE_URL } from 'widget/helpers/constants';
|
||||
|
||||
export const API = axios.create({
|
||||
baseURL: APP_BASE_URL,
|
||||
withCredentials: false,
|
||||
});
|
||||
|
||||
export const setHeader = (key, value) => {
|
||||
API.defaults.headers.common[key] = value;
|
||||
};
|
||||
|
||||
export const removeHeader = key => {
|
||||
delete API.defaults.headers.common[key];
|
||||
};
|
||||
12
app/javascript/widget/helpers/constants.js
Executable file
12
app/javascript/widget/helpers/constants.js
Executable file
@@ -0,0 +1,12 @@
|
||||
export const APP_BASE_URL = '';
|
||||
|
||||
export const MESSAGE_STATUS = {
|
||||
FAILED: 'failed',
|
||||
SUCCESS: 'success',
|
||||
PROGRESS: 'progress',
|
||||
};
|
||||
|
||||
export const MESSAGE_TYPE = {
|
||||
INCOMING: 0,
|
||||
OUTGOING: 1,
|
||||
};
|
||||
10
app/javascript/widget/helpers/utils.js
Executable file
10
app/javascript/widget/helpers/utils.js
Executable file
@@ -0,0 +1,10 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
export const isEmptyObject = obj =>
|
||||
Object.keys(obj).length === 0 && obj.constructor === Object;
|
||||
|
||||
export const arrayToHashById = array =>
|
||||
array.reduce((map, obj) => {
|
||||
const newMap = map;
|
||||
newMap[obj.id] = obj;
|
||||
return newMap;
|
||||
}, {});
|
||||
24
app/javascript/widget/router.js
Executable file
24
app/javascript/widget/router.js
Executable file
@@ -0,0 +1,24 @@
|
||||
import Vue from 'vue';
|
||||
import Router from 'vue-router';
|
||||
import Home from './views/Home.vue';
|
||||
|
||||
Vue.use(Router);
|
||||
|
||||
export default new Router({
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: Home,
|
||||
},
|
||||
// {
|
||||
// path: '/about',
|
||||
// name: 'about',
|
||||
// // route level code-splitting
|
||||
// // this generates a separate chunk (about.[hash].js) for this route
|
||||
// // which is lazy-loaded when the route is visited.
|
||||
// component: () =>
|
||||
// import(/* webpackChunkName: "about" */ './views/About.vue'),
|
||||
// },
|
||||
],
|
||||
});
|
||||
11
app/javascript/widget/store/index.js
Executable file
11
app/javascript/widget/store/index.js
Executable file
@@ -0,0 +1,11 @@
|
||||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import conversation from 'widget/store/modules/conversation';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
export default new Vuex.Store({
|
||||
modules: {
|
||||
conversation,
|
||||
},
|
||||
});
|
||||
60
app/javascript/widget/store/modules/conversation.js
Executable file
60
app/javascript/widget/store/modules/conversation.js
Executable file
@@ -0,0 +1,60 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import Vue from 'vue';
|
||||
import { sendMessageAPI, getConversationAPI } from 'widget/api/conversation';
|
||||
|
||||
export const DEFAULT_CONVERSATION = 'default';
|
||||
const state = {
|
||||
conversations: {},
|
||||
};
|
||||
|
||||
const getters = {
|
||||
getConversation: _state => _state.conversations,
|
||||
};
|
||||
|
||||
const actions = {
|
||||
sendMessage: async (_, params) => {
|
||||
const { content } = params;
|
||||
await sendMessageAPI(content);
|
||||
},
|
||||
|
||||
fetchOldConversations: async ({ commit }) => {
|
||||
try {
|
||||
const { data } = await getConversationAPI();
|
||||
commit('initMessagesInConversation', data);
|
||||
} catch (error) {
|
||||
// Handle error
|
||||
}
|
||||
},
|
||||
|
||||
addMessage({ commit }, data) {
|
||||
commit('pushMessageToConversations', data);
|
||||
},
|
||||
};
|
||||
|
||||
const mutations = {
|
||||
initInboxInConversations($state, lastConversation) {
|
||||
Vue.set($state.conversations, lastConversation, {});
|
||||
},
|
||||
|
||||
pushMessageToConversations($state, message) {
|
||||
const { id } = message;
|
||||
const messagesInbox = $state.conversations;
|
||||
Vue.set(messagesInbox, id, message);
|
||||
},
|
||||
|
||||
initMessagesInConversation(_state, payload) {
|
||||
if (!payload.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
payload.map(message => Vue.set(_state.conversations, message.id, message));
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations,
|
||||
};
|
||||
5
app/javascript/widget/views/About.vue
Executable file
5
app/javascript/widget/views/About.vue
Executable file
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div class="about">
|
||||
<h1>Chatwoot</h1>
|
||||
</div>
|
||||
</template>
|
||||
78
app/javascript/widget/views/Home.vue
Executable file
78
app/javascript/widget/views/Home.vue
Executable file
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div class="home">
|
||||
<div class="header-wrap">
|
||||
<ChatHeaderExpanded />
|
||||
</div>
|
||||
<div class="conversation-wrap">
|
||||
<ConversationWrap :messages="getConversation" />
|
||||
</div>
|
||||
<div class="footer-wrap">
|
||||
<ChatFooter :on-send-message="handleSendMessage" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
|
||||
// import { DEFAULT_CONVERSATION } from 'widget/store/modules/conversation';
|
||||
import ChatFooter from 'widget/components/ChatFooter.vue';
|
||||
import ChatHeaderExpanded from 'widget/components/ChatHeaderExpanded.vue';
|
||||
import ConversationWrap from 'widget/components/ConversationWrap.vue';
|
||||
|
||||
export default {
|
||||
name: 'Home',
|
||||
components: {
|
||||
ChatFooter,
|
||||
ChatHeaderExpanded,
|
||||
ConversationWrap,
|
||||
},
|
||||
methods: {
|
||||
...mapActions('conversation', ['sendMessage']),
|
||||
handleSendMessage(content) {
|
||||
this.sendMessage({
|
||||
content,
|
||||
});
|
||||
},
|
||||
scrollToBottom() {
|
||||
const container = this.$el.querySelector('.conversation-wrap');
|
||||
container.scrollTop = container.scrollHeight;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('conversation', ['getConversation']),
|
||||
},
|
||||
mounted() {
|
||||
this.scrollToBottom();
|
||||
},
|
||||
updated() {
|
||||
this.scrollToBottom();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~widget/assets/scss/woot.scss';
|
||||
|
||||
.home {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
background: $color-background;
|
||||
|
||||
.header-wrap {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.conversation-wrap {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.footer-wrap {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user