[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:
parent
a4114288f3
commit
16fe912fbd
80 changed files with 2040 additions and 106 deletions
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>
|
Loading…
Add table
Add a link
Reference in a new issue