@defra/flood-webchat 0.0.1-beta.8 → 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 (50) hide show
  1. package/README.md +89 -1
  2. package/dist/client.js +1 -754
  3. package/dist/server.js +1 -383
  4. package/main.scss +12 -8
  5. package/package.json +27 -22
  6. package/src/client/components/availability/availability.jsx +89 -54
  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 +55 -15
  15. package/src/client/components/panel/panel.jsx +128 -71
  16. package/src/client/components/panel/panel.scss +63 -22
  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 +50 -30
  20. package/src/client/components/screens/request-chat.jsx +177 -4
  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/index.js +15 -6
  43. package/src/server/lib/client.js +31 -37
  44. package/src/server/lib/utils.js +10 -2
  45. package/webpack.config.mjs +0 -2
  46. package/webpack.prod.mjs +7 -0
  47. package/dist/client.js.map +0 -1
  48. package/dist/server.js.map +0 -1
  49. package/src/client/lib/external-stores.js +0 -4
  50. package/src/client/lib/external-sync-store.js +0 -42
@@ -1,106 +1,163 @@
1
1
  import React, { useEffect, useState, useCallback } from 'react'
2
2
  import { createPortal } from 'react-dom'
3
+ import { classnames } from '../../lib/classnames.js'
3
4
 
4
- import { PanelHeader } from './panel-header.jsx'
5
5
  import { PreChat } from '../screens/pre-chat.jsx'
6
6
  import { RequestChat } from '../screens/request-chat.jsx'
7
-
8
- export const setAriaHidden = (bool) => {
9
- for (const node of document.body.children) {
10
- if (node.id !== 'wc-panel') {
11
- (bool) ? node.setAttribute('aria-hidden', 'true') : node.removeAttribute('aria-hidden')
12
- }
13
- }
14
- }
15
-
16
- export const getFocusableElements = () => {
17
- const selectors = [
18
- '#wc-panel a:not([disabled])',
19
- '#wc-panel button:not([disabled])',
20
- '#wc-panel select:not([disabled])',
21
- '#wc-panel input:not([disabled])',
22
- '#wc-panel textarea:not([disabled])',
23
- '#wc-panel *[tabindex="0"]:not([disabled])'
24
- ]
25
-
26
- const elements = document.body.querySelectorAll(selectors.join(','))
27
- return Array.from(elements).filter(e => !e.closest('[hidden]') && !e.closest('[aria-hidden="true"]'))
28
- }
29
-
30
- export function Panel ({ screenNumber, onClose }) {
31
- const [screen, setScreen] = useState(screenNumber)
32
- const [panelElements, setPanelElements] = useState([])
33
-
34
- const onKeyDown = useCallback((e) => {
35
- if (e.key === 'Escape' || e.key === 'Esc') return onClose()
36
-
37
- if (e.key === 'Tab') {
38
- const webchatPanelElement = document.querySelector('#wc-panel')
39
-
40
- if (webchatPanelElement && !document.activeElement.closest('#wc-panel')) {
41
- webchatPanelElement.focus()
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)
42
32
  }
33
+ setChatVisibility(false)
34
+ historyReplaceState()
35
+ }
36
+ }, [thread, threadId, setUnseenCount, setChatVisibility])
43
37
 
44
- if (e.shiftKey) {
45
- if (document.activeElement === panelElements[0]) {
46
- panelElements[panelElements.length - 1].focus()
47
- e.preventDefault()
48
- } else if (document.activeElement === document.querySelector('#wc-panel')) {
49
- panelElements[panelElements.length - 1]?.focus()
50
- e.preventDefault()
51
- }
52
- } else if (document.activeElement === panelElements[panelElements.length - 1]) {
53
- panelElements[0].focus()
54
- e.preventDefault()
55
- }
38
+ /**
39
+ * Focus previously focused element when closing the webchat
40
+ */
41
+ useEffect(() => {
42
+ return () => {
43
+ document.getElementById(instigatorId)?.focus()
56
44
  }
57
- }, [panelElements])
45
+ }, [])
58
46
 
47
+ /**
48
+ * We need ammend classes on the body element to handle mobile behaviour
49
+ */
59
50
  useEffect(() => {
60
- setPanelElements(getFocusableElements())
61
- }, [screen])
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])
62
59
 
60
+ /**
61
+ * Initializes the eventListener for pressing the escape key
62
+ */
63
63
  useEffect(() => {
64
- setAriaHidden(true)
65
- document.querySelector('#wc-panel').focus()
66
- document.addEventListener('keydown', onKeyDown)
64
+ document.addEventListener('keydown', onEscapeKey)
67
65
 
68
66
  return () => {
69
- setAriaHidden()
70
- document.body.querySelector('.wc-availability__link')?.focus()
71
- document.removeEventListener('keydown', onKeyDown)
67
+ document.removeEventListener('keydown', onEscapeKey)
72
68
  }
73
- }, [panelElements])
69
+ }, [onEscapeKey, setChatVisibility])
74
70
 
75
- const onBack = (e) => {
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 => {
76
103
  e.preventDefault()
77
- setScreen(screen - 1)
104
+ setScreen(newScreen)
78
105
  }
79
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
+
80
114
  let ScreenComponent
81
115
 
82
116
  switch (screen) {
83
117
  case 0:
84
- ScreenComponent = <PreChat screen={screen} setScreen={setScreen} />
118
+ ScreenComponent = <PreChat onContinue={goToRequestChatScreen} />
85
119
  break
86
120
  case 1:
87
- ScreenComponent = <RequestChat onBack={onBack} />
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} />
88
140
  break
89
141
  default:
90
- ScreenComponent = <PreChat screen={screen} setScreen={setScreen} />
142
+ ScreenComponent = <PreChat onContinue={goToRequestChatScreen} />
91
143
  }
92
144
 
93
- const Component = (
94
- <div id='wc-panel' className='wc-panel' role='dialog' tabIndex='-1' aria-modal='true' aria-labelledby='wc-header-title'>
95
- <PanelHeader screen={screen} isConnected={false} onBack={onBack} onClose={onClose} />
145
+ if (availability === 'UNAVAILABLE') {
146
+ ScreenComponent = <Unavailable />
147
+ }
96
148
 
97
- <div className='wc-body'>
98
- <div className='wc-content govuk-body-s'>
99
- {ScreenComponent}
100
- </div>
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}
101
153
  </div>
154
+ <LiveRegion />
102
155
  </div>
103
156
  )
104
157
 
105
- return createPortal(Component, document.body)
158
+ return (
159
+ <>
160
+ {createPortal(Component, document.body)}
161
+ </>
162
+ )
106
163
  }
@@ -1,26 +1,51 @@
1
1
  .wc-panel {
2
2
  display: flex;
3
- flex-direction: column;
4
3
  position: fixed;
5
- bottom: 0;
6
- right: 0;
4
+ flex-wrap: nowrap;
5
+ z-index: 999;
7
6
  top: 0;
7
+ right: 0;
8
+ bottom: 0;
8
9
  left: 0;
9
- width: 100%;
10
- height: 100%;
11
- background-color: govuk-colour('white');
12
- z-index: 999;
13
- outline: none;
14
-
15
- @include mq ($from: 'tablet') {
16
- bottom: 0;
17
- right: 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) {
18
16
  top: auto;
19
17
  left: auto;
20
18
  width: 400px;
21
19
  height: 540px;
22
- margin-bottom: 10px;
23
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;
24
49
  }
25
50
  }
26
51
 
@@ -29,43 +54,59 @@
29
54
  flex-direction: row;
30
55
  align-items: center;
31
56
  background-color: govuk-colour('black');
57
+ border-bottom: 1px solid govuk-colour('black');
58
+ padding: 3px;
32
59
 
33
60
  &__link {
34
61
  margin: 0 govuk-spacing(2);
35
62
  }
36
63
 
37
64
  &__title {
38
- padding: 0 0 0 govuk-spacing(2);
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
+ }
39
71
  color: govuk-colour('white');
40
72
  margin-bottom: 0;
41
73
  }
42
74
 
43
- &__close {
75
+ &__close,
76
+ &__hide,
77
+ &__back {
44
78
  display: flex;
45
79
  justify-content: center;
46
80
  color: govuk-colour('white');
47
81
  background: none;
48
82
  border: 0;
49
- padding: govuk-spacing(2);
83
+ padding: 7px;
50
84
  margin: 0 0 0 auto;
51
85
  cursor: pointer;
52
86
 
53
87
  &:focus {
54
- @include govuk-focused-text;
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
55
98
  }
56
99
  }
57
100
  }
58
101
 
59
102
  .wc-body {
60
- @include govuk-responsive-padding(3);
61
-
103
+ font-size: 16px;
62
104
  flex: 1;
63
105
  position: relative;
64
106
  overflow-y: auto;
65
107
  overscroll-behavior: contain;
66
- border: 1px solid govuk-colour('black');
67
108
  }
68
109
 
69
- .wc-back-link {
70
- margin: govuk-spacing(1) 0 govuk-spacing(6) 0;
110
+ .wc-content {
111
+ padding: 20px 15px;
71
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
+ }
@@ -1,42 +1,62 @@
1
1
  import React from 'react'
2
+ import { PanelHeader } from '../panel/panel-header.jsx'
2
3
 
3
- export function PreChat ({ screen, setScreen }) {
4
- const onContinue = (e) => {
5
- e.preventDefault()
6
- setScreen(screen + 1)
7
- }
8
-
4
+ export function PreChat ({ onContinue }) {
9
5
  return (
10
6
  <>
11
- <h3 className='govuk-heading-s'>What you can use webchat for</h3>
12
- <p className='govuk-body-s'>Webchat lets you talk directly to a Floodline adviser.</p>
7
+ <PanelHeader />
13
8
 
14
- <p className='govuk-body-s'>You can use webchat to get:</p>
15
- <ul className='govuk-list govuk-list--bullet govuk-body-s'>
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>
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>
20
13
 
21
- <p className='govuk-body-s'>There are other ways to <a className='govuk-link' href='https://www.gov.uk/sign-up-for-flood-warnings'>sign up for flood warnings</a> and <a className='govuk-link' href='https://www.fws.environment-agency.gov.uk/app/olr/login'>manage your flood warnings account</a>.</p>
22
- <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>
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>
23
41
 
24
- <button className='govuk-button' data-module='govuk-button' onClick={onContinue}>Continue</button>
42
+ <button className='govuk-button wc-button govuk-!-margin-top-1' data-module='govuk-button' onClick={onContinue}>Continue</button>
25
43
 
26
- <h3 className='govuk-heading-s'>Other flood information</h3>
44
+ <h3 className='wc-heading'>Other flood information</h3>
27
45
 
28
- <p className='govuk-body-s'>Call Floodline for all other flood and flood warning information.</p>
29
- <p className='govuk-body-s'>
30
- <strong>Floodline helpline</strong>
31
- <br />
32
- Telephone: 0345 988 1188
33
- <br />
34
- Textphone: 0345 602 6340
35
- <br />
36
- Open 24 hours a day, 7 days a week
37
- <br />
38
- <a className='govuk-link' href='https://gov.uk/call-charges'>Find out more about call charges</a>
39
- </p>
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>
40
60
  </>
41
61
  )
42
62
  }