@defra/flood-webchat 0.0.1-beta.3 → 0.0.1-beta.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -1
- package/dist/client.js +1876 -215
- package/dist/client.js.map +1 -1
- package/dist/server.js +181 -240
- package/dist/server.js.map +1 -1
- package/main.scss +11 -6
- package/package.json +6 -2
- package/src/client/components/availability/availability.jsx +59 -49
- package/src/client/components/availability/availability.scss +7 -33
- package/src/client/components/chat/chat.jsx +176 -0
- package/src/client/components/chat/chat.scss +221 -0
- package/src/client/components/errorSummary/error-summary.jsx +34 -0
- package/src/client/components/message/message.jsx +20 -0
- package/src/client/components/panel/panel-footer.jsx +9 -0
- package/src/client/components/panel/panel-header.jsx +75 -0
- package/src/client/components/panel/panel.jsx +144 -0
- package/src/client/components/panel/panel.scss +112 -0
- package/src/client/components/screens/end-chat.jsx +39 -0
- package/src/client/components/screens/feedback.jsx +23 -0
- package/src/client/components/screens/pre-chat.jsx +62 -0
- package/src/client/components/screens/request-chat.jsx +173 -0
- package/src/client/components/screens/settings.jsx +69 -0
- package/src/client/components/screens/unavailable.jsx +23 -0
- package/src/client/hooks/useFocusedElements.js +81 -0
- package/src/client/hooks/useTextareaAutosize.js +13 -0
- package/src/client/index.jsx +15 -1
- package/src/client/lib/check-availability.js +1 -0
- package/src/client/lib/history.js +13 -0
- package/src/client/lib/message-notification.js +36 -0
- package/src/client/lib/transform-messages.js +34 -0
- package/src/client/scss/_objects.scss +33 -0
- package/src/client/scss/_settings.scss +75 -0
- package/src/client/scss/_tools.scss +33 -0
- package/src/client/scss/_utilities.scss +25 -0
- package/src/client/store/AppProvider.jsx +174 -0
- package/src/client/store/actions-map.js +135 -0
- package/src/client/store/constants.js +3 -0
- package/src/client/store/reducer.js +30 -0
- package/src/client/store/useApp.js +5 -0
- package/src/client/store/useChatSdk.js +37 -0
- package/src/server/index.js +15 -6
- package/src/server/lib/client.js +31 -35
- package/src/server/lib/utils.js +18 -9
- package/src/client/lib/external-stores.js +0 -4
- package/src/client/lib/external-sync-store.js +0 -42
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
@keyframes blink {
|
|
2
|
+
50% {
|
|
3
|
+
fill: transparent
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.wc-status {
|
|
8
|
+
display: flex;
|
|
9
|
+
position: relative;
|
|
10
|
+
padding: 15px;
|
|
11
|
+
@include mq ($from: tablet) {
|
|
12
|
+
padding: 10px;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
&::after {
|
|
16
|
+
position: absolute;
|
|
17
|
+
content: "";
|
|
18
|
+
bottom: 0;
|
|
19
|
+
left: 15px;
|
|
20
|
+
right: 15px;
|
|
21
|
+
height: 0;
|
|
22
|
+
border-bottom: 1px solid $govuk-border-colour;
|
|
23
|
+
@include mq ($from: tablet) {
|
|
24
|
+
left: 10px;
|
|
25
|
+
right: 10px;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
&__availability {
|
|
30
|
+
font-size: 16px;
|
|
31
|
+
margin-bottom: 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
&__link {
|
|
35
|
+
@extend %wc-link-button;
|
|
36
|
+
margin-left: auto;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.wc-body[tabindex="0"]:focus-visible {
|
|
41
|
+
outline: 3px solid transparent;
|
|
42
|
+
outline-offset: -2px;
|
|
43
|
+
box-shadow:
|
|
44
|
+
inset 0 0 0 2px $govuk-focus-colour,
|
|
45
|
+
inset 0 0 0 4px govuk-colour('black'),
|
|
46
|
+
0 0 0 1px $govuk-focus-colour;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.wc-chat {
|
|
50
|
+
@extend %govuk-body-s;
|
|
51
|
+
line-height: 1.25;
|
|
52
|
+
font-size: 14px;
|
|
53
|
+
overflow: auto;
|
|
54
|
+
list-style: none;
|
|
55
|
+
padding: 0;
|
|
56
|
+
|
|
57
|
+
&__message {
|
|
58
|
+
margin: 5px 15px 0;
|
|
59
|
+
@include mq ($from: tablet) {
|
|
60
|
+
margin-left: 10px;
|
|
61
|
+
margin-right: 10px;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
&:first-child {
|
|
65
|
+
padding-top: 15px;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
&.inbound {
|
|
69
|
+
text-align: right;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
&__from {
|
|
74
|
+
line-height: 1.25;
|
|
75
|
+
color: govuk-colour('dark-grey');
|
|
76
|
+
margin-top: 2px;
|
|
77
|
+
margin-bottom: 2px;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
&__text {
|
|
81
|
+
font-size: 16px;
|
|
82
|
+
padding: 10px 12.5px;
|
|
83
|
+
margin-bottom: 0;
|
|
84
|
+
display: inline-block;
|
|
85
|
+
|
|
86
|
+
&.inbound {
|
|
87
|
+
color: govuk-colour('white');
|
|
88
|
+
background-color: govuk-colour('blue');
|
|
89
|
+
border-radius: 10px 0 10px 10px;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
&.outbound {
|
|
93
|
+
background-color: govuk-colour('light-grey');
|
|
94
|
+
border-radius: 0 10px 10px;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
svg {
|
|
98
|
+
display: block;
|
|
99
|
+
color: govuk-colour('dark-grey');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
svg circle {
|
|
103
|
+
animation: 1s blink infinite;
|
|
104
|
+
fill: currentcolor;
|
|
105
|
+
|
|
106
|
+
&:nth-child(2) {
|
|
107
|
+
animation-delay: 250ms
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
&:nth-child(3) {
|
|
111
|
+
animation-delay: 500ms
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.wc-form {
|
|
118
|
+
position: relative;
|
|
119
|
+
display: flex;
|
|
120
|
+
flex-wrap: nowrap;
|
|
121
|
+
align-items: baseline;
|
|
122
|
+
border-top: 1px solid govuk-colour('black');
|
|
123
|
+
border-bottom: 1px solid govuk-colour('black');
|
|
124
|
+
|
|
125
|
+
&__label {
|
|
126
|
+
position: absolute;
|
|
127
|
+
pointer-events: none;
|
|
128
|
+
font-size: 16px;
|
|
129
|
+
line-height: 20px;
|
|
130
|
+
margin: 20px;
|
|
131
|
+
@include mq ($from: tablet) {
|
|
132
|
+
margin: 18px 18px 19px;
|
|
133
|
+
}
|
|
134
|
+
left: 0;
|
|
135
|
+
bottom: 0;
|
|
136
|
+
color: $govuk-secondary-text-colour;
|
|
137
|
+
z-index: 3;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
&__textarea {
|
|
141
|
+
@extend %govuk-body-s;
|
|
142
|
+
box-sizing: border-box;
|
|
143
|
+
min-height: auto;
|
|
144
|
+
font-size: 16px;
|
|
145
|
+
line-height: 20px;
|
|
146
|
+
max-height: 120px;
|
|
147
|
+
flex: 1 1 auto;
|
|
148
|
+
align-self: flex-end;
|
|
149
|
+
margin: 20px;
|
|
150
|
+
@include mq ($from: tablet) {
|
|
151
|
+
margin: 18px 18px 19px;
|
|
152
|
+
}
|
|
153
|
+
padding: 0;
|
|
154
|
+
border: 0;
|
|
155
|
+
height: 20px;
|
|
156
|
+
resize: none;
|
|
157
|
+
overflow-x: hidden;
|
|
158
|
+
overscroll-behavior: contain;
|
|
159
|
+
|
|
160
|
+
&:focus {
|
|
161
|
+
outline: none;
|
|
162
|
+
box-shadow: none;
|
|
163
|
+
z-index: 2;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
&__button {
|
|
168
|
+
margin: 10px 15px 12px 0;
|
|
169
|
+
@include mq ($from: tablet) {
|
|
170
|
+
margin-right: 10px;
|
|
171
|
+
}
|
|
172
|
+
font-size: 16px;
|
|
173
|
+
position: relative;
|
|
174
|
+
flex: 0 0 auto;
|
|
175
|
+
align-self: flex-end;
|
|
176
|
+
display: flex;
|
|
177
|
+
flex-direction: row;
|
|
178
|
+
flex-wrap: nowrap;
|
|
179
|
+
align-items: center;
|
|
180
|
+
width: auto;
|
|
181
|
+
cursor: pointer;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
&.wc-focus-within::after {
|
|
185
|
+
@include focus($glow: 5px, $strong: 2px, $background: 0, $inset: 0);
|
|
186
|
+
left: 4px;
|
|
187
|
+
right: 4px;
|
|
188
|
+
top: 4px;
|
|
189
|
+
bottom: 4px;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.wc-body:focus-visible + .wc-footer .wc-form {
|
|
194
|
+
border-top: 1px solid transparent;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.wc-footer {
|
|
198
|
+
position: relative;
|
|
199
|
+
flex: 0 0 0;
|
|
200
|
+
|
|
201
|
+
&__settings {
|
|
202
|
+
@include govuk-responsive-padding(2);
|
|
203
|
+
|
|
204
|
+
display: flex;
|
|
205
|
+
flex-direction: row;
|
|
206
|
+
|
|
207
|
+
&-link {
|
|
208
|
+
@extend %wc-link-button;
|
|
209
|
+
|
|
210
|
+
&:first-child {
|
|
211
|
+
margin-right: govuk-spacing(3);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
&__input {
|
|
217
|
+
@include govuk-responsive-padding(2);
|
|
218
|
+
|
|
219
|
+
border-top: 1px solid govuk-colour('black');
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
export const ErrorSummary = ({ errors }) => {
|
|
4
|
+
const errs = Object.keys(errors)
|
|
5
|
+
|
|
6
|
+
const goToInput = e => {
|
|
7
|
+
e.preventDefault()
|
|
8
|
+
const key = e.target.getAttribute('data-key')
|
|
9
|
+
document.querySelector(`#wc-${key}`).focus()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (errs.length === 0) {
|
|
13
|
+
return null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className='govuk-error-summary govuk-!-static-margin-bottom-7' data-module='govuk-error-summary' tabIndex='-1'>
|
|
18
|
+
<div role='alert'>
|
|
19
|
+
<h2 id='wc-error' className='wc-error-summary__title govuk-error-summary__title'>
|
|
20
|
+
There is a problem
|
|
21
|
+
</h2>
|
|
22
|
+
<div className='govuk-error-summary__body'>
|
|
23
|
+
<ul className='govuk-list govuk-error-summary__list wc-error-summary__list'>
|
|
24
|
+
{errs.map(key => (
|
|
25
|
+
<li key={key}>
|
|
26
|
+
<a href='#' data-key={key} onClick={goToInput}>{errors[key]}</a>
|
|
27
|
+
</li>
|
|
28
|
+
))}
|
|
29
|
+
</ul>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { classnames } from '../../lib/classnames'
|
|
3
|
+
|
|
4
|
+
export function Message ({ message, previousMessage }) {
|
|
5
|
+
const outbound = message.direction === 'outbound'
|
|
6
|
+
|
|
7
|
+
const messageOwnerSameAsPrevious = message?.direction === previousMessage?.direction
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<li className={classnames('wc-chat__message', outbound ? 'outbound' : 'inbound')}>
|
|
11
|
+
<div className='wc-chat__from'>
|
|
12
|
+
{messageOwnerSameAsPrevious ? null : <span>{outbound ? message.assignee : 'You'}</span>}
|
|
13
|
+
<span className='govuk-visually-hidden'>:</span>
|
|
14
|
+
</div>
|
|
15
|
+
<div className={classnames('wc-chat__text', outbound ? 'outbound' : 'inbound')}>
|
|
16
|
+
{message.text}
|
|
17
|
+
</div>
|
|
18
|
+
</li>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { useApp } from '../../store/useApp.js'
|
|
3
|
+
import { historyReplaceState } from '../../lib/history.js'
|
|
4
|
+
|
|
5
|
+
export function PanelHeader () {
|
|
6
|
+
const { thread, threadId, setChatVisibility, setUnseenCount, isMobile } = useApp()
|
|
7
|
+
|
|
8
|
+
const onClose = e => {
|
|
9
|
+
e.preventDefault()
|
|
10
|
+
setChatVisibility(false)
|
|
11
|
+
historyReplaceState()
|
|
12
|
+
|
|
13
|
+
if (threadId) {
|
|
14
|
+
thread?.lastMessageSeen()
|
|
15
|
+
setUnseenCount(0)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const BackButtonComponent = (
|
|
20
|
+
<button className='wc-header__back' aria-label='Close the webchat' onClick={onClose}>
|
|
21
|
+
<svg aria-hidden='true' focusable='false' width='20' height='20' viewBox='0 0 20 20'>
|
|
22
|
+
<path d='M4.828,11L12.314,18.485L10.899,19.899L1,10L10.899,0.101L12.314,1.515L4.828,9L19,9L19,11L4.828,11Z' fill='currentColor' />
|
|
23
|
+
</svg>
|
|
24
|
+
</button>
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
const CloseButtonComponent = (
|
|
28
|
+
<button className='wc-header__close' aria-label='Close the webchat' onClick={onClose}>
|
|
29
|
+
<svg aria-hidden='true' focusable='false' width='20' height='20' viewBox='0 0 20 20'>
|
|
30
|
+
<path d='M10,8.6L15.6,3L17,4.4L11.4,10L17,15.6L15.6,17L10,11.4L4.4,17L3,15.6L8.6,10L3,4.4L4.4,3L10,8.6Z' fill='currentColor' />
|
|
31
|
+
</svg>
|
|
32
|
+
</button>
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
const MinimiseButtonComponent = (
|
|
36
|
+
<button className='wc-header__hide' aria-label='Minimise the webchat' onClick={onClose}>
|
|
37
|
+
<svg aria-hidden='true' focusable='false' width='20' height='20' viewBox='0 0 20 20'>
|
|
38
|
+
<path d='M10 14.4l-7-7L4.4 6l5.6 5.6L15.6 6 17 7.4l-7 7z' fill='currentColor' />
|
|
39
|
+
</svg>
|
|
40
|
+
</button>
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
const isMobileAndHasHistory = isMobile && window.history.state
|
|
44
|
+
const isMobileAndNoHistory = isMobile && !window.history.state
|
|
45
|
+
|
|
46
|
+
let RightButtonComponent = CloseButtonComponent
|
|
47
|
+
|
|
48
|
+
if (thread) {
|
|
49
|
+
RightButtonComponent = MinimiseButtonComponent
|
|
50
|
+
|
|
51
|
+
if (isMobileAndNoHistory) {
|
|
52
|
+
RightButtonComponent = MinimiseButtonComponent
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!thread && isMobileAndNoHistory) {
|
|
57
|
+
RightButtonComponent = CloseButtonComponent
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (isMobileAndHasHistory) {
|
|
61
|
+
RightButtonComponent = null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div className='wc-header'>
|
|
66
|
+
{isMobileAndHasHistory ? BackButtonComponent : null}
|
|
67
|
+
|
|
68
|
+
<h2 id='wc-header-title' className='wc-header__title'>
|
|
69
|
+
Floodline Webchat
|
|
70
|
+
</h2>
|
|
71
|
+
|
|
72
|
+
{RightButtonComponent}
|
|
73
|
+
</div>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import React, { useEffect, useState, useCallback } from 'react'
|
|
2
|
+
import { createPortal } from 'react-dom'
|
|
3
|
+
|
|
4
|
+
import { PreChat } from '../screens/pre-chat.jsx'
|
|
5
|
+
import { RequestChat } from '../screens/request-chat.jsx'
|
|
6
|
+
import { Chat } from '../chat/chat.jsx'
|
|
7
|
+
import { Unavailable } from '../screens/unavailable.jsx'
|
|
8
|
+
import { EndChat } from '../screens/end-chat.jsx'
|
|
9
|
+
import { Settings } from '../screens/settings.jsx'
|
|
10
|
+
import { Feedback } from '../screens/feedback.jsx'
|
|
11
|
+
|
|
12
|
+
import { useApp } from '../../store/useApp.js'
|
|
13
|
+
import { useChatSdk } from '../../store/useChatSdk.js'
|
|
14
|
+
import { useFocusedElements } from '../../hooks/useFocusedElements.js'
|
|
15
|
+
import { historyReplaceState } from '../../lib/history.js'
|
|
16
|
+
|
|
17
|
+
export function Panel () {
|
|
18
|
+
const { sdk, availability, thread, threadId, setThread, setThreadId, setChatVisibility, setMessages, setUnseenCount, isMobile } = useApp()
|
|
19
|
+
const { fetchThread, fetchMessages } = useChatSdk(sdk)
|
|
20
|
+
|
|
21
|
+
const [screen, setScreen] = useState(threadId ? 2 : 0)
|
|
22
|
+
|
|
23
|
+
useFocusedElements(screen)
|
|
24
|
+
|
|
25
|
+
const onEscapeKey = useCallback(e => {
|
|
26
|
+
if (e.key === 'Escape' || e.key === 'Esc') {
|
|
27
|
+
if (threadId) {
|
|
28
|
+
thread?.lastMessageSeen()
|
|
29
|
+
setUnseenCount(0)
|
|
30
|
+
}
|
|
31
|
+
setChatVisibility(false)
|
|
32
|
+
historyReplaceState()
|
|
33
|
+
}
|
|
34
|
+
}, [thread, threadId, setUnseenCount, setChatVisibility])
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* We need ammend classes on the body element to handle mobile behaviour
|
|
38
|
+
*/
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
document.body.classList.remove('wc-u-hidden')
|
|
41
|
+
document.getElementsByTagName('html')[0].classList.add('wc-u-html')
|
|
42
|
+
document.body.classList.add('wc-u-body')
|
|
43
|
+
return () => {
|
|
44
|
+
document.getElementsByTagName('html')[0].classList.remove('wc-u-html')
|
|
45
|
+
document.body.classList.remove('wc-u-body')
|
|
46
|
+
}
|
|
47
|
+
}, [isMobile])
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Initializes the eventListener for pressing the escape key
|
|
51
|
+
*/
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
document.addEventListener('keydown', onEscapeKey)
|
|
54
|
+
|
|
55
|
+
return () => {
|
|
56
|
+
document.removeEventListener('keydown', onEscapeKey)
|
|
57
|
+
}
|
|
58
|
+
}, [onEscapeKey, setChatVisibility])
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Recovers the thread if there is a threadId but no thread loaded in to state
|
|
62
|
+
*/
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
const recover = async () => {
|
|
65
|
+
try {
|
|
66
|
+
const fetchedThread = await fetchThread(threadId)
|
|
67
|
+
setThread(fetchedThread)
|
|
68
|
+
|
|
69
|
+
const fetchedMessages = await fetchMessages(fetchedThread, threadId)
|
|
70
|
+
setMessages(fetchedMessages)
|
|
71
|
+
} catch (err) {
|
|
72
|
+
console.log('[Chat Error] fetchThread', err)
|
|
73
|
+
|
|
74
|
+
setThreadId()
|
|
75
|
+
setThread()
|
|
76
|
+
setScreen(0)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (threadId) {
|
|
81
|
+
if (thread) {
|
|
82
|
+
thread.lastMessageSeen()
|
|
83
|
+
setUnseenCount(0)
|
|
84
|
+
setScreen(2)
|
|
85
|
+
} else {
|
|
86
|
+
recover()
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}, [thread, threadId])
|
|
90
|
+
|
|
91
|
+
const handleScreenChange = newScreen => e => {
|
|
92
|
+
e.preventDefault()
|
|
93
|
+
setScreen(newScreen)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const goToPreChatScreen = handleScreenChange(0)
|
|
97
|
+
const goToRequestChatScreen = handleScreenChange(1)
|
|
98
|
+
const goToChatScreen = handleScreenChange(2)
|
|
99
|
+
const goToEndChatScreen = handleScreenChange(3)
|
|
100
|
+
const goToFeedbackScreen = handleScreenChange(4)
|
|
101
|
+
const goToSettingsScreen = handleScreenChange(5)
|
|
102
|
+
let ScreenComponent
|
|
103
|
+
|
|
104
|
+
switch (screen) {
|
|
105
|
+
case 0:
|
|
106
|
+
ScreenComponent = <PreChat onContinue={goToRequestChatScreen} />
|
|
107
|
+
break
|
|
108
|
+
case 1:
|
|
109
|
+
ScreenComponent = <RequestChat onPreChatScreen={goToPreChatScreen} />
|
|
110
|
+
break
|
|
111
|
+
case 2:
|
|
112
|
+
ScreenComponent = <Chat onSettingsScreen={goToSettingsScreen} onEndChatScreen={goToEndChatScreen} />
|
|
113
|
+
break
|
|
114
|
+
case 3:
|
|
115
|
+
ScreenComponent = <EndChat onChatScreen={goToChatScreen} onEndChatConfirm={goToFeedbackScreen} />
|
|
116
|
+
break
|
|
117
|
+
case 4:
|
|
118
|
+
ScreenComponent = <Feedback onCancel={() => setChatVisibility(false)} />
|
|
119
|
+
break
|
|
120
|
+
case 5:
|
|
121
|
+
ScreenComponent = <Settings onCancel={goToChatScreen} />
|
|
122
|
+
break
|
|
123
|
+
default:
|
|
124
|
+
ScreenComponent = <PreChat onContinue={goToRequestChatScreen} />
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (availability === 'UNAVAILABLE') {
|
|
128
|
+
ScreenComponent = <Unavailable />
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const Component = (
|
|
132
|
+
<div id='wc-panel' role='dialog' className='wc-panel' tabIndex='-1' aria-modal='true' aria-labelledby='wc-header-title'>
|
|
133
|
+
<div className='wc-panel__inner'>
|
|
134
|
+
{ScreenComponent}
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<>
|
|
141
|
+
{createPortal(Component, document.body)}
|
|
142
|
+
</>
|
|
143
|
+
)
|
|
144
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
.wc-panel {
|
|
2
|
+
display: flex;
|
|
3
|
+
position: fixed;
|
|
4
|
+
flex-wrap: nowrap;
|
|
5
|
+
z-index: 999;
|
|
6
|
+
top: 0;
|
|
7
|
+
right: 0;
|
|
8
|
+
bottom: 0;
|
|
9
|
+
left: 0;
|
|
10
|
+
padding: 0;
|
|
11
|
+
box-sizing: border-box;
|
|
12
|
+
-webkit-overflow-scrolling: touch;
|
|
13
|
+
touch-action: pan-x pan-y;
|
|
14
|
+
background-color: #ffffff;
|
|
15
|
+
@include mq ($from: tablet) {
|
|
16
|
+
top: auto;
|
|
17
|
+
left: auto;
|
|
18
|
+
width: 400px;
|
|
19
|
+
height: 540px;
|
|
20
|
+
margin-right: 10px;
|
|
21
|
+
margin-bottom: 10px;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
&:focus {
|
|
25
|
+
outline: none;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
&:focus.wc-focus-visible::after {
|
|
29
|
+
@include focus($glow: 5px, $strong: 2px, $background: 0, $inset: 0);
|
|
30
|
+
left: 4px;
|
|
31
|
+
right: 4px;
|
|
32
|
+
top: 4px;
|
|
33
|
+
bottom: 4px;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
&__inner {
|
|
37
|
+
position: relative;
|
|
38
|
+
flex: 0 0 100%;
|
|
39
|
+
display: flex;
|
|
40
|
+
flex-wrap: nowrap;
|
|
41
|
+
flex-direction: column;
|
|
42
|
+
background-color: transparent;
|
|
43
|
+
border: 0;
|
|
44
|
+
outline: 0;
|
|
45
|
+
@include mq ($from: tablet) {
|
|
46
|
+
border: 1px solid govuk-colour('black');
|
|
47
|
+
}
|
|
48
|
+
box-sizing: border-box;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.wc-header {
|
|
53
|
+
display: flex;
|
|
54
|
+
flex-direction: row;
|
|
55
|
+
align-items: center;
|
|
56
|
+
background-color: govuk-colour('black');
|
|
57
|
+
border-bottom: 1px solid govuk-colour('black');
|
|
58
|
+
padding: 3px;
|
|
59
|
+
|
|
60
|
+
&__link {
|
|
61
|
+
margin: 0 govuk-spacing(2);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
&__title {
|
|
65
|
+
@extend %govuk-heading-s;
|
|
66
|
+
font-size: 19px;
|
|
67
|
+
padding: 0 0 0 10px;
|
|
68
|
+
@include mq ($from: tablet) {
|
|
69
|
+
padding-left: 7px;
|
|
70
|
+
}
|
|
71
|
+
color: govuk-colour('white');
|
|
72
|
+
margin-bottom: 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
&__close,
|
|
76
|
+
&__hide,
|
|
77
|
+
&__back {
|
|
78
|
+
display: flex;
|
|
79
|
+
justify-content: center;
|
|
80
|
+
color: govuk-colour('white');
|
|
81
|
+
background: none;
|
|
82
|
+
border: 0;
|
|
83
|
+
padding: 7px;
|
|
84
|
+
margin: 0 0 0 auto;
|
|
85
|
+
cursor: pointer;
|
|
86
|
+
|
|
87
|
+
&:focus {
|
|
88
|
+
outline: 3px solid $govuk-focus-colour;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
&__back {
|
|
93
|
+
margin: 0;
|
|
94
|
+
display: flex;
|
|
95
|
+
|
|
96
|
+
@include mq ($from: 'tablet') {
|
|
97
|
+
display: none
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.wc-body {
|
|
103
|
+
font-size: 16px;
|
|
104
|
+
flex: 1;
|
|
105
|
+
position: relative;
|
|
106
|
+
overflow-y: auto;
|
|
107
|
+
overscroll-behavior: contain;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.wc-content {
|
|
111
|
+
padding: 20px 15px;
|
|
112
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { PanelHeader } from '../panel/panel-header.jsx'
|
|
3
|
+
import { useApp } from '../../store/useApp.js'
|
|
4
|
+
|
|
5
|
+
export function EndChat ({ onChatScreen, onEndChatConfirm }) {
|
|
6
|
+
const { setCustomerId, setThreadId, setMessages, agentStatus, thread, setUnseenCount } = useApp()
|
|
7
|
+
|
|
8
|
+
const confirmEndChat = async e => {
|
|
9
|
+
e.preventDefault()
|
|
10
|
+
|
|
11
|
+
setThreadId()
|
|
12
|
+
setMessages([])
|
|
13
|
+
setCustomerId()
|
|
14
|
+
thread.lastMessageSeen()
|
|
15
|
+
setUnseenCount(0)
|
|
16
|
+
|
|
17
|
+
// End chat if still open
|
|
18
|
+
if (agentStatus !== 'closed') {
|
|
19
|
+
thread.endChat()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
onEndChatConfirm(e)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<>
|
|
27
|
+
<PanelHeader />
|
|
28
|
+
<div className='wc-body'>
|
|
29
|
+
<div className='wc-content'>
|
|
30
|
+
<h3 className='wc-heading' aria-live='polite'>Are you sure you want to end the chat?</h3>
|
|
31
|
+
<div className='govuk-button-group'>
|
|
32
|
+
<a href='#endChat' role='button' className='wc-button govuk-button' data-module='govuk-button' id='confirmEndChat' onClick={confirmEndChat}>Yes, end chat</a>
|
|
33
|
+
<a href='#resumeChat' role='button' className='wc-link govuk-link' onClick={onChatScreen}>No, resume chat</a>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { PanelHeader } from '../panel/panel-header.jsx'
|
|
3
|
+
|
|
4
|
+
export function Feedback ({ onCancel }) {
|
|
5
|
+
const feedbackCancel = async e => {
|
|
6
|
+
onCancel(e)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<>
|
|
11
|
+
<PanelHeader />
|
|
12
|
+
|
|
13
|
+
<div className='wc-body'>
|
|
14
|
+
<div className='wc-content'>
|
|
15
|
+
<h3 id='wc-subtitle' className='wc-heading'>Give Feedback on Floodline webchat</h3>
|
|
16
|
+
<p>Please note we’re unable to respond to feedback.</p>
|
|
17
|
+
<p><a className='govuk-link' href='#' target='_blank' rel='noreferrer'>Feedback Link</a></p>
|
|
18
|
+
<p><a id='feedback-cancel' className='wc-link govuk-link' href='#' data-module='govuk-button' role='button' onClick={feedbackCancel}>Close</a></p>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
</>
|
|
22
|
+
)
|
|
23
|
+
}
|