@defra/flood-webchat 0.0.1-beta.9 → 1.0.1

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.
Files changed (48) hide show
  1. package/README.md +6 -0
  2. package/dist/client.js +1 -329
  3. package/dist/server.js +1 -286
  4. package/main.scss +13 -8
  5. package/package.json +28 -21
  6. package/src/client/components/availability/availability.jsx +104 -65
  7. package/src/client/components/availability/availability.scss +20 -32
  8. package/src/client/components/chat/chat.jsx +239 -0
  9. package/src/client/components/chat/chat.scss +258 -0
  10. package/src/client/components/errorSummary/error-summary.jsx +34 -0
  11. package/src/client/components/live-region.jsx +55 -0
  12. package/src/client/components/message/message.jsx +21 -0
  13. package/src/client/components/panel/panel-footer.jsx +9 -0
  14. package/src/client/components/panel/panel-header.jsx +75 -0
  15. package/src/client/components/panel/panel.jsx +163 -0
  16. package/src/client/components/panel/panel.scss +112 -0
  17. package/src/client/components/screens/end-chat.jsx +77 -0
  18. package/src/client/components/screens/feedback.jsx +65 -0
  19. package/src/client/components/screens/pre-chat.jsx +62 -0
  20. package/src/client/components/screens/request-chat.jsx +183 -0
  21. package/src/client/components/screens/settings.jsx +97 -0
  22. package/src/client/components/screens/unavailable.jsx +23 -0
  23. package/src/client/components/skip-link.jsx +42 -0
  24. package/src/client/hooks/useFocusedElements.js +80 -0
  25. package/src/client/hooks/useTextareaAutosize.js +13 -0
  26. package/src/client/index.jsx +18 -1
  27. package/src/client/lib/agent-status-headline.js +17 -0
  28. package/src/client/lib/check-availability.js +1 -0
  29. package/src/client/lib/history.js +16 -0
  30. package/src/client/lib/message-notification.js +30 -0
  31. package/src/client/lib/transform-messages.js +47 -0
  32. package/src/client/scss/_objects.scss +33 -0
  33. package/src/client/scss/_settings.scss +101 -0
  34. package/src/client/scss/_tools.scss +33 -0
  35. package/src/client/scss/_utilities.scss +25 -0
  36. package/src/client/store/AppProvider.jsx +184 -0
  37. package/src/client/store/actions-map.js +153 -0
  38. package/src/client/store/constants.js +5 -0
  39. package/src/client/store/reducer.js +31 -0
  40. package/src/client/store/useApp.js +5 -0
  41. package/src/client/store/useChatSdk.js +37 -0
  42. package/src/server/lib/utils.js +25 -9
  43. package/webpack.config.mjs +0 -2
  44. package/webpack.prod.mjs +7 -0
  45. package/dist/client.js.map +0 -1
  46. package/dist/server.js.map +0 -1
  47. package/src/client/lib/external-stores.js +0 -4
  48. package/src/client/lib/external-sync-store.js +0 -42
@@ -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 (threadId) {
49
+ RightButtonComponent = MinimiseButtonComponent
50
+
51
+ if (isMobileAndNoHistory) {
52
+ RightButtonComponent = MinimiseButtonComponent
53
+ }
54
+ }
55
+
56
+ if (!threadId && 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' className='wc-header__title'>
69
+ Floodline webchat
70
+ </h2>
71
+
72
+ {RightButtonComponent}
73
+ </div>
74
+ )
75
+ }
@@ -0,0 +1,163 @@
1
+ import React, { useEffect, useState, useCallback } from 'react'
2
+ import { createPortal } from 'react-dom'
3
+ import { classnames } from '../../lib/classnames.js'
4
+
5
+ import { PreChat } from '../screens/pre-chat.jsx'
6
+ import { RequestChat } from '../screens/request-chat.jsx'
7
+ import { Chat } from '../chat/chat.jsx'
8
+ import { Unavailable } from '../screens/unavailable.jsx'
9
+ import { EndChat } from '../screens/end-chat.jsx'
10
+ import { Settings } from '../screens/settings.jsx'
11
+ import { Feedback } from '../screens/feedback.jsx'
12
+ import { LiveRegion } from '../live-region.jsx'
13
+
14
+ import { useApp } from '../../store/useApp.js'
15
+ import { useChatSdk } from '../../store/useChatSdk.js'
16
+ import { useFocusedElements } from '../../hooks/useFocusedElements.js'
17
+ import { historyReplaceState } from '../../lib/history.js'
18
+
19
+ export function Panel () {
20
+ const { sdk, availability, instigatorId, thread, threadId, setThread, setThreadId, setChatVisibility, setMessages, setUnseenCount, isMobile, isKeyboard } = useApp()
21
+ const { fetchThread, fetchMessages } = useChatSdk(sdk)
22
+
23
+ const [screen, setScreen] = useState(threadId ? 2 : 0)
24
+
25
+ useFocusedElements(screen)
26
+
27
+ const onEscapeKey = useCallback(e => {
28
+ if (e.key === 'Escape' || e.key === 'Esc') {
29
+ if (threadId) {
30
+ thread?.lastMessageSeen()
31
+ setUnseenCount(0)
32
+ }
33
+ setChatVisibility(false)
34
+ historyReplaceState()
35
+ }
36
+ }, [thread, threadId, setUnseenCount, setChatVisibility])
37
+
38
+ /**
39
+ * Focus previously focused element when closing the webchat
40
+ */
41
+ useEffect(() => {
42
+ return () => {
43
+ document.getElementById(instigatorId)?.focus()
44
+ }
45
+ }, [])
46
+
47
+ /**
48
+ * We need ammend classes on the body element to handle mobile behaviour
49
+ */
50
+ useEffect(() => {
51
+ document.body.classList.remove('wc-u-hidden')
52
+ document.getElementsByTagName('html')[0].classList.add('wc-u-html')
53
+ document.body.classList.add('wc-u-body')
54
+ return () => {
55
+ document.getElementsByTagName('html')[0].classList.remove('wc-u-html')
56
+ document.body.classList.remove('wc-u-body')
57
+ }
58
+ }, [isMobile])
59
+
60
+ /**
61
+ * Initializes the eventListener for pressing the escape key
62
+ */
63
+ useEffect(() => {
64
+ document.addEventListener('keydown', onEscapeKey)
65
+
66
+ return () => {
67
+ document.removeEventListener('keydown', onEscapeKey)
68
+ }
69
+ }, [onEscapeKey, setChatVisibility])
70
+
71
+ /**
72
+ * Recovers the thread if there is a threadId but no thread loaded in to state
73
+ */
74
+ useEffect(() => {
75
+ const recover = async () => {
76
+ try {
77
+ const fetchedThread = await fetchThread(threadId)
78
+ setThread(fetchedThread)
79
+
80
+ const fetchedMessages = await fetchMessages(fetchedThread, threadId)
81
+ setMessages(fetchedMessages)
82
+ } catch (err) {
83
+ console.log('[Chat Error] fetchThread', err)
84
+
85
+ setThreadId()
86
+ setThread()
87
+ setScreen(0)
88
+ }
89
+ }
90
+
91
+ if (threadId) {
92
+ if (thread) {
93
+ thread.lastMessageSeen()
94
+ setUnseenCount(0)
95
+ setScreen(2)
96
+ } else {
97
+ recover()
98
+ }
99
+ }
100
+ }, [thread, threadId])
101
+
102
+ const handleScreenChange = newScreen => e => {
103
+ e.preventDefault()
104
+ setScreen(newScreen)
105
+ }
106
+
107
+ const goToPreChatScreen = handleScreenChange(0)
108
+ const goToRequestChatScreen = handleScreenChange(1)
109
+ const goToChatScreen = handleScreenChange(2)
110
+ const goToEndChatScreen = handleScreenChange(3)
111
+ const goToFeedbackScreen = handleScreenChange(4)
112
+ const goToSettingsScreen = handleScreenChange(5)
113
+
114
+ let ScreenComponent
115
+
116
+ switch (screen) {
117
+ case 0:
118
+ ScreenComponent = <PreChat onContinue={goToRequestChatScreen} />
119
+ break
120
+ case 1:
121
+ ScreenComponent = <RequestChat onPreChatScreen={goToPreChatScreen} />
122
+ break
123
+ case 2:
124
+ ScreenComponent = <Chat onSettingsScreen={goToSettingsScreen} onEndChatScreen={goToEndChatScreen} />
125
+ break
126
+ case 3:
127
+ ScreenComponent = <EndChat onChatScreen={goToChatScreen} onEndChatConfirm={goToFeedbackScreen} />
128
+ break
129
+ case 4:
130
+ ScreenComponent = (
131
+ <Feedback onCancel={() => {
132
+ setChatVisibility(false)
133
+ historyReplaceState()
134
+ }}
135
+ />
136
+ )
137
+ break
138
+ case 5:
139
+ ScreenComponent = <Settings onCancel={goToChatScreen} />
140
+ break
141
+ default:
142
+ ScreenComponent = <PreChat onContinue={goToRequestChatScreen} />
143
+ }
144
+
145
+ if (availability === 'UNAVAILABLE') {
146
+ ScreenComponent = <Unavailable />
147
+ }
148
+
149
+ const Component = (
150
+ <div id='wc-panel' role='dialog' className={classnames('wc-panel', isKeyboard && 'wc-focus-visible')} tabIndex='-1' aria-modal='true' aria-labelledby='wc-header wc-subtitle'>
151
+ <div className='wc-panel__inner'>
152
+ {ScreenComponent}
153
+ </div>
154
+ <LiveRegion />
155
+ </div>
156
+ )
157
+
158
+ return (
159
+ <>
160
+ {createPortal(Component, document.body)}
161
+ </>
162
+ )
163
+ }
@@ -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,77 @@
1
+ import React, { useEffect } 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, threadId, setUnseenCount, isKeyboard } = useApp()
7
+
8
+ const confirmEndChat = async e => {
9
+ e.preventDefault()
10
+
11
+ window.localStorage.setItem('tmpThreadId', threadId)
12
+ setThreadId()
13
+ setMessages([])
14
+ setCustomerId()
15
+ thread.lastMessageSeen()
16
+ setUnseenCount(0)
17
+
18
+ // End chat if still open
19
+ if (agentStatus !== 'closed') {
20
+ thread.endChat()
21
+ }
22
+
23
+ onEndChatConfirm(e)
24
+ }
25
+
26
+ useEffect(() => {
27
+ if (isKeyboard) {
28
+ setTimeout(() => {
29
+ document.querySelector('#confirm-endchat').focus()
30
+ }, 10)
31
+ }
32
+ }, [])
33
+
34
+ const handleKeyPress = e => {
35
+ if ((e.key === 'Enter' || e.key === ' ') && e.target.id === 'confirm-endchat') {
36
+ confirmEndChat(e)
37
+ }
38
+ if ((e.key === 'Enter' || e.key === ' ') && e.target.id === 'resume-chat') {
39
+ onChatScreen(e)
40
+ }
41
+ }
42
+
43
+ return (
44
+ <>
45
+ <PanelHeader />
46
+ <div className='wc-body'>
47
+ <div className='wc-content'>
48
+ <h3 id='wc-subtitle' className='wc-heading' aria-live='polite'>Are you sure you want to end the chat?</h3>
49
+ <div className='govuk-button-group'>
50
+ <a
51
+ id='confirm-endchat'
52
+ href='#endChat'
53
+ role='button'
54
+ className='wc-button govuk-button'
55
+ data-module='govuk-button'
56
+ onClick={confirmEndChat}
57
+ onKeyDown={handleKeyPress}
58
+ >
59
+ Yes, end chat
60
+ </a>
61
+ <a
62
+ id='resume-chat'
63
+ href='#resumeChat'
64
+ role='button'
65
+ className='wc-link govuk-link'
66
+ data-module='govuk-button'
67
+ onClick={onChatScreen}
68
+ onKeyDown={handleKeyPress}
69
+ >
70
+ No, resume chat
71
+ </a>
72
+ </div>
73
+ </div>
74
+ </div>
75
+ </>
76
+ )
77
+ }
@@ -0,0 +1,65 @@
1
+ import React from 'react'
2
+ import { PanelHeader } from '../panel/panel-header.jsx'
3
+
4
+ export function Feedback ({ onCancel }) {
5
+ const feedbackSend = () => {
6
+ const tmpThreadId = window.localStorage.getItem('tmpThreadId')
7
+ window.location.href = `https://defragroup.eu.qualtrics.com/jfe/form/SV_8dgFSJcxxIfqx5Y?Id=${tmpThreadId}&Source=${window.location.href}`
8
+ window.localStorage.removeItem('tmpThreadId')
9
+ }
10
+
11
+ const feedbackClose = async e => {
12
+ window.localStorage.removeItem('tmpThreadId')
13
+ onCancel(e)
14
+ }
15
+
16
+ const handleKeyPress = e => {
17
+ if (e.key === 'Enter' || e.key === ' ') {
18
+ if (e.target.id === 'feedback-close') {
19
+ feedbackClose(e)
20
+ }
21
+ if (e.target.id === 'feedback-send') {
22
+ feedbackSend()
23
+ }
24
+ }
25
+ }
26
+
27
+ return (
28
+ <>
29
+ <PanelHeader />
30
+
31
+ <div className='wc-body'>
32
+ <div className='wc-content'>
33
+ <h3 id='wc-subtitle' className='wc-heading'>Give feedback on Floodline webchat</h3>
34
+ <p>We're running webchat as a trial. you can&nbsp;
35
+ <a
36
+ id='feedback-send'
37
+ className='govuk-link'
38
+ href='#'
39
+ target='_blank'
40
+ rel='noreferrer'
41
+ onKeyDown={handleKeyPress}
42
+ onClick={feedbackSend}
43
+ >
44
+ give feedback
45
+ </a>
46
+ &nbsp;to help us improve it.
47
+ </p>
48
+ <p>
49
+ <a
50
+ id='feedback-close'
51
+ className='wc-link govuk-link'
52
+ href='#'
53
+ data-module='govuk-button'
54
+ role='button'
55
+ onKeyDown={handleKeyPress}
56
+ onClick={feedbackClose}
57
+ >
58
+ Close
59
+ </a>
60
+ </p>
61
+ </div>
62
+ </div>
63
+ </>
64
+ )
65
+ }
@@ -0,0 +1,62 @@
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
+ <div className='wc-content'>
11
+ <h3 id='wc-subtitle' className='wc-heading' aria-live='polite'>What you can use webchat for</h3>
12
+ <p>Webchat lets you talk directly to a Floodline adviser.</p>
13
+
14
+ <p>You can use webchat to get:</p>
15
+ <ul className='wc-list'>
16
+ <li>current flood warnings and alerts in your area</li>
17
+ <li>information on the flood warning service</li>
18
+ <li>advice on what to do before, during and after a flood</li>
19
+ </ul>
20
+ <p>
21
+ There are other ways to&nbsp;
22
+ <a className='govuk-link' href='https://www.gov.uk/sign-up-for-flood-warnings' target='_blank' rel='noreferrer'>
23
+ sign up for flood warnings
24
+ </a>
25
+ &nbsp;and&nbsp;
26
+ <a className='govuk-link' href='https://www.fws.environment-agency.gov.uk/app/olr/login' target='_blank' rel='noreferrer'>
27
+ manage your flood warnings account
28
+ </a>.
29
+ </p>
30
+ <p>
31
+ Do not use webchat to&nbsp;
32
+ <a
33
+ className='govuk-link'
34
+ href='https://www.gov.uk/report-flood-cause'
35
+ target='_blank'
36
+ rel='noreferrer'
37
+ >
38
+ report a flood
39
+ </a>.
40
+ </p>
41
+
42
+ <button className='govuk-button wc-button govuk-!-margin-top-1' data-module='govuk-button' onClick={onContinue}>Continue</button>
43
+
44
+ <h3 className='wc-heading'>Other flood information</h3>
45
+
46
+ <p>Call Floodline for all other flood and flood warning information.</p>
47
+ <p>
48
+ <strong>Floodline helpline</strong>
49
+ <br />
50
+ Telephone: 0345 988 1188
51
+ <br />
52
+ Textphone: 0345 602 6340
53
+ <br />
54
+ Open 24 hours a day, 7 days a week
55
+ <br />
56
+ <a className='govuk-link' href='https://gov.uk/call-charges' target='_blank' rel='noreferrer'>Find out more about call charges</a>
57
+ </p>
58
+ </div>
59
+ </div>
60
+ </>
61
+ )
62
+ }