@defra/flood-webchat 0.0.1-beta.1 → 0.0.1-beta.10
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 +1316 -214
- package/dist/client.js.map +1 -1
- package/dist/server.js +181 -240
- package/dist/server.js.map +1 -1
- package/main.scss +4 -3
- package/package.json +9 -3
- package/src/client/components/availability/availability.jsx +43 -46
- package/src/client/components/chat/chat.jsx +143 -0
- package/src/client/components/chat/chat.scss +134 -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 +39 -0
- package/src/client/components/panel/panel.jsx +109 -0
- package/src/client/components/panel/panel.scss +72 -0
- package/src/client/components/screens/end-chat.jsx +17 -0
- package/src/client/components/screens/pre-chat.jsx +55 -0
- package/src/client/components/screens/request-chat.jsx +170 -0
- package/src/client/components/screens/unavailable.jsx +23 -0
- package/src/client/hooks/useFocusedElements.js +71 -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/transform-messages.js +14 -0
- package/src/client/store/AppProvider.jsx +114 -0
- package/src/client/store/actions-map.js +97 -0
- package/src/client/store/reducer.js +29 -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,39 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { useApp } from '../../store/useApp.js'
|
|
3
|
+
|
|
4
|
+
export function PanelHeader () {
|
|
5
|
+
const { thread, setChatVisibility } = useApp()
|
|
6
|
+
|
|
7
|
+
const onClose = e => {
|
|
8
|
+
e.preventDefault()
|
|
9
|
+
setChatVisibility(false)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let ButtonComponent = (
|
|
13
|
+
<button className='wc-header__close' aria-label='Close the webchat' onClick={onClose}>
|
|
14
|
+
<svg aria-hidden='true' focusable='false' width='20' height='20' viewBox='0 0 20 20'>
|
|
15
|
+
<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' />
|
|
16
|
+
</svg>
|
|
17
|
+
</button>
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
if (thread) {
|
|
21
|
+
ButtonComponent = (
|
|
22
|
+
<button className='wc-header__hide' aria-label='Minimise the webchat' onClick={onClose}>
|
|
23
|
+
<svg aria-hidden='true' focusable='false' width='20' height='20' viewBox='0 0 20 20'>
|
|
24
|
+
<path d='M10 14.4l-7-7L4.4 6l5.6 5.6L15.6 6 17 7.4l-7 7z' fill='currentColor' />
|
|
25
|
+
</svg>
|
|
26
|
+
</button>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className='wc-header'>
|
|
32
|
+
<h2 id='wc-header-title' className='wc-header__title govuk-heading-s'>
|
|
33
|
+
Floodline Webchat
|
|
34
|
+
</h2>
|
|
35
|
+
|
|
36
|
+
{ButtonComponent}
|
|
37
|
+
</div>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
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
|
+
|
|
10
|
+
import { useApp } from '../../store/useApp.js'
|
|
11
|
+
import { useChatSdk } from '../../store/useChatSdk.js'
|
|
12
|
+
import { useFocusedElements } from '../../hooks/useFocusedElements.js'
|
|
13
|
+
|
|
14
|
+
export function Panel () {
|
|
15
|
+
const { sdk, availability, thread, threadId, setThread, setThreadId, setChatVisibility, setMessages } = useApp()
|
|
16
|
+
const { fetchThread, fetchMessages } = useChatSdk(sdk)
|
|
17
|
+
|
|
18
|
+
const [screen, setScreen] = useState(threadId ? 2 : 0)
|
|
19
|
+
|
|
20
|
+
useFocusedElements(screen)
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Initializes the eventListener for pressing the escape key
|
|
24
|
+
*/
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const escapeKeyEvent = document.addEventListener('keydown', onEscapeKey)
|
|
27
|
+
|
|
28
|
+
return () => {
|
|
29
|
+
document.removeEventListener('keydown', escapeKeyEvent)
|
|
30
|
+
}
|
|
31
|
+
}, [])
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Recovers the thread if there is a threadId but no thread loaded in to state
|
|
35
|
+
*/
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const recover = async () => {
|
|
38
|
+
try {
|
|
39
|
+
const fetchedThread = await fetchThread(threadId)
|
|
40
|
+
setThread(fetchedThread)
|
|
41
|
+
|
|
42
|
+
const fetchedMessages = await fetchMessages(fetchedThread, threadId)
|
|
43
|
+
setMessages(fetchedMessages)
|
|
44
|
+
} catch (err) {
|
|
45
|
+
console.log('[Chat Error] fetchThread', err)
|
|
46
|
+
|
|
47
|
+
setThreadId()
|
|
48
|
+
setThread()
|
|
49
|
+
setScreen(0)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (threadId) {
|
|
54
|
+
(thread) ? setScreen(2) : recover()
|
|
55
|
+
}
|
|
56
|
+
}, [thread, threadId])
|
|
57
|
+
|
|
58
|
+
const onEscapeKey = useCallback(e => {
|
|
59
|
+
if (e.key === 'Escape' || e.key === 'Esc') {
|
|
60
|
+
setChatVisibility(false)
|
|
61
|
+
}
|
|
62
|
+
}, [])
|
|
63
|
+
|
|
64
|
+
const handleScreenChange = newScreen => e => {
|
|
65
|
+
e.preventDefault()
|
|
66
|
+
setScreen(newScreen)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const onForward = handleScreenChange(screen + 1)
|
|
70
|
+
const onBack = handleScreenChange(screen - 1)
|
|
71
|
+
const onEndChat = handleScreenChange(3)
|
|
72
|
+
const onResume = handleScreenChange(2)
|
|
73
|
+
|
|
74
|
+
const onEndChatConfirm = e => {
|
|
75
|
+
e.preventDefault()
|
|
76
|
+
console.log('confirmed end chat')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let ScreenComponent
|
|
80
|
+
|
|
81
|
+
switch (screen) {
|
|
82
|
+
case 0:
|
|
83
|
+
ScreenComponent = <PreChat onForward={onForward} />
|
|
84
|
+
break
|
|
85
|
+
case 1:
|
|
86
|
+
ScreenComponent = <RequestChat onBack={onBack} />
|
|
87
|
+
break
|
|
88
|
+
case 2:
|
|
89
|
+
ScreenComponent = <Chat setScreen={setScreen} onEndChat={onEndChat} />
|
|
90
|
+
break
|
|
91
|
+
case 3:
|
|
92
|
+
ScreenComponent = <EndChat onResume={onResume} onEndChatConfirm={onEndChatConfirm} />
|
|
93
|
+
break
|
|
94
|
+
default:
|
|
95
|
+
ScreenComponent = <PreChat onForward={onForward} />
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (availability === 'UNAVAILABLE') {
|
|
99
|
+
ScreenComponent = <Unavailable />
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const Component = (
|
|
103
|
+
<div id='wc-panel' className='wc-panel' role='dialog' tabIndex='-1' aria-modal='true' aria-labelledby='wc-header-title'>
|
|
104
|
+
{ScreenComponent}
|
|
105
|
+
</div>
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return createPortal(Component, document.body)
|
|
109
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
.wc-panel {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
position: fixed;
|
|
5
|
+
bottom: 0;
|
|
6
|
+
right: 0;
|
|
7
|
+
top: 0;
|
|
8
|
+
left: 0;
|
|
9
|
+
width: 100%;
|
|
10
|
+
height: 100%;
|
|
11
|
+
background-color: govuk-colour('white');
|
|
12
|
+
border: 1px solid govuk-colour('black');
|
|
13
|
+
z-index: 999;
|
|
14
|
+
outline: none;
|
|
15
|
+
|
|
16
|
+
@include mq ($from: 'tablet') {
|
|
17
|
+
bottom: 0;
|
|
18
|
+
right: 0;
|
|
19
|
+
top: auto;
|
|
20
|
+
left: auto;
|
|
21
|
+
width: 400px;
|
|
22
|
+
height: 540px;
|
|
23
|
+
margin-bottom: 10px;
|
|
24
|
+
margin-right: 10px;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.wc-header {
|
|
29
|
+
display: flex;
|
|
30
|
+
flex-direction: row;
|
|
31
|
+
align-items: center;
|
|
32
|
+
background-color: govuk-colour('black');
|
|
33
|
+
|
|
34
|
+
&__link {
|
|
35
|
+
margin: 0 govuk-spacing(2);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
&__title {
|
|
39
|
+
padding: 0 0 0 govuk-spacing(2);
|
|
40
|
+
color: govuk-colour('white');
|
|
41
|
+
margin-bottom: 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
&__close,
|
|
45
|
+
&__hide {
|
|
46
|
+
display: flex;
|
|
47
|
+
justify-content: center;
|
|
48
|
+
color: govuk-colour('white');
|
|
49
|
+
background: none;
|
|
50
|
+
border: 0;
|
|
51
|
+
padding: govuk-spacing(2);
|
|
52
|
+
margin: 0 0 0 auto;
|
|
53
|
+
cursor: pointer;
|
|
54
|
+
|
|
55
|
+
&:focus {
|
|
56
|
+
@include govuk-focused-text;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.wc-body {
|
|
62
|
+
@include govuk-responsive-padding(3);
|
|
63
|
+
|
|
64
|
+
flex: 1;
|
|
65
|
+
position: relative;
|
|
66
|
+
overflow-y: auto;
|
|
67
|
+
overscroll-behavior: contain;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.wc-back-link {
|
|
71
|
+
margin: govuk-spacing(1) 0 govuk-spacing(6) 0;
|
|
72
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { PanelHeader } from '../panel/panel-header.jsx'
|
|
3
|
+
|
|
4
|
+
export function EndChat ({ onResume, onEndChatConfirm }) {
|
|
5
|
+
return (
|
|
6
|
+
<>
|
|
7
|
+
<PanelHeader />
|
|
8
|
+
<div className='wc-body'>
|
|
9
|
+
<h3 className='govuk-heading-s' aria-live='polite'>Are you sure you want to end the chat?</h3>
|
|
10
|
+
<div className='govuk-button-group'>
|
|
11
|
+
<a href='#' className='govuk-button govuk-!-font-size-16' data-module='govuk-button' onClick={onEndChatConfirm}>Yes, end chat</a>
|
|
12
|
+
<a href='#' className='govuk-link govuk-!-font-size-16' onClick={onResume}>No, resume chat</a>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
</>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { PanelHeader } from '../panel/panel-header.jsx'
|
|
3
|
+
|
|
4
|
+
export function PreChat ({ onForward }) {
|
|
5
|
+
return (
|
|
6
|
+
<>
|
|
7
|
+
<PanelHeader />
|
|
8
|
+
|
|
9
|
+
<div className='wc-body'>
|
|
10
|
+
<h3 className='govuk-heading-s' aria-live='polite'>What you can use webchat for</h3>
|
|
11
|
+
<p className='govuk-body-s'>Webchat lets you talk directly to a Floodline adviser.</p>
|
|
12
|
+
|
|
13
|
+
<p className='govuk-body-s'>You can use webchat to get:</p>
|
|
14
|
+
<ul className='govuk-list govuk-list--bullet govuk-body-s'>
|
|
15
|
+
<li>current flood warnings and alerts in your area</li>
|
|
16
|
+
<li>information on the flood warning service</li>
|
|
17
|
+
<li>advice on what to do before, during and after a flood</li>
|
|
18
|
+
</ul>
|
|
19
|
+
|
|
20
|
+
<p className='govuk-body-s'>
|
|
21
|
+
{
|
|
22
|
+
'There are other ways to '
|
|
23
|
+
}
|
|
24
|
+
<a className='govuk-link' href='https://www.gov.uk/sign-up-for-flood-warnings'>
|
|
25
|
+
sign up for flood warnings
|
|
26
|
+
</a>
|
|
27
|
+
{
|
|
28
|
+
' and '
|
|
29
|
+
}
|
|
30
|
+
<a className='govuk-link' href='https://www.fws.environment-agency.gov.uk/app/olr/login'>
|
|
31
|
+
manage your flood warnings account
|
|
32
|
+
</a>.
|
|
33
|
+
</p>
|
|
34
|
+
<p className='govuk-body-s'>Do not use webchat to <a className='govuk-link' href='https://www.gov.uk/report-flood-cause'>report a flood</a>.</p>
|
|
35
|
+
|
|
36
|
+
<button className='govuk-button govuk-!-font-size-16' data-module='govuk-button' onClick={onForward}>Continue</button>
|
|
37
|
+
|
|
38
|
+
<h3 className='govuk-heading-s'>Other flood information</h3>
|
|
39
|
+
|
|
40
|
+
<p className='govuk-body-s'>Call Floodline for all other flood and flood warning information.</p>
|
|
41
|
+
<p className='govuk-body-s'>
|
|
42
|
+
<strong>Floodline helpline</strong>
|
|
43
|
+
<br />
|
|
44
|
+
Telephone: 0345 988 1188
|
|
45
|
+
<br />
|
|
46
|
+
Textphone: 0345 602 6340
|
|
47
|
+
<br />
|
|
48
|
+
Open 24 hours a day, 7 days a week
|
|
49
|
+
<br />
|
|
50
|
+
<a className='govuk-link' href='https://gov.uk/call-charges'>Find out more about call charges</a>
|
|
51
|
+
</p>
|
|
52
|
+
</div>
|
|
53
|
+
</>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import React, { useRef, useState, useEffect } from 'react'
|
|
2
|
+
import * as uuid from 'uuid'
|
|
3
|
+
|
|
4
|
+
import { classnames } from '../../lib/classnames.js'
|
|
5
|
+
import { PanelHeader } from '../panel/panel-header.jsx'
|
|
6
|
+
import { useApp } from '../../store/useApp.js'
|
|
7
|
+
import { useChatSdk } from '../../store/useChatSdk.js'
|
|
8
|
+
|
|
9
|
+
const QUESTION_MAX_LENGTH = 500
|
|
10
|
+
|
|
11
|
+
export function RequestChat ({ onBack }) {
|
|
12
|
+
const { sdk, setCustomerId, setThreadId, setThread } = useApp()
|
|
13
|
+
const { fetchCustomerId, fetchThread } = useChatSdk(sdk)
|
|
14
|
+
|
|
15
|
+
const [errors, setErrors] = useState({})
|
|
16
|
+
const [questionLength, setQuestionLength] = useState(0)
|
|
17
|
+
|
|
18
|
+
const nameRef = useRef()
|
|
19
|
+
const questionRef = useRef()
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (Object.keys(errors).length) {
|
|
23
|
+
document.querySelector('.govuk-error-summary')?.focus()
|
|
24
|
+
}
|
|
25
|
+
}, [errors])
|
|
26
|
+
|
|
27
|
+
const onQuestionChange = e => {
|
|
28
|
+
setQuestionLength(e.target.value.length)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const onRequestChat = async e => {
|
|
32
|
+
e.preventDefault()
|
|
33
|
+
|
|
34
|
+
const errs = {}
|
|
35
|
+
|
|
36
|
+
if (nameRef.current.value.length === 0) {
|
|
37
|
+
errs.name = 'Enter your name'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (questionRef.current.value.length === 0) {
|
|
41
|
+
errs.question = 'Enter your question'
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (questionRef.current.value.length > QUESTION_MAX_LENGTH) {
|
|
45
|
+
errs.question = 'Your question must be 500 characters or less'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (Object.keys(errs).length === 0) {
|
|
49
|
+
try {
|
|
50
|
+
const threadId = uuid.v4()
|
|
51
|
+
const customerId = await fetchCustomerId()
|
|
52
|
+
const thread = await fetchThread(threadId)
|
|
53
|
+
|
|
54
|
+
sdk.getCustomer().setName(nameRef.current.value)
|
|
55
|
+
|
|
56
|
+
await thread.startChat(questionRef.current.value || 'Begin conversation')
|
|
57
|
+
|
|
58
|
+
setCustomerId(customerId)
|
|
59
|
+
setThreadId(threadId)
|
|
60
|
+
setThread(thread)
|
|
61
|
+
} catch (err) {
|
|
62
|
+
console.log('[Request Chat Error]', err)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
setErrors(errs)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const isQuestionLengthValid = QUESTION_MAX_LENGTH >= questionLength
|
|
70
|
+
const questionLengthRemaining = QUESTION_MAX_LENGTH - questionLength
|
|
71
|
+
const questionLengthExceeded = questionLength - QUESTION_MAX_LENGTH
|
|
72
|
+
|
|
73
|
+
let questionHint = `You have ${questionLengthRemaining} characters remaining`
|
|
74
|
+
|
|
75
|
+
if (!isQuestionLengthValid) {
|
|
76
|
+
questionHint = `You have ${questionLengthExceeded} characters too many`
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<>
|
|
81
|
+
<PanelHeader />
|
|
82
|
+
|
|
83
|
+
<div className='wc-body'>
|
|
84
|
+
<a href='#' className='wc-back-link govuk-back-link' onClick={onBack}>What you can use webchat for</a>
|
|
85
|
+
|
|
86
|
+
{Object.keys(errors).length > 0
|
|
87
|
+
? (
|
|
88
|
+
<div className='govuk-error-summary govuk-!-static-margin-bottom-7' data-module='govuk-error-summary' tabIndex='-1'>
|
|
89
|
+
<div role='alert'>
|
|
90
|
+
<h2 id='wc-error' className='govuk-error-summary__title'>
|
|
91
|
+
There is a problem
|
|
92
|
+
</h2>
|
|
93
|
+
<div className='govuk-error-summary__body'>
|
|
94
|
+
<ul className='govuk-list govuk-error-summary__list'>
|
|
95
|
+
{Object.keys(errors).map(key => (
|
|
96
|
+
<li key={key}>
|
|
97
|
+
<a href={`#${key}`}>{errors[key]}</a>
|
|
98
|
+
</li>
|
|
99
|
+
))}
|
|
100
|
+
</ul>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
)
|
|
105
|
+
: null}
|
|
106
|
+
|
|
107
|
+
<h3 className='govuk-heading-s' aria-live='polite'>Your name and question</h3>
|
|
108
|
+
|
|
109
|
+
<form>
|
|
110
|
+
<div className={classnames('govuk-form-group', errors.name && 'govuk-form-group--error')}>
|
|
111
|
+
<label className='govuk-label' htmlFor='wc-name'>
|
|
112
|
+
Your name
|
|
113
|
+
</label>
|
|
114
|
+
{errors.name && (
|
|
115
|
+
<p className='govuk-error-message'>
|
|
116
|
+
<span className='govuk-visually-hidden'>Error:</span> {errors.name}
|
|
117
|
+
</p>
|
|
118
|
+
)}
|
|
119
|
+
<input
|
|
120
|
+
ref={nameRef}
|
|
121
|
+
className={classnames('govuk-input', errors.name && 'govuk-input--error')}
|
|
122
|
+
id='wc-name'
|
|
123
|
+
name='name'
|
|
124
|
+
type='text'
|
|
125
|
+
data-testid='request-chat-user-name'
|
|
126
|
+
/>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<div className='govuk-character-count' data-module='govuk-character-count' data-maxlength='500'>
|
|
130
|
+
<div className={classnames('govuk-form-group', errors.question && 'govuk-form-group--error')}>
|
|
131
|
+
<label className='govuk-label' htmlFor='wc-question'>
|
|
132
|
+
Your question
|
|
133
|
+
</label>
|
|
134
|
+
{errors.question && (
|
|
135
|
+
<p className='govuk-error-message'>
|
|
136
|
+
<span className='govuk-visually-hidden'>Error:</span>
|
|
137
|
+
{errors.question}
|
|
138
|
+
</p>
|
|
139
|
+
)}
|
|
140
|
+
<textarea
|
|
141
|
+
ref={questionRef}
|
|
142
|
+
id='wc-question'
|
|
143
|
+
name='question'
|
|
144
|
+
rows='5'
|
|
145
|
+
aria-describedby='wc-question-info'
|
|
146
|
+
onChange={onQuestionChange}
|
|
147
|
+
className={classnames('govuk-textarea', 'govuk-js-character-count', !isQuestionLengthValid || errors.question ? 'govuk-textarea--error' : '')}
|
|
148
|
+
data-testid='request-chat-user-question'
|
|
149
|
+
/>
|
|
150
|
+
<div id='wc-question-info' className='govuk-hint govuk-character-count__message' style={{ color: `${!isQuestionLengthValid ? '#d4351c' : ''}` }} aria-hidden='true'>
|
|
151
|
+
{questionHint}
|
|
152
|
+
</div>
|
|
153
|
+
<div className='govuk-character-count__sr-status govuk-visually-hidden' aria-live='polite'>
|
|
154
|
+
{questionHint}
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<button
|
|
160
|
+
className='govuk-button govuk-!-margin-top-1 govuk-!-font-size-16'
|
|
161
|
+
data-module='govuk-button'
|
|
162
|
+
onClick={onRequestChat}
|
|
163
|
+
>
|
|
164
|
+
Request chat
|
|
165
|
+
</button>
|
|
166
|
+
</form>
|
|
167
|
+
</div>
|
|
168
|
+
</>
|
|
169
|
+
)
|
|
170
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { PanelHeader } from '../panel/panel-header.jsx'
|
|
3
|
+
|
|
4
|
+
export function Unavailable () {
|
|
5
|
+
return (
|
|
6
|
+
<>
|
|
7
|
+
<PanelHeader />
|
|
8
|
+
|
|
9
|
+
<div className='wc-body'>
|
|
10
|
+
<h3 id='wc-subtitle' className='govuk-heading-m'>Webchat is currently not available</h3>
|
|
11
|
+
<p className='govuk-body-s'>Try again later, or call Floodline:</p>
|
|
12
|
+
<p className='govuk-body-s'>
|
|
13
|
+
<strong>Floodline helpline</strong>
|
|
14
|
+
<br />Telephone: 0345 988 1188
|
|
15
|
+
<br />Textphone: 0345 602 6340
|
|
16
|
+
<br />Open 24 hours a day, 7 days a week
|
|
17
|
+
<br /><a className='govuk-link' href='https://gov.uk/call-charges'>Find out more about call charges</a>
|
|
18
|
+
</p>
|
|
19
|
+
<p className='govuk-body-s'>We're running webchat as a trial, it will not always be available.</p>
|
|
20
|
+
</div>
|
|
21
|
+
</>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react'
|
|
2
|
+
|
|
3
|
+
const setAriaHidden = bool => {
|
|
4
|
+
for (const node of document.body.children) {
|
|
5
|
+
if (node.id !== 'wc-panel') {
|
|
6
|
+
(bool) ? node.setAttribute('aria-hidden', 'true') : node.removeAttribute('aria-hidden')
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const getFocusableElements = () => {
|
|
12
|
+
const selectors = [
|
|
13
|
+
'#wc-panel a:not([disabled])',
|
|
14
|
+
'#wc-panel button:not([disabled])',
|
|
15
|
+
'#wc-panel select:not([disabled])',
|
|
16
|
+
'#wc-panel input:not([disabled])',
|
|
17
|
+
'#wc-panel textarea:not([disabled])',
|
|
18
|
+
'#wc-panel *[tabindex="0"]:not([disabled])'
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
const elements = document.body.querySelectorAll(selectors.join(','))
|
|
22
|
+
return Array.from(elements).filter(e => !e.closest('[hidden]') && !e.closest('[aria-hidden="true"]'))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const useFocusedElements = screen => {
|
|
26
|
+
const [panelElements, setPanelElements] = useState([])
|
|
27
|
+
|
|
28
|
+
const onKeyDown = useCallback(e => {
|
|
29
|
+
const webchatPanelElement = document.querySelector('#wc-panel')
|
|
30
|
+
|
|
31
|
+
if (e.key === 'Tab') {
|
|
32
|
+
if (webchatPanelElement && !document.activeElement.closest('#wc-panel')) {
|
|
33
|
+
webchatPanelElement.focus()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (e.shiftKey) {
|
|
37
|
+
if (document.activeElement === panelElements[0]) {
|
|
38
|
+
panelElements[panelElements.length - 1].focus()
|
|
39
|
+
e.preventDefault()
|
|
40
|
+
} else if (document.activeElement === document.querySelector('#wc-panel')) {
|
|
41
|
+
panelElements[panelElements.length - 1]?.focus()
|
|
42
|
+
e.preventDefault()
|
|
43
|
+
}
|
|
44
|
+
} else if (document.activeElement === panelElements[panelElements.length - 1]) {
|
|
45
|
+
panelElements[0].focus()
|
|
46
|
+
e.preventDefault()
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}, [panelElements])
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
setPanelElements(getFocusableElements())
|
|
53
|
+
}, [screen])
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
setAriaHidden(true)
|
|
57
|
+
|
|
58
|
+
const panelElement = document.querySelector('#wc-panel')
|
|
59
|
+
panelElement.focus()
|
|
60
|
+
|
|
61
|
+
document.addEventListener('keydown', onKeyDown)
|
|
62
|
+
|
|
63
|
+
return () => {
|
|
64
|
+
setAriaHidden()
|
|
65
|
+
document.body.querySelector('.wc-availability__link')?.focus()
|
|
66
|
+
document.removeEventListener('keydown', onKeyDown)
|
|
67
|
+
}
|
|
68
|
+
}, [panelElements])
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export { useFocusedElements }
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useEffect } from 'react'
|
|
2
|
+
|
|
3
|
+
export const useTextareaAutosize = (textAreaRef, value) => {
|
|
4
|
+
useEffect(() => {
|
|
5
|
+
if (textAreaRef) {
|
|
6
|
+
textAreaRef.style.height = '0px'
|
|
7
|
+
|
|
8
|
+
const scrollHeight = textAreaRef.scrollHeight
|
|
9
|
+
|
|
10
|
+
textAreaRef.style.height = `${scrollHeight}px`
|
|
11
|
+
}
|
|
12
|
+
}, [textAreaRef, value])
|
|
13
|
+
}
|
package/src/client/index.jsx
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { createRoot } from 'react-dom/client'
|
|
3
|
+
import { ChatSdk } from '@nice-devone/nice-cxone-chat-web-sdk'
|
|
3
4
|
import { Availability } from './components/availability/availability.jsx'
|
|
4
5
|
import { checkAvailability } from './lib/check-availability'
|
|
6
|
+
import { AppProvider } from './store/AppProvider.jsx'
|
|
7
|
+
import { CUSTOMER_ID_STORAGE_KEY } from './store/reducer.js'
|
|
5
8
|
|
|
6
9
|
export async function init (container, options) {
|
|
10
|
+
const sdk = new ChatSdk({
|
|
11
|
+
brandId: options.brandId,
|
|
12
|
+
channelId: options.channelId,
|
|
13
|
+
customerId: window.localStorage.getItem(CUSTOMER_ID_STORAGE_KEY) || '',
|
|
14
|
+
environment: options.environment
|
|
15
|
+
})
|
|
16
|
+
|
|
7
17
|
const root = createRoot(container)
|
|
8
18
|
let availability
|
|
9
19
|
try {
|
|
@@ -12,5 +22,9 @@ export async function init (container, options) {
|
|
|
12
22
|
} catch (e) {
|
|
13
23
|
availability = 'UNAVAILABLE'
|
|
14
24
|
}
|
|
15
|
-
root.render(
|
|
25
|
+
root.render(
|
|
26
|
+
<AppProvider sdk={sdk} availability={availability}>
|
|
27
|
+
<Availability availability={availability} />
|
|
28
|
+
</AppProvider>
|
|
29
|
+
)
|
|
16
30
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import sanitizeHtml from 'sanitize-html'
|
|
2
|
+
|
|
3
|
+
export const transformMessage = message => {
|
|
4
|
+
return {
|
|
5
|
+
id: message.id,
|
|
6
|
+
text: sanitizeHtml(message.messageContent?.text),
|
|
7
|
+
createdAt: new Date(message.createdAt),
|
|
8
|
+
user: message.authorEndUserIdentity?.fullName?.trim() || null,
|
|
9
|
+
assignee: message.authorUser?.firstName || null,
|
|
10
|
+
direction: message.direction
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const transformMessages = messages => messages.map(message => transformMessage(message))
|