@defra/flood-webchat 0.0.1-beta.2 → 0.0.1-beta.21
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 +1607 -212
- 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 +6 -2
- package/src/client/components/availability/availability.jsx +44 -46
- package/src/client/components/availability/availability.scss +105 -5
- package/src/client/components/chat/chat.jsx +169 -0
- package/src/client/components/chat/chat.scss +143 -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 +41 -0
- package/src/client/components/panel/panel.jsx +119 -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 +65 -0
- package/src/client/components/screens/request-chat.jsx +156 -0
- package/src/client/components/screens/settings.jsx +67 -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/message-notification.js +36 -0
- package/src/client/lib/transform-messages.js +35 -0
- package/src/client/store/AppProvider.jsx +139 -0
- package/src/client/store/actions-map.js +119 -0
- package/src/client/store/constants.js +3 -0
- package/src/client/store/reducer.js +28 -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,143 @@
|
|
|
1
|
+
@keyframes blink {
|
|
2
|
+
50% {
|
|
3
|
+
fill: transparent
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.wc-status {
|
|
8
|
+
display: flex;
|
|
9
|
+
border-bottom: 1px solid $govuk-border-colour;
|
|
10
|
+
padding: govuk-spacing(2);
|
|
11
|
+
|
|
12
|
+
&__availability {
|
|
13
|
+
margin-bottom: 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
&__link {
|
|
17
|
+
margin-left: auto;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.defra-context-footer {
|
|
22
|
+
border-top:2px solid govuk-colour('blue');
|
|
23
|
+
margin-top: 30px;
|
|
24
|
+
padding-top: 10px;
|
|
25
|
+
@include mq ($from: tablet) {
|
|
26
|
+
margin-top: 40px;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.wc-chat {
|
|
31
|
+
&__messages {
|
|
32
|
+
overflow: auto;
|
|
33
|
+
list-style: none;
|
|
34
|
+
padding: 0;
|
|
35
|
+
margin: govuk-spacing(2) 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
&__message {
|
|
39
|
+
margin-bottom: govuk-spacing(1);
|
|
40
|
+
|
|
41
|
+
&.inbound {
|
|
42
|
+
text-align: right;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
&__from {
|
|
47
|
+
color: govuk-colour('dark-grey');
|
|
48
|
+
margin-bottom: 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
&__text {
|
|
52
|
+
padding: govuk-spacing(2);
|
|
53
|
+
margin-bottom: 0;
|
|
54
|
+
display: inline-block;
|
|
55
|
+
|
|
56
|
+
&.inbound {
|
|
57
|
+
color: govuk-colour('white');
|
|
58
|
+
background-color: govuk-colour('blue');
|
|
59
|
+
border-radius: 8px 0 8px 8px;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
&.outbound {
|
|
63
|
+
background-color: govuk-colour('light-grey');
|
|
64
|
+
border-radius: 0 8px 8px;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
svg {
|
|
68
|
+
display: block;
|
|
69
|
+
color: govuk-colour('dark-grey');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
svg circle {
|
|
73
|
+
animation: 1s blink infinite;
|
|
74
|
+
fill: currentcolor;
|
|
75
|
+
|
|
76
|
+
&:nth-child(2) {
|
|
77
|
+
animation-delay: 250ms
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
&:nth-child(3) {
|
|
81
|
+
animation-delay: 500ms
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.wc-form {
|
|
88
|
+
position: relative;
|
|
89
|
+
display: flex;
|
|
90
|
+
flex-direction: row;
|
|
91
|
+
align-items: center;
|
|
92
|
+
width: 100%;
|
|
93
|
+
|
|
94
|
+
&__label {
|
|
95
|
+
position: absolute;
|
|
96
|
+
top: 9px;
|
|
97
|
+
left: 5px;
|
|
98
|
+
z-index: 3;
|
|
99
|
+
margin-bottom: 0;
|
|
100
|
+
color: govuk-colour('dark-grey');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
&__textarea {
|
|
104
|
+
position: relative;
|
|
105
|
+
width: 100%;
|
|
106
|
+
margin-bottom: 0;
|
|
107
|
+
min-height: auto;
|
|
108
|
+
max-height: 120px;
|
|
109
|
+
resize: none;
|
|
110
|
+
border: none;
|
|
111
|
+
overflow-x: hidden;
|
|
112
|
+
overscroll-behavior: contain;
|
|
113
|
+
z-index: 4;
|
|
114
|
+
background-color: transparent;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
&__button {
|
|
118
|
+
margin: auto 0 0 govuk-spacing(3);
|
|
119
|
+
width: auto;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.wc-footer {
|
|
124
|
+
&__settings {
|
|
125
|
+
@include govuk-responsive-padding(2);
|
|
126
|
+
|
|
127
|
+
display: flex;
|
|
128
|
+
flex-direction: row;
|
|
129
|
+
border-top: 1px solid govuk-colour('black');
|
|
130
|
+
|
|
131
|
+
&-link {
|
|
132
|
+
&:first-child {
|
|
133
|
+
margin-right: govuk-spacing(3);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
&__input {
|
|
139
|
+
@include govuk-responsive-padding(2);
|
|
140
|
+
|
|
141
|
+
border-top: 1px solid govuk-colour('black');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -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='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'>
|
|
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 govuk-body', outbound ? 'outbound' : 'inbound')}>
|
|
11
|
+
<div className='wc-chat__from govuk-!-font-size-14'>
|
|
12
|
+
{messageOwnerSameAsPrevious ? null : <span>{outbound ? message.assignee : 'You'}</span>}
|
|
13
|
+
<span className='govuk-visually-hidden'>:</span>
|
|
14
|
+
</div>
|
|
15
|
+
<p className={classnames('wc-chat__text', 'govuk-body-s', outbound ? 'outbound' : 'inbound')}>
|
|
16
|
+
{message.text}
|
|
17
|
+
</p>
|
|
18
|
+
</li>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { useApp } from '../../store/useApp.js'
|
|
3
|
+
|
|
4
|
+
export function PanelHeader () {
|
|
5
|
+
const { thread, setChatVisibility, setUnseenCount } = useApp()
|
|
6
|
+
|
|
7
|
+
const onClose = e => {
|
|
8
|
+
e.preventDefault()
|
|
9
|
+
setChatVisibility(false)
|
|
10
|
+
thread.lastMessageSeen()
|
|
11
|
+
setUnseenCount(0)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let ButtonComponent = (
|
|
15
|
+
<button className='wc-header__close' aria-label='Close the webchat' onClick={onClose}>
|
|
16
|
+
<svg aria-hidden='true' focusable='false' width='20' height='20' viewBox='0 0 20 20'>
|
|
17
|
+
<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' />
|
|
18
|
+
</svg>
|
|
19
|
+
</button>
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
if (thread) {
|
|
23
|
+
ButtonComponent = (
|
|
24
|
+
<button className='wc-header__hide' aria-label='Minimise the webchat' onClick={onClose}>
|
|
25
|
+
<svg aria-hidden='true' focusable='false' width='20' height='20' viewBox='0 0 20 20'>
|
|
26
|
+
<path d='M10 14.4l-7-7L4.4 6l5.6 5.6L15.6 6 17 7.4l-7 7z' fill='currentColor' />
|
|
27
|
+
</svg>
|
|
28
|
+
</button>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className='wc-header'>
|
|
34
|
+
<h2 id='wc-header-title' className='wc-header__title govuk-heading-s'>
|
|
35
|
+
Floodline Webchat
|
|
36
|
+
</h2>
|
|
37
|
+
|
|
38
|
+
{ButtonComponent}
|
|
39
|
+
</div>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
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
|
+
|
|
11
|
+
import { useApp } from '../../store/useApp.js'
|
|
12
|
+
import { useChatSdk } from '../../store/useChatSdk.js'
|
|
13
|
+
import { useFocusedElements } from '../../hooks/useFocusedElements.js'
|
|
14
|
+
|
|
15
|
+
export function Panel () {
|
|
16
|
+
const { sdk, availability, thread, threadId, setThread, setThreadId, setChatVisibility, setMessages, setUnseenCount } = useApp()
|
|
17
|
+
const { fetchThread, fetchMessages } = useChatSdk(sdk)
|
|
18
|
+
|
|
19
|
+
const [screen, setScreen] = useState(threadId ? 2 : 0)
|
|
20
|
+
|
|
21
|
+
useFocusedElements(screen)
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Initializes the eventListener for pressing the escape key
|
|
25
|
+
*/
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const escapeKeyEvent = document.addEventListener('keydown', onEscapeKey)
|
|
28
|
+
|
|
29
|
+
return () => {
|
|
30
|
+
document.removeEventListener('keydown', escapeKeyEvent)
|
|
31
|
+
}
|
|
32
|
+
}, [])
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Recovers the thread if there is a threadId but no thread loaded in to state
|
|
36
|
+
*/
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
const recover = async () => {
|
|
39
|
+
try {
|
|
40
|
+
const fetchedThread = await fetchThread(threadId)
|
|
41
|
+
setThread(fetchedThread)
|
|
42
|
+
|
|
43
|
+
const fetchedMessages = await fetchMessages(fetchedThread, threadId)
|
|
44
|
+
setMessages(fetchedMessages)
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.log('[Chat Error] fetchThread', err)
|
|
47
|
+
|
|
48
|
+
setThreadId()
|
|
49
|
+
setThread()
|
|
50
|
+
setScreen(0)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (threadId) {
|
|
55
|
+
if (thread) {
|
|
56
|
+
thread.lastMessageSeen()
|
|
57
|
+
setUnseenCount(0)
|
|
58
|
+
setScreen(2)
|
|
59
|
+
} else {
|
|
60
|
+
recover()
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}, [thread, threadId])
|
|
64
|
+
|
|
65
|
+
const onEscapeKey = useCallback(e => {
|
|
66
|
+
if (e.key === 'Escape' || e.key === 'Esc') {
|
|
67
|
+
setChatVisibility(false)
|
|
68
|
+
}
|
|
69
|
+
}, [])
|
|
70
|
+
|
|
71
|
+
const handleScreenChange = newScreen => e => {
|
|
72
|
+
e.preventDefault()
|
|
73
|
+
setScreen(newScreen)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const goToPreChatScreen = handleScreenChange(0)
|
|
77
|
+
const goToRequestChatScreen = handleScreenChange(1)
|
|
78
|
+
const goToChatScreen = handleScreenChange(2)
|
|
79
|
+
const goToEndChatScreen = handleScreenChange(3)
|
|
80
|
+
const goToSettingsScreen = handleScreenChange(5)
|
|
81
|
+
|
|
82
|
+
let ScreenComponent
|
|
83
|
+
|
|
84
|
+
switch (screen) {
|
|
85
|
+
case 0:
|
|
86
|
+
ScreenComponent = <PreChat onContinue={goToRequestChatScreen} />
|
|
87
|
+
break
|
|
88
|
+
case 1:
|
|
89
|
+
ScreenComponent = <RequestChat onPreChatScreen={goToPreChatScreen} />
|
|
90
|
+
break
|
|
91
|
+
case 2:
|
|
92
|
+
ScreenComponent = <Chat onSettingsScreen={goToSettingsScreen} onEndChatScreen={goToEndChatScreen} />
|
|
93
|
+
break
|
|
94
|
+
case 3:
|
|
95
|
+
ScreenComponent = <EndChat onChatScreen={goToChatScreen} />
|
|
96
|
+
break
|
|
97
|
+
case 5:
|
|
98
|
+
ScreenComponent = <Settings onCancel={goToChatScreen} />
|
|
99
|
+
break
|
|
100
|
+
default:
|
|
101
|
+
ScreenComponent = <PreChat onContinue={goToRequestChatScreen} />
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (availability === 'UNAVAILABLE') {
|
|
105
|
+
ScreenComponent = <Unavailable />
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const Component = (
|
|
109
|
+
<div id='wc-panel' role='dialog' className='wc-panel' tabIndex='-1' aria-modal='true' aria-labelledby='wc-header-title'>
|
|
110
|
+
{ScreenComponent}
|
|
111
|
+
</div>
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<>
|
|
116
|
+
{createPortal(Component, document.body)}
|
|
117
|
+
</>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
@@ -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 ({ onChatScreen }) {
|
|
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'>Yes, end chat</a>
|
|
12
|
+
<a href='#' className='govuk-link govuk-!-font-size-16' onClick={onChatScreen}>No, resume chat</a>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
</>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { PanelHeader } from '../panel/panel-header.jsx'
|
|
3
|
+
|
|
4
|
+
export function PreChat ({ onContinue }) {
|
|
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' target='_blank' rel='noreferrer'>
|
|
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' target='_blank' rel='noreferrer'>
|
|
31
|
+
manage your flood warnings account
|
|
32
|
+
</a>.
|
|
33
|
+
</p>
|
|
34
|
+
<p className='govuk-body-s'>
|
|
35
|
+
{'Do not use webchat to '}
|
|
36
|
+
<a
|
|
37
|
+
className='govuk-link'
|
|
38
|
+
href='https://www.gov.uk/report-flood-cause'
|
|
39
|
+
target='_blank'
|
|
40
|
+
rel='noreferrer'
|
|
41
|
+
>
|
|
42
|
+
report a flood
|
|
43
|
+
</a>.
|
|
44
|
+
</p>
|
|
45
|
+
|
|
46
|
+
<button className='govuk-button govuk-!-font-size-16' data-module='govuk-button' onClick={onContinue}>Continue</button>
|
|
47
|
+
|
|
48
|
+
<h3 className='govuk-heading-s'>Other flood information</h3>
|
|
49
|
+
|
|
50
|
+
<p className='govuk-body-s'>Call Floodline for all other flood and flood warning information.</p>
|
|
51
|
+
<p className='govuk-body-s'>
|
|
52
|
+
<strong>Floodline helpline</strong>
|
|
53
|
+
<br />
|
|
54
|
+
Telephone: 0345 988 1188
|
|
55
|
+
<br />
|
|
56
|
+
Textphone: 0345 602 6340
|
|
57
|
+
<br />
|
|
58
|
+
Open 24 hours a day, 7 days a week
|
|
59
|
+
<br />
|
|
60
|
+
<a className='govuk-link' href='https://gov.uk/call-charges' target='_blank' rel='noreferrer'>Find out more about call charges</a>
|
|
61
|
+
</p>
|
|
62
|
+
</div>
|
|
63
|
+
</>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
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
|
+
import { ErrorSummary } from '../errorSummary/error-summary.jsx'
|
|
9
|
+
|
|
10
|
+
const QUESTION_MAX_LENGTH = 500
|
|
11
|
+
|
|
12
|
+
export function RequestChat ({ onPreChatScreen }) {
|
|
13
|
+
const { sdk, setCustomerId, setThreadId, setThread } = useApp()
|
|
14
|
+
const { fetchCustomerId, fetchThread } = useChatSdk(sdk)
|
|
15
|
+
|
|
16
|
+
const [errors, setErrors] = useState({})
|
|
17
|
+
const [questionLength, setQuestionLength] = useState(0)
|
|
18
|
+
|
|
19
|
+
const nameRef = useRef()
|
|
20
|
+
const questionRef = useRef()
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (Object.keys(errors).length) {
|
|
24
|
+
document.querySelector('.govuk-error-summary')?.focus()
|
|
25
|
+
}
|
|
26
|
+
}, [errors])
|
|
27
|
+
|
|
28
|
+
const onQuestionChange = e => {
|
|
29
|
+
setQuestionLength(e.target.value.length)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const onRequestChat = async e => {
|
|
33
|
+
e.preventDefault()
|
|
34
|
+
|
|
35
|
+
const errs = {}
|
|
36
|
+
|
|
37
|
+
if (nameRef.current.value.length === 0) {
|
|
38
|
+
errs.name = 'Enter your name'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (questionRef.current.value.length === 0) {
|
|
42
|
+
errs.question = 'Enter your question'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (questionRef.current.value.length > QUESTION_MAX_LENGTH) {
|
|
46
|
+
errs.question = 'Your question must be 500 characters or less'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (Object.keys(errs).length === 0) {
|
|
50
|
+
try {
|
|
51
|
+
const threadId = uuid.v4()
|
|
52
|
+
const customerId = await fetchCustomerId()
|
|
53
|
+
const thread = await fetchThread(threadId)
|
|
54
|
+
|
|
55
|
+
sdk.getCustomer().setName(nameRef.current.value)
|
|
56
|
+
|
|
57
|
+
await thread.startChat(questionRef.current.value || 'Begin conversation')
|
|
58
|
+
|
|
59
|
+
setCustomerId(customerId)
|
|
60
|
+
setThreadId(threadId)
|
|
61
|
+
setThread(thread)
|
|
62
|
+
} catch (err) {
|
|
63
|
+
console.log('[Request Chat Error]', err)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
setErrors(errs)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const isQuestionLengthValid = QUESTION_MAX_LENGTH >= questionLength
|
|
71
|
+
const questionLengthRemaining = QUESTION_MAX_LENGTH - questionLength
|
|
72
|
+
const questionLengthExceeded = questionLength - QUESTION_MAX_LENGTH
|
|
73
|
+
|
|
74
|
+
let questionHint = `You have ${questionLengthRemaining} characters remaining`
|
|
75
|
+
|
|
76
|
+
if (!isQuestionLengthValid) {
|
|
77
|
+
questionHint = `You have ${questionLengthExceeded} characters too many`
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<>
|
|
82
|
+
<PanelHeader />
|
|
83
|
+
|
|
84
|
+
<div className='wc-body'>
|
|
85
|
+
<a href='#' className='wc-back-link govuk-back-link' onClick={onPreChatScreen}>
|
|
86
|
+
What you can use webchat for
|
|
87
|
+
</a>
|
|
88
|
+
|
|
89
|
+
<ErrorSummary errors={errors} />
|
|
90
|
+
|
|
91
|
+
<h3 className='govuk-heading-s' aria-live='polite'>
|
|
92
|
+
Your name and question
|
|
93
|
+
</h3>
|
|
94
|
+
|
|
95
|
+
<form>
|
|
96
|
+
<div className={classnames('govuk-form-group', errors.name && 'govuk-form-group--error')}>
|
|
97
|
+
<label className='govuk-label' htmlFor='wc-name'>
|
|
98
|
+
Your name
|
|
99
|
+
</label>
|
|
100
|
+
{errors.name && (
|
|
101
|
+
<p className='govuk-error-message'>
|
|
102
|
+
<span className='govuk-visually-hidden'>Error:</span> {errors.name}
|
|
103
|
+
</p>
|
|
104
|
+
)}
|
|
105
|
+
<input
|
|
106
|
+
ref={nameRef}
|
|
107
|
+
className={classnames('govuk-input', errors.name && 'govuk-input--error')}
|
|
108
|
+
id='wc-name'
|
|
109
|
+
name='name'
|
|
110
|
+
type='text'
|
|
111
|
+
data-testid='request-chat-user-name'
|
|
112
|
+
/>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<div className='govuk-character-count' data-module='govuk-character-count' data-maxlength='500'>
|
|
116
|
+
<div className={classnames('govuk-form-group', errors.question && 'govuk-form-group--error')}>
|
|
117
|
+
<label className='govuk-label' htmlFor='wc-question'>
|
|
118
|
+
Your question
|
|
119
|
+
</label>
|
|
120
|
+
{errors.question && (
|
|
121
|
+
<p className='govuk-error-message'>
|
|
122
|
+
<span className='govuk-visually-hidden'>Error:</span>
|
|
123
|
+
{errors.question}
|
|
124
|
+
</p>
|
|
125
|
+
)}
|
|
126
|
+
<textarea
|
|
127
|
+
ref={questionRef}
|
|
128
|
+
id='wc-question'
|
|
129
|
+
name='question'
|
|
130
|
+
rows='5'
|
|
131
|
+
aria-describedby='wc-question-info'
|
|
132
|
+
onChange={onQuestionChange}
|
|
133
|
+
className={classnames('govuk-textarea', 'govuk-js-character-count', !isQuestionLengthValid || errors.question ? 'govuk-textarea--error' : '')}
|
|
134
|
+
data-testid='request-chat-user-question'
|
|
135
|
+
/>
|
|
136
|
+
<div id='wc-question-info' className='govuk-hint govuk-character-count__message' style={{ color: `${!isQuestionLengthValid ? '#d4351c' : ''}` }} aria-hidden='true'>
|
|
137
|
+
{questionHint}
|
|
138
|
+
</div>
|
|
139
|
+
<div className='govuk-character-count__sr-status govuk-visually-hidden' aria-live='polite'>
|
|
140
|
+
{questionHint}
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<button
|
|
146
|
+
className='govuk-button govuk-!-margin-top-1 govuk-!-font-size-16'
|
|
147
|
+
data-module='govuk-button'
|
|
148
|
+
onClick={onRequestChat}
|
|
149
|
+
>
|
|
150
|
+
Request chat
|
|
151
|
+
</button>
|
|
152
|
+
</form>
|
|
153
|
+
</div>
|
|
154
|
+
</>
|
|
155
|
+
)
|
|
156
|
+
}
|