@defra/flood-webchat 0.0.1-alpha.9 → 0.0.1-beta.2
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 +1 -83
- package/dist/client.js +445 -0
- package/dist/client.js.map +1 -0
- package/dist/server.js +362 -0
- package/dist/server.js.map +1 -0
- package/docs/development-guide.md +1 -2
- package/jest.config.mjs +29 -0
- package/main.scss +5 -741
- package/package.json +18 -13
- package/src/client/components/availability/availability.jsx +101 -0
- package/src/client/components/availability/availability.scss +89 -0
- package/src/client/index.jsx +16 -0
- package/src/client/lib/check-availability.js +7 -0
- package/src/client/lib/classnames.js +1 -0
- package/src/client/lib/external-stores.js +4 -0
- package/src/client/lib/external-sync-store.js +42 -0
- package/src/server/index.js +11 -6
- package/src/server/{client.js → lib/client.js} +32 -12
- package/src/server/{utils.js → lib/utils.js} +8 -13
- package/webpack.config.mjs +32 -0
- package/assets/audio/notification.mp3 +0 -0
- package/babel.config.cjs +0 -3
- package/dist/templates.js +0 -554
- package/src/client/index.js +0 -14
- package/src/client/lib/availability.js +0 -75
- package/src/client/lib/config.js +0 -17
- package/src/client/lib/keyboard.js +0 -149
- package/src/client/lib/notification.js +0 -59
- package/src/client/lib/nunjucks.js +0 -3
- package/src/client/lib/panel.js +0 -293
- package/src/client/lib/provider.js +0 -11
- package/src/client/lib/skiplink.js +0 -27
- package/src/client/lib/state.js +0 -82
- package/src/client/lib/transcript.js +0 -34
- package/src/client/lib/utils.js +0 -190
- package/src/client/lib/webchat.js +0 -1075
- package/src/style/_mixins.scss +0 -13
package/package.json
CHANGED
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@defra/flood-webchat",
|
|
3
|
-
"version": "0.0.1-
|
|
3
|
+
"version": "0.0.1-beta.2",
|
|
4
4
|
"description": "",
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"browser": "src/client/index.js",
|
|
5
|
+
"main": "dist/server.js",
|
|
6
|
+
"browser": "dist/client.js",
|
|
8
7
|
"scripts": {
|
|
9
|
-
"test": "npm run
|
|
10
|
-
"dev": "
|
|
8
|
+
"test": "npm run lint && npm run unit-test",
|
|
9
|
+
"dev": "(cd demo && webpack server)",
|
|
10
|
+
"build": "webpack",
|
|
11
11
|
"unit-test": "jest",
|
|
12
12
|
"lint": "npm run lint:js && npm run lint:scss",
|
|
13
13
|
"lint:fix": "npm run lint:js -- --fix && npm run lint:scss -- --fix",
|
|
14
14
|
"lint:js": "standard",
|
|
15
15
|
"lint:scss": "stylelint \"**/*.scss\"",
|
|
16
|
-
"prepare": "node -e \"try { require('husky').install() } catch (e) {if (e.code !== 'MODULE_NOT_FOUND') { throw e } }\""
|
|
17
|
-
"compile-templates": "mkdir -p dist && nunjucks-precompile templates > ./dist/templates.js"
|
|
16
|
+
"prepare": "node -e \"try { require('husky').install() } catch (e) {if (e.code !== 'MODULE_NOT_FOUND') { throw e } }\""
|
|
18
17
|
},
|
|
19
18
|
"standard": {
|
|
20
19
|
"ignore": [
|
|
@@ -30,6 +29,9 @@
|
|
|
30
29
|
"devDependencies": {
|
|
31
30
|
"@babel/core": "^7.22.10",
|
|
32
31
|
"@babel/preset-env": "^7.22.10",
|
|
32
|
+
"@babel/preset-react": "^7.22.15",
|
|
33
|
+
"@testing-library/react": "^14.0.0",
|
|
34
|
+
"@testing-library/user-event": "^14.5.1",
|
|
33
35
|
"@types/jest": "^29.5.4",
|
|
34
36
|
"babel-jest": "^29.6.4",
|
|
35
37
|
"babel-loader": "^9.1.3",
|
|
@@ -49,14 +51,17 @@
|
|
|
49
51
|
"stylelint-config-gds": "^1.0.0",
|
|
50
52
|
"webpack": "^5.88.2",
|
|
51
53
|
"webpack-cli": "^5.1.4",
|
|
52
|
-
"webpack-dev-server": "^4.15.1"
|
|
54
|
+
"webpack-dev-server": "^4.15.1",
|
|
55
|
+
"webpack-node-externals": "^3.0.0"
|
|
53
56
|
},
|
|
54
57
|
"peerDependencies": {
|
|
55
|
-
"govuk-frontend": "^
|
|
58
|
+
"govuk-frontend": "^4.0.0"
|
|
56
59
|
},
|
|
57
60
|
"dependencies": {
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
+
"axios": "^1.5.0",
|
|
62
|
+
"jwt-decode": "^3.1.2",
|
|
63
|
+
"querystring": "^0.2.1",
|
|
64
|
+
"react": "^18.2.0",
|
|
65
|
+
"react-dom": "^18.2.0"
|
|
61
66
|
}
|
|
62
67
|
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { classnames } from '../../lib/classnames'
|
|
3
|
+
import { useMessageThread, useWebchatOpenState } from '../../lib/external-stores'
|
|
4
|
+
|
|
5
|
+
export function Availability (props) {
|
|
6
|
+
const [isOpen, setOpen] = useWebchatOpenState()
|
|
7
|
+
const [isFixed, setFixed] = useState(false)
|
|
8
|
+
const buttonRef = useRef()
|
|
9
|
+
const onClick = () => {
|
|
10
|
+
setOpen(!isOpen)
|
|
11
|
+
}
|
|
12
|
+
const onKeyDown = event => {
|
|
13
|
+
if (event.key === ' ') {
|
|
14
|
+
event.preventDefault()
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const onKeyUp = event => {
|
|
18
|
+
if (event.key === ' ') {
|
|
19
|
+
setOpen(!isOpen)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const intersectionCallback = entries => {
|
|
24
|
+
const [entry] = entries
|
|
25
|
+
const isBelowFold = !entry.isIntersecting && entry.boundingClientRect.top > 0
|
|
26
|
+
setFixed(!isOpen && isBelowFold)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const observer = new window.IntersectionObserver(intersectionCallback, {
|
|
31
|
+
rootMargin: '35px'
|
|
32
|
+
})
|
|
33
|
+
const parentElement = buttonRef.current?.parentElement
|
|
34
|
+
if (parentElement) {
|
|
35
|
+
observer.observe(parentElement)
|
|
36
|
+
}
|
|
37
|
+
return () => {
|
|
38
|
+
if (parentElement) {
|
|
39
|
+
observer.unobserve(parentElement)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}, [buttonRef, isOpen])
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
document.documentElement.classList.toggle('wc-scroll-padding', isFixed)
|
|
46
|
+
document.body.classList.toggle('wc-scroll-padding', isFixed)
|
|
47
|
+
}, [isFixed])
|
|
48
|
+
|
|
49
|
+
switch (props.availability) {
|
|
50
|
+
case 'AVAILABLE':
|
|
51
|
+
return (
|
|
52
|
+
<div
|
|
53
|
+
className={classnames('wc-availability', isFixed && 'wc-availability--fixed')}
|
|
54
|
+
ref={buttonRef}
|
|
55
|
+
>
|
|
56
|
+
<div className='wc-availability__inner'>
|
|
57
|
+
<a
|
|
58
|
+
className='wc-availability__link'
|
|
59
|
+
href='#webchat' role='button' draggable='false'
|
|
60
|
+
onClick={onClick}
|
|
61
|
+
onKeyUp={onKeyUp}
|
|
62
|
+
onKeyDown={onKeyDown}
|
|
63
|
+
>
|
|
64
|
+
<AvailabilityContent />
|
|
65
|
+
</a>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
)
|
|
69
|
+
case 'EXISTING':
|
|
70
|
+
case 'UNAVAILABLE':
|
|
71
|
+
return (
|
|
72
|
+
<p className='govuk-body'>When it is available, a 'start chat' link will appear.</p>
|
|
73
|
+
)
|
|
74
|
+
default:
|
|
75
|
+
return (
|
|
76
|
+
<p className='govuk-body'>Checking availability</p>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function AvailabilityContent () {
|
|
82
|
+
const [thread] = useMessageThread()
|
|
83
|
+
const unreadMessageCount = thread.filter(message => !message.read).length
|
|
84
|
+
if (!thread.length) {
|
|
85
|
+
return (
|
|
86
|
+
<>
|
|
87
|
+
Start Chat
|
|
88
|
+
</>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
return (
|
|
92
|
+
<>
|
|
93
|
+
Show Chat {!!unreadMessageCount && (
|
|
94
|
+
<>
|
|
95
|
+
<span className='wc-availability__unseen'>{unreadMessageCount}</span>
|
|
96
|
+
<span className='govuk-visually-hidden'> {unreadMessageCount === 1 ? 'new message' : 'new messages'}</span>
|
|
97
|
+
</>
|
|
98
|
+
)}
|
|
99
|
+
</>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
|
|
2
|
+
.wc-availability__inner {
|
|
3
|
+
max-width: 960px;
|
|
4
|
+
margin-left: auto;
|
|
5
|
+
margin-right: auto;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.wc-availability--fixed {
|
|
9
|
+
position: fixed;
|
|
10
|
+
bottom: 0;
|
|
11
|
+
left: 0;
|
|
12
|
+
right: 0;
|
|
13
|
+
padding-left: 15px;
|
|
14
|
+
padding-right: 15px;
|
|
15
|
+
|
|
16
|
+
@include mq ($from: 'tablet') {
|
|
17
|
+
padding-left: 30px;
|
|
18
|
+
padding-right: 30px;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
background-color: govuk-colour('light-grey');
|
|
22
|
+
|
|
23
|
+
.wc-availability__link {
|
|
24
|
+
margin-top: 10px;
|
|
25
|
+
margin-bottom: 10px;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.wc-availability__link {
|
|
30
|
+
@include govuk-font($size: 19);
|
|
31
|
+
position:relative;
|
|
32
|
+
display:inline-block;
|
|
33
|
+
margin-right: 5px;
|
|
34
|
+
margin-bottom: 0;
|
|
35
|
+
fill: govuk-colour('black');
|
|
36
|
+
|
|
37
|
+
@include mq ($from: tablet) {
|
|
38
|
+
margin-right: 10px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.govuk-visually-hidden, .wc-availability__unseen {
|
|
42
|
+
pointer-events: none;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
&:hover {
|
|
46
|
+
color: $govuk-link-hover-colour;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
&:active {
|
|
50
|
+
color: govuk-colour('black');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
&:visited {
|
|
54
|
+
color: $govuk-link-colour;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
&:focus {
|
|
58
|
+
box-shadow: none;
|
|
59
|
+
background-color: transparent;
|
|
60
|
+
text-decoration: underline;
|
|
61
|
+
color: $govuk-focus-colour;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
&:hover:not(:focus):not(:active) {
|
|
65
|
+
color: $govuk-link-hover-colour;
|
|
66
|
+
|
|
67
|
+
.wc-availability__unseen {
|
|
68
|
+
background-color: govuk-colour('dark-blue');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.wc-availability__unseen {
|
|
74
|
+
display: inline-block;
|
|
75
|
+
position: relative;
|
|
76
|
+
border-radius: 10px;
|
|
77
|
+
background-color: govuk-colour('black');
|
|
78
|
+
color: govuk-colour('white');
|
|
79
|
+
font-weight: bold;
|
|
80
|
+
font-size: 14px;
|
|
81
|
+
padding: 0 6px;
|
|
82
|
+
margin-left: 5px;
|
|
83
|
+
|
|
84
|
+
@include mq ($from: tablet) {
|
|
85
|
+
top: -1px;
|
|
86
|
+
padding-left: 7px;
|
|
87
|
+
padding-right: 7px;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { createRoot } from 'react-dom/client'
|
|
3
|
+
import { Availability } from './components/availability/availability.jsx'
|
|
4
|
+
import { checkAvailability } from './lib/check-availability'
|
|
5
|
+
|
|
6
|
+
export async function init (container, options) {
|
|
7
|
+
const root = createRoot(container)
|
|
8
|
+
let availability
|
|
9
|
+
try {
|
|
10
|
+
const result = await checkAvailability(options.availabilityEndpoint)
|
|
11
|
+
availability = result.availability
|
|
12
|
+
} catch (e) {
|
|
13
|
+
availability = 'UNAVAILABLE'
|
|
14
|
+
}
|
|
15
|
+
root.render(<Availability availability={availability} />)
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const classnames = (...classes) => classes.filter(classname => classname && typeof classname === 'string').join(' ')
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useSyncExternalStore } from 'react'
|
|
2
|
+
|
|
3
|
+
export default class ExternalSyncStore {
|
|
4
|
+
constructor (initialValue) {
|
|
5
|
+
this._value = initialValue
|
|
6
|
+
this._listeners = []
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
update (value) {
|
|
10
|
+
this._value = value
|
|
11
|
+
this.emitChange()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
subscribe (listener) {
|
|
15
|
+
this._listeners = [...this._listeners, listener]
|
|
16
|
+
return () => {
|
|
17
|
+
this._listeners = this._listeners.filter(l => l !== listener)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
emitChange () {
|
|
22
|
+
for (const listener of this._listeners) {
|
|
23
|
+
listener()
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
getSnapshot () {
|
|
28
|
+
return this._value
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static create (initialValue) {
|
|
32
|
+
const store = new ExternalSyncStore(initialValue)
|
|
33
|
+
const boundSubscribe = store.subscribe.bind(store)
|
|
34
|
+
const boundGetSnapshot = store.getSnapshot.bind(store)
|
|
35
|
+
const boundUpdate = store.update.bind(store)
|
|
36
|
+
|
|
37
|
+
return () => [
|
|
38
|
+
useSyncExternalStore(boundSubscribe, boundGetSnapshot),
|
|
39
|
+
boundUpdate
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/server/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
const { authenticate, getHost, getIsOpen, getActivity } = require('./lib/client.js')
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Returns webchat availability
|
|
@@ -12,7 +12,7 @@ import { authenticate, getHost, getIsOpen, getActivity } from './client.js'
|
|
|
12
12
|
* @param options.maxQueueCount {string}
|
|
13
13
|
* @returns {Promise<{date: Date, availability: (string)}>}
|
|
14
14
|
*/
|
|
15
|
-
|
|
15
|
+
module.exports = async function getAvailability ({
|
|
16
16
|
clientId,
|
|
17
17
|
clientSecret,
|
|
18
18
|
accessKey,
|
|
@@ -25,6 +25,7 @@ export default async function getAvailability ({
|
|
|
25
25
|
|
|
26
26
|
// Cache authentication and re-authenticate when needed (lasts 1 hour?)
|
|
27
27
|
const { tenantId, token, tokenType } = await authenticate({ authorisation, accessKey, accessSecret })
|
|
28
|
+
|
|
28
29
|
const host = await getHost({ tenantId })
|
|
29
30
|
|
|
30
31
|
const [{ hasCapacity, hasAgentsAvailable }, isOpen] = await Promise.all([
|
|
@@ -32,12 +33,16 @@ export default async function getAvailability ({
|
|
|
32
33
|
getIsOpen({ token, tokenType, host, hoursEndpoint })
|
|
33
34
|
])
|
|
34
35
|
|
|
35
|
-
// Hours of operation
|
|
36
|
-
|
|
37
|
-
// Availability
|
|
38
36
|
const isAvailable = isOpen && hasAgentsAvailable && hasCapacity
|
|
39
37
|
const isExistingOnly = isOpen && hasAgentsAvailable && !hasCapacity
|
|
40
|
-
|
|
38
|
+
|
|
39
|
+
let availability = 'UNAVAILABLE'
|
|
40
|
+
|
|
41
|
+
if (isAvailable) {
|
|
42
|
+
availability = 'AVAILABLE'
|
|
43
|
+
} else if (isExistingOnly) {
|
|
44
|
+
availability = 'EXISTING'
|
|
45
|
+
}
|
|
41
46
|
|
|
42
47
|
return {
|
|
43
48
|
date: new Date(),
|
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
const querystring = require('querystring')
|
|
2
|
+
const axios = require('axios')
|
|
3
|
+
const jwtdecode = require('jwt-decode')
|
|
4
|
+
const { isWithinHours } = require('./utils.js')
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
const contentType = 'application/x-www-form-urlencoded'
|
|
7
|
+
|
|
8
|
+
const authenticate = async ({ authorisation, accessKey, accessSecret }) => {
|
|
6
9
|
const uri = 'https://cxone.niceincontact.com/auth/token'
|
|
7
10
|
|
|
8
11
|
const config = {
|
|
9
12
|
signal: AbortSignal.timeout(3000),
|
|
10
13
|
headers: {
|
|
11
14
|
Host: 'eu1.niceincontact.com',
|
|
12
|
-
'Content-Type':
|
|
15
|
+
'Content-Type': contentType,
|
|
13
16
|
Authorization: authorisation
|
|
14
17
|
}
|
|
15
18
|
}
|
|
@@ -19,54 +22,71 @@ export async function authenticate ({ authorisation, accessKey, accessSecret })
|
|
|
19
22
|
username: accessKey,
|
|
20
23
|
password: accessSecret
|
|
21
24
|
})
|
|
22
|
-
|
|
25
|
+
|
|
23
26
|
const auth = await axios.post(uri, body, config)
|
|
27
|
+
|
|
24
28
|
return {
|
|
25
29
|
token: auth.data.access_token,
|
|
26
30
|
tokenType: auth.data.token_type,
|
|
27
|
-
tenantId:
|
|
31
|
+
tenantId: jwtdecode(auth.data.id_token)?.tenantId
|
|
28
32
|
}
|
|
29
33
|
}
|
|
30
34
|
|
|
31
|
-
|
|
35
|
+
const getHost = async ({ tenantId }) => {
|
|
32
36
|
const uri = `https://cxone.niceincontact.com/.well-known/cxone-configuration?tenantId=${tenantId}`
|
|
37
|
+
|
|
33
38
|
const config = {
|
|
34
39
|
signal: AbortSignal.timeout(3000)
|
|
35
40
|
}
|
|
41
|
+
|
|
36
42
|
const api = await axios.get(uri, config)
|
|
43
|
+
|
|
37
44
|
return `api-${api.data.area}.niceincontact.com`
|
|
38
45
|
}
|
|
39
46
|
|
|
40
|
-
|
|
47
|
+
const getActivity = async ({ tokenType, token, host, skillEndpoint, maxQueueCount }) => {
|
|
41
48
|
const config = {
|
|
42
49
|
signal: AbortSignal.timeout(3000),
|
|
43
50
|
headers: {
|
|
44
51
|
Host: host,
|
|
45
52
|
Authorization: `${tokenType} ${token}`,
|
|
46
|
-
'Content-Type':
|
|
53
|
+
'Content-Type': contentType
|
|
47
54
|
}
|
|
48
55
|
}
|
|
49
56
|
const uri = `https://${host}${skillEndpoint}`
|
|
57
|
+
|
|
50
58
|
const skill = await axios.get(uri, config)
|
|
59
|
+
|
|
51
60
|
const activity = skill.data.skillActivity[0]
|
|
61
|
+
|
|
52
62
|
return {
|
|
53
63
|
hasCapacity: activity.queueCount < maxQueueCount,
|
|
54
64
|
hasAgentsAvailable: activity.agentsAvailable >= 1
|
|
55
65
|
}
|
|
56
66
|
}
|
|
57
67
|
|
|
58
|
-
|
|
68
|
+
const getIsOpen = async ({ host, token, tokenType, hoursEndpoint }) => {
|
|
59
69
|
const config = {
|
|
60
70
|
signal: AbortSignal.timeout(3000),
|
|
61
71
|
headers: {
|
|
62
72
|
Host: 'api-l36.niceincontact.com',
|
|
63
73
|
Authorization: `${tokenType} ${token}`,
|
|
64
|
-
'Content-Type':
|
|
74
|
+
'Content-Type': contentType
|
|
65
75
|
}
|
|
66
76
|
}
|
|
67
77
|
|
|
68
78
|
const uri = `https://${host}${hoursEndpoint}`
|
|
79
|
+
|
|
69
80
|
const hours = await axios.get(uri, config)
|
|
81
|
+
|
|
70
82
|
const days = hours.data.resultSet.hoursOfOperationProfiles[0].days
|
|
83
|
+
|
|
71
84
|
return isWithinHours(days)
|
|
72
85
|
}
|
|
86
|
+
|
|
87
|
+
module.exports = {
|
|
88
|
+
authenticate,
|
|
89
|
+
getHost,
|
|
90
|
+
getIsOpen,
|
|
91
|
+
getActivity
|
|
92
|
+
}
|
|
@@ -1,19 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
const base64Url = token.split('.')[1]
|
|
3
|
-
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
|
|
4
|
-
const jsonPayload = decodeURIComponent(Buffer.from(base64, 'base64').toString('ascii').split('').map(c => {
|
|
5
|
-
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
|
|
6
|
-
}).join(''))
|
|
7
|
-
|
|
8
|
-
return JSON.parse(jsonPayload).tenantId
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export const isWithinHours = (days) => {
|
|
1
|
+
const isWithinHours = days => {
|
|
12
2
|
const now = new Date()
|
|
13
3
|
const name = now.toLocaleDateString('en-GB', { weekday: 'long' })
|
|
14
4
|
const day = days.find(d => d.day.toLowerCase() === name.toLowerCase())
|
|
15
5
|
const date = now.toLocaleDateString('en-GB').split('/')
|
|
16
|
-
const open = `${date[2]}-${date[1]}-${date[0]}T${day.openTime}`
|
|
17
|
-
const close = `${date[2]}-${date[1]}-${date[0]}T${day.closeTime}`
|
|
6
|
+
const open = `${date[2]}-${date[1]}-${date[0]}T${day.openTime}.000Z`
|
|
7
|
+
const close = `${date[2]}-${date[1]}-${date[0]}T${day.closeTime}.000Z`
|
|
8
|
+
|
|
18
9
|
return now.getTime() >= Date.parse(open) && now.getTime() <= Date.parse(close)
|
|
19
10
|
}
|
|
11
|
+
|
|
12
|
+
module.exports = {
|
|
13
|
+
isWithinHours
|
|
14
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
|
|
3
|
+
import nodeExternals from 'webpack-node-externals'
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(new URL(import.meta.url).pathname)
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
entry: {
|
|
9
|
+
client: path.join(__dirname, 'src/client/index.jsx'),
|
|
10
|
+
server: path.join(__dirname, 'src/server/index.js')
|
|
11
|
+
},
|
|
12
|
+
devtool: 'source-map',
|
|
13
|
+
mode: 'development',
|
|
14
|
+
output: {
|
|
15
|
+
path: path.resolve(__dirname, 'dist'),
|
|
16
|
+
library: {
|
|
17
|
+
type: 'commonjs2'
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
target: 'node',
|
|
21
|
+
externals: [nodeExternals()],
|
|
22
|
+
module: {
|
|
23
|
+
rules: [
|
|
24
|
+
{
|
|
25
|
+
test: /\.jsx?$/i,
|
|
26
|
+
use: [
|
|
27
|
+
'babel-loader'
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
}
|
|
Binary file
|
package/babel.config.cjs
DELETED