@seamapi/react 1.30.2 → 1.31.0
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 -1
- package/dist/elements.js +5937 -5482
- package/dist/elements.js.map +1 -1
- package/dist/index.css +106 -0
- package/dist/index.css.map +1 -1
- package/dist/index.min.css +1 -1
- package/dist/index.min.css.map +1 -1
- package/lib/dates.d.ts +50 -0
- package/lib/dates.js +93 -1
- package/lib/dates.js.map +1 -1
- package/lib/icons/CheckboxBlank.d.ts +2 -0
- package/lib/icons/CheckboxBlank.js +5 -0
- package/lib/icons/CheckboxBlank.js.map +1 -0
- package/lib/icons/CheckboxFilled.d.ts +2 -0
- package/lib/icons/CheckboxFilled.js +5 -0
- package/lib/icons/CheckboxFilled.js.map +1 -0
- package/lib/icons/Edit.d.ts +2 -0
- package/lib/icons/Edit.js +7 -0
- package/lib/icons/Edit.js.map +1 -0
- package/lib/icons/RadioChecked.d.ts +2 -0
- package/lib/icons/RadioChecked.js +5 -0
- package/lib/icons/RadioChecked.js.map +1 -0
- package/lib/icons/RadioUnchecked.d.ts +2 -0
- package/lib/icons/RadioUnchecked.js +5 -0
- package/lib/icons/RadioUnchecked.js.map +1 -0
- package/lib/seam/components/AccessCodeForm/AccessCodeForm.js +66 -12
- package/lib/seam/components/AccessCodeForm/AccessCodeForm.js.map +1 -1
- package/lib/seam/components/AccessCodeForm/AccessCodeFormDatePicker.d.ts +13 -0
- package/lib/seam/components/AccessCodeForm/AccessCodeFormDatePicker.js +18 -0
- package/lib/seam/components/AccessCodeForm/AccessCodeFormDatePicker.js.map +1 -0
- package/lib/seam/components/AccessCodeForm/AccessCodeFormTimes.d.ts +8 -0
- package/lib/seam/components/AccessCodeForm/AccessCodeFormTimes.js +12 -0
- package/lib/seam/components/AccessCodeForm/AccessCodeFormTimes.js.map +1 -0
- package/lib/seam/components/AccessCodeForm/AccessCodeFormTimezonePicker.d.ts +8 -0
- package/lib/seam/components/AccessCodeForm/AccessCodeFormTimezonePicker.js +25 -0
- package/lib/seam/components/AccessCodeForm/AccessCodeFormTimezonePicker.js.map +1 -0
- package/lib/seam/components/DeviceTable/DeviceTable.d.ts +2 -1
- package/lib/seam/components/DeviceTable/DeviceTable.js +2 -2
- package/lib/seam/components/DeviceTable/DeviceTable.js.map +1 -1
- package/lib/ui/Checkbox.d.ts +9 -0
- package/lib/ui/Checkbox.js +17 -0
- package/lib/ui/Checkbox.js.map +1 -0
- package/lib/ui/DateTimePicker/DateTimePicker.d.ts +5 -0
- package/lib/ui/DateTimePicker/DateTimePicker.js +8 -0
- package/lib/ui/DateTimePicker/DateTimePicker.js.map +1 -0
- package/lib/ui/FormField.js +2 -1
- package/lib/ui/FormField.js.map +1 -1
- package/lib/ui/Menu/Menu.d.ts +2 -2
- package/lib/ui/Menu/Menu.js +1 -5
- package/lib/ui/Menu/Menu.js.map +1 -1
- package/lib/ui/RadioField/Radio.d.ts +7 -0
- package/lib/ui/RadioField/Radio.js +19 -0
- package/lib/ui/RadioField/Radio.js.map +1 -0
- package/lib/ui/RadioField/RadioField.d.ts +15 -0
- package/lib/ui/RadioField/RadioField.js +20 -0
- package/lib/ui/RadioField/RadioField.js.map +1 -0
- package/lib/ui/TextField/TextField.d.ts +4 -2
- package/lib/ui/TextField/TextField.js +2 -2
- package/lib/ui/TextField/TextField.js.map +1 -1
- package/lib/ui/layout/ContentHeader.d.ts +2 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +9 -4
- package/src/lib/dates.ts +120 -1
- package/src/lib/icons/CheckboxBlank.tsx +22 -0
- package/src/lib/icons/CheckboxFilled.tsx +23 -0
- package/src/lib/icons/Edit.tsx +36 -0
- package/src/lib/icons/RadioChecked.tsx +21 -0
- package/src/lib/icons/RadioUnchecked.tsx +23 -0
- package/src/lib/seam/components/AccessCodeForm/AccessCodeForm.tsx +137 -23
- package/src/lib/seam/components/AccessCodeForm/AccessCodeFormDatePicker.tsx +68 -0
- package/src/lib/seam/components/AccessCodeForm/AccessCodeFormTimes.tsx +34 -0
- package/src/lib/seam/components/AccessCodeForm/AccessCodeFormTimezonePicker.tsx +70 -0
- package/src/lib/seam/components/DeviceTable/DeviceTable.element.ts +1 -0
- package/src/lib/seam/components/DeviceTable/DeviceTable.tsx +9 -5
- package/src/lib/ui/Checkbox.tsx +44 -0
- package/src/lib/ui/DateTimePicker/DateTimePicker.tsx +30 -0
- package/src/lib/ui/FormField.tsx +2 -1
- package/src/lib/ui/Menu/Menu.tsx +4 -8
- package/src/lib/ui/RadioField/Radio.tsx +45 -0
- package/src/lib/ui/RadioField/RadioField.tsx +55 -0
- package/src/lib/ui/TextField/TextField.tsx +12 -2
- package/src/lib/ui/layout/ContentHeader.tsx +1 -1
- package/src/lib/version.ts +1 -1
- package/src/styles/_access-code-form.scss +88 -0
- package/src/styles/_checkbox.scss +25 -0
- package/src/styles/_inputs.scss +3 -0
- package/src/styles/_main.scss +4 -0
- package/src/styles/_radio-field.scss +31 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seamapi/react",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.31.0",
|
|
4
4
|
"description": "Seam Components.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -77,10 +77,12 @@
|
|
|
77
77
|
"test:update": "jest --updateSnapshot",
|
|
78
78
|
"test:watch": "jest --watch",
|
|
79
79
|
"test:inspect": "node --inspect-brk ./node_modules/jest/bin/jest.js --runInBand",
|
|
80
|
-
"lint": "eslint --ignore-path .gitignore
|
|
80
|
+
"lint": "eslint --ignore-path .gitignore .",
|
|
81
81
|
"prelint": "prettier --check --ignore-path .gitignore .",
|
|
82
82
|
"postlint": "stylelint '**/*.scss'",
|
|
83
|
-
"prepack": "
|
|
83
|
+
"prepack": "tsx ./prepack.ts",
|
|
84
|
+
"version": "tsx ./version.ts",
|
|
85
|
+
"preversion": "tsx ./preversion.ts",
|
|
84
86
|
"postversion": "git push --follow-tags",
|
|
85
87
|
"storybook": "storybook dev --port 6006",
|
|
86
88
|
"storybook:docs": "storybook dev --docs --port 6007",
|
|
@@ -94,7 +96,7 @@
|
|
|
94
96
|
"generate": "svgr --out-dir src/lib/icons -- assets/icons",
|
|
95
97
|
"pregenerate": "del src/lib/icons",
|
|
96
98
|
"postgenerate": "eslint --fix src/lib/icons",
|
|
97
|
-
"format": "eslint --ignore-path .gitignore --fix
|
|
99
|
+
"format": "eslint --ignore-path .gitignore --fix .",
|
|
98
100
|
"preformat": "prettier --write --ignore-path .gitignore .",
|
|
99
101
|
"postformat": "stylelint --fix '**/*.scss'"
|
|
100
102
|
},
|
|
@@ -160,8 +162,10 @@
|
|
|
160
162
|
"eslint-plugin-simple-import-sort": "^10.0.0",
|
|
161
163
|
"eslint-plugin-storybook": "^0.6.11",
|
|
162
164
|
"eslint-plugin-unused-imports": "^2.0.0",
|
|
165
|
+
"execa": "^7.1.1",
|
|
163
166
|
"fast-glob": "^3.2.12",
|
|
164
167
|
"http-proxy-middleware": "^2.0.6",
|
|
168
|
+
"isomorphic-fetch": "^3.0.0",
|
|
165
169
|
"jest": "^29.5.0",
|
|
166
170
|
"jest-environment-jsdom": "^29.5.0",
|
|
167
171
|
"prettier": "^3.0.0",
|
|
@@ -177,6 +181,7 @@
|
|
|
177
181
|
"ts-jest": "^29.1.0",
|
|
178
182
|
"tsc-alias": "^1.8.5",
|
|
179
183
|
"tsconfig-paths-webpack-plugin": "^4.0.1",
|
|
184
|
+
"tsx": "^3.12.7",
|
|
180
185
|
"typescript": "^5.1.0",
|
|
181
186
|
"vite": "^4.2.1",
|
|
182
187
|
"vite-tsconfig-paths": "^4.0.8",
|
package/src/lib/dates.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DateTime } from 'luxon'
|
|
1
|
+
import { DateTime, IANAZone } from 'luxon'
|
|
2
2
|
|
|
3
3
|
export const compareByCreatedAtDesc = (
|
|
4
4
|
a: { created_at: string },
|
|
@@ -9,3 +9,122 @@ export const compareByCreatedAtDesc = (
|
|
|
9
9
|
if (!t1.isValid || !t2.isValid) return 0
|
|
10
10
|
return t1.toMillis() - t2.toMillis()
|
|
11
11
|
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get the timezone strings supported by the user's browser.
|
|
15
|
+
*
|
|
16
|
+
* @returns string[]
|
|
17
|
+
*/
|
|
18
|
+
export function getTimezones(): string[] {
|
|
19
|
+
return Intl.supportedValuesOf('timeZone')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get the default browser timezone.
|
|
24
|
+
*
|
|
25
|
+
* @returns string
|
|
26
|
+
*/
|
|
27
|
+
export function getBrowserTimezone(): string {
|
|
28
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Takes an IANA timezone, like America/Los_Angeles, into a more readable
|
|
33
|
+
* string: Los Angeles (America).
|
|
34
|
+
* @param timezone
|
|
35
|
+
* @returns string
|
|
36
|
+
*/
|
|
37
|
+
export function getTimezoneLabel(timezone: string): string {
|
|
38
|
+
const [region = '', city = ''] = timezone.replace(/_/g, ' ').split('/')
|
|
39
|
+
return `${city} (${region})`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get a timezones offset from UTC in minutes.
|
|
44
|
+
*
|
|
45
|
+
* @param timezone
|
|
46
|
+
* @returns minutes
|
|
47
|
+
*/
|
|
48
|
+
function getTimezoneOffsetMinutes(timezone: string): number {
|
|
49
|
+
return DateTime.local().setZone(timezone).offset
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Compares 2 timezones (America/Los_angeles) by their offset
|
|
54
|
+
* minutes in ascending order.
|
|
55
|
+
*
|
|
56
|
+
* @param timezoneA
|
|
57
|
+
* @param timezonB
|
|
58
|
+
* @returns number
|
|
59
|
+
*/
|
|
60
|
+
export const compareByTimezoneOffsetAsc = (
|
|
61
|
+
timezoneA: string,
|
|
62
|
+
timezonB: string
|
|
63
|
+
): number =>
|
|
64
|
+
getTimezoneOffsetMinutes(timezoneA) - getTimezoneOffsetMinutes(timezonB)
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get the timezone offset
|
|
68
|
+
* America/Los_angeles -> -07:00
|
|
69
|
+
*
|
|
70
|
+
* eg. America/Los_Angeles -> UTC-07:00
|
|
71
|
+
*
|
|
72
|
+
* @param timezone
|
|
73
|
+
* @returns offset
|
|
74
|
+
*/
|
|
75
|
+
export function getTimezoneOffset(timezone: string): string {
|
|
76
|
+
return IANAZone.create(timezone).formatOffset(Date.now(), 'short')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const formatDateReadable = (
|
|
80
|
+
date: string,
|
|
81
|
+
options: {
|
|
82
|
+
showWeekday?: boolean
|
|
83
|
+
} = {}
|
|
84
|
+
): string => {
|
|
85
|
+
const { showWeekday = true } = options
|
|
86
|
+
|
|
87
|
+
// '2023-04-17' to 'Mon Apr 17, 2023' / 'Apr 17, 2023'
|
|
88
|
+
const format = showWeekday ? 'EEE MMM d, yyyy' : 'MMM d, yyyy'
|
|
89
|
+
|
|
90
|
+
return DateTime.fromFormat(date, 'yyyy-MM-dd').toFormat(format)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const formatTimeReadable = (time: string): string | null => {
|
|
94
|
+
const dateTime = DateTime.fromFormat(time, 'HH:mm:ss')
|
|
95
|
+
if (!dateTime.isValid) {
|
|
96
|
+
return null
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return dateTime.toFormat('h:mm a')
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const formatDateTimeReadable = (dateTime: string): string => {
|
|
103
|
+
const [date = '', time = ''] = dateTime.split('T')
|
|
104
|
+
return `${formatDateReadable(date, { showWeekday: false })} at ${
|
|
105
|
+
formatTimeReadable(time) ?? ''
|
|
106
|
+
}`
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const getNow = (): string => getDateTimeOnly(DateTime.now())
|
|
110
|
+
export const get24HoursLater = (): string =>
|
|
111
|
+
getDateTimeOnly(DateTime.now().plus({ days: 1 }))
|
|
112
|
+
|
|
113
|
+
function getDateTimeOnly(dateTime: DateTime): string {
|
|
114
|
+
const date = dateTime.toFormat('yyyy-MM-dd')
|
|
115
|
+
const time = dateTime.toFormat('HH:mm:ss')
|
|
116
|
+
return `${date}T${time}`
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Takes a date (2023-07-20T00:00:00), and a timezone (America/Los_angeles), and
|
|
121
|
+
* returns an ISO8601 Date (2023-07-20T00:00:00.000-07:00).
|
|
122
|
+
*
|
|
123
|
+
* @param date
|
|
124
|
+
* @param timezone
|
|
125
|
+
* @returns ISOdate
|
|
126
|
+
*/
|
|
127
|
+
export const createIsoDate = (date: string, timezone: string): string => {
|
|
128
|
+
const offset = getTimezoneOffset(timezone)
|
|
129
|
+
return `${date}.000${offset}`
|
|
130
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Automaically generated by SVGR from assets/icons/*.svg.
|
|
3
|
+
* Do not edit this file or add other components to this directory.
|
|
4
|
+
*/
|
|
5
|
+
import type { SVGProps } from 'react'
|
|
6
|
+
export function CheckboxBlankIcon(props: SVGProps<SVGSVGElement>): JSX.Element {
|
|
7
|
+
return (
|
|
8
|
+
<svg
|
|
9
|
+
xmlns='http://www.w3.org/2000/svg'
|
|
10
|
+
width={24}
|
|
11
|
+
height={24}
|
|
12
|
+
fill='none'
|
|
13
|
+
{...props}
|
|
14
|
+
>
|
|
15
|
+
<path
|
|
16
|
+
fill='#000'
|
|
17
|
+
fillOpacity={0.6}
|
|
18
|
+
d='M19 5v14H5V5h14Zm0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2Z'
|
|
19
|
+
/>
|
|
20
|
+
</svg>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Automaically generated by SVGR from assets/icons/*.svg.
|
|
3
|
+
* Do not edit this file or add other components to this directory.
|
|
4
|
+
*/
|
|
5
|
+
import type { SVGProps } from 'react'
|
|
6
|
+
export function CheckboxFilledIcon(
|
|
7
|
+
props: SVGProps<SVGSVGElement>
|
|
8
|
+
): JSX.Element {
|
|
9
|
+
return (
|
|
10
|
+
<svg
|
|
11
|
+
xmlns='http://www.w3.org/2000/svg'
|
|
12
|
+
width={24}
|
|
13
|
+
height={24}
|
|
14
|
+
fill='none'
|
|
15
|
+
{...props}
|
|
16
|
+
>
|
|
17
|
+
<path
|
|
18
|
+
fill='#2196F3'
|
|
19
|
+
d='M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2Zm-9 14-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9Z'
|
|
20
|
+
/>
|
|
21
|
+
</svg>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Automaically generated by SVGR from assets/icons/*.svg.
|
|
3
|
+
* Do not edit this file or add other components to this directory.
|
|
4
|
+
*/
|
|
5
|
+
import type { SVGProps } from 'react'
|
|
6
|
+
export function EditIcon(props: SVGProps<SVGSVGElement>): JSX.Element {
|
|
7
|
+
return (
|
|
8
|
+
<svg
|
|
9
|
+
xmlns='http://www.w3.org/2000/svg'
|
|
10
|
+
width={24}
|
|
11
|
+
height={24}
|
|
12
|
+
fill='none'
|
|
13
|
+
{...props}
|
|
14
|
+
>
|
|
15
|
+
<mask
|
|
16
|
+
id='edit_svg__a'
|
|
17
|
+
width={24}
|
|
18
|
+
height={24}
|
|
19
|
+
x={0}
|
|
20
|
+
y={0}
|
|
21
|
+
maskUnits='userSpaceOnUse'
|
|
22
|
+
style={{
|
|
23
|
+
maskType: 'alpha',
|
|
24
|
+
}}
|
|
25
|
+
>
|
|
26
|
+
<path fill='#D9D9D9' d='M0 0h24v24H0z' />
|
|
27
|
+
</mask>
|
|
28
|
+
<g mask='url(#edit_svg__a)'>
|
|
29
|
+
<path
|
|
30
|
+
fill='#232B3A'
|
|
31
|
+
d='M5 19h1.4l8.625-8.625-1.4-1.4L5 17.6V19ZM19.3 8.925l-4.25-4.2 1.4-1.4a1.92 1.92 0 0 1 1.413-.575 1.92 1.92 0 0 1 1.412.575l1.4 1.4c.383.383.583.846.6 1.388a1.806 1.806 0 0 1-.55 1.387L19.3 8.925ZM17.85 10.4 7.25 21H3v-4.25l10.6-10.6 4.25 4.25Z'
|
|
32
|
+
/>
|
|
33
|
+
</g>
|
|
34
|
+
</svg>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Automaically generated by SVGR from assets/icons/*.svg.
|
|
3
|
+
* Do not edit this file or add other components to this directory.
|
|
4
|
+
*/
|
|
5
|
+
import type { SVGProps } from 'react'
|
|
6
|
+
export function RadioCheckedIcon(props: SVGProps<SVGSVGElement>): JSX.Element {
|
|
7
|
+
return (
|
|
8
|
+
<svg
|
|
9
|
+
xmlns='http://www.w3.org/2000/svg'
|
|
10
|
+
width={24}
|
|
11
|
+
height={24}
|
|
12
|
+
fill='none'
|
|
13
|
+
{...props}
|
|
14
|
+
>
|
|
15
|
+
<path
|
|
16
|
+
fill='#2196F3'
|
|
17
|
+
d='M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5Zm0-5C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8Z'
|
|
18
|
+
/>
|
|
19
|
+
</svg>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Automaically generated by SVGR from assets/icons/*.svg.
|
|
3
|
+
* Do not edit this file or add other components to this directory.
|
|
4
|
+
*/
|
|
5
|
+
import type { SVGProps } from 'react'
|
|
6
|
+
export function RadioUncheckedIcon(
|
|
7
|
+
props: SVGProps<SVGSVGElement>
|
|
8
|
+
): JSX.Element {
|
|
9
|
+
return (
|
|
10
|
+
<svg
|
|
11
|
+
xmlns='http://www.w3.org/2000/svg'
|
|
12
|
+
width={24}
|
|
13
|
+
height={24}
|
|
14
|
+
fill='none'
|
|
15
|
+
{...props}
|
|
16
|
+
>
|
|
17
|
+
<path
|
|
18
|
+
fill='#232B3A'
|
|
19
|
+
d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8Z'
|
|
20
|
+
/>
|
|
21
|
+
</svg>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import classNames from 'classnames'
|
|
2
2
|
import { useState } from 'react'
|
|
3
|
+
import type { AccessCode } from 'seamapi'
|
|
3
4
|
|
|
5
|
+
import {
|
|
6
|
+
createIsoDate,
|
|
7
|
+
get24HoursLater,
|
|
8
|
+
getBrowserTimezone,
|
|
9
|
+
getNow,
|
|
10
|
+
} from 'lib/dates.js'
|
|
4
11
|
import { useCreateAccessCode } from 'lib/seam/access-codes/use-create-access-code.js'
|
|
12
|
+
import { AccessCodeFormDatePicker } from 'lib/seam/components/AccessCodeForm/AccessCodeFormDatePicker.js'
|
|
13
|
+
import { AccessCodeFormTimes } from 'lib/seam/components/AccessCodeForm/AccessCodeFormTimes.js'
|
|
14
|
+
import { AccessCodeFormTimezonePicker } from 'lib/seam/components/AccessCodeForm/AccessCodeFormTimezonePicker.js'
|
|
5
15
|
import { useDevice, type UseDeviceData } from 'lib/seam/devices/use-device.js'
|
|
6
16
|
import { Button } from 'lib/ui/Button.js'
|
|
7
17
|
import { FormField } from 'lib/ui/FormField.js'
|
|
8
18
|
import { InputLabel } from 'lib/ui/InputLabel.js'
|
|
9
19
|
import { ContentHeader } from 'lib/ui/layout/ContentHeader.js'
|
|
20
|
+
import { RadioField } from 'lib/ui/RadioField/RadioField.js'
|
|
10
21
|
import { TextField } from 'lib/ui/TextField/TextField.js'
|
|
22
|
+
import { useToggle } from 'lib/ui/use-toggle.js'
|
|
11
23
|
|
|
12
24
|
export interface AccessCodeFormProps {
|
|
13
25
|
deviceId: string
|
|
@@ -42,37 +54,53 @@ function Content({
|
|
|
42
54
|
device: NonNullable<UseDeviceData>
|
|
43
55
|
}): JSX.Element {
|
|
44
56
|
const [name, setName] = useState('')
|
|
57
|
+
const [type, setType] = useState<AccessCode['type']>('ongoing')
|
|
58
|
+
const [datePickerVisible, setDatePickerVisible] = useState(false)
|
|
59
|
+
const [timezone, setTimezone] = useState<string>(getBrowserTimezone())
|
|
60
|
+
const [startDate, setStartDate] = useState<string>(getNow())
|
|
61
|
+
const [endDate, setEndDate] = useState<string>(get24HoursLater())
|
|
62
|
+
const [timezonePickerVisible, toggleTimezonePicker] = useToggle()
|
|
45
63
|
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
64
|
+
const { submit, isLoading } = useSubmit({
|
|
65
|
+
name,
|
|
66
|
+
type,
|
|
67
|
+
device,
|
|
68
|
+
startDate,
|
|
69
|
+
endDate,
|
|
70
|
+
timezone,
|
|
71
|
+
onSuccess: onBack,
|
|
72
|
+
})
|
|
52
73
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
74
|
+
if (timezonePickerVisible) {
|
|
75
|
+
return (
|
|
76
|
+
<AccessCodeFormTimezonePicker
|
|
77
|
+
value={timezone}
|
|
78
|
+
onChange={setTimezone}
|
|
79
|
+
onClose={toggleTimezonePicker}
|
|
80
|
+
/>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
56
83
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
84
|
+
if (datePickerVisible) {
|
|
85
|
+
return (
|
|
86
|
+
<AccessCodeFormDatePicker
|
|
87
|
+
startDate={startDate}
|
|
88
|
+
setStartDate={setStartDate}
|
|
89
|
+
endDate={endDate}
|
|
90
|
+
setEndDate={setEndDate}
|
|
91
|
+
timezone={timezone}
|
|
92
|
+
onChangeTimezone={toggleTimezonePicker}
|
|
93
|
+
onBack={() => {
|
|
94
|
+
setDatePickerVisible(false)
|
|
95
|
+
}}
|
|
96
|
+
/>
|
|
67
97
|
)
|
|
68
98
|
}
|
|
69
99
|
|
|
70
100
|
const nameError = name.length > 60 ? t.overCharacterLimitError : undefined
|
|
71
101
|
|
|
72
102
|
const isFormValid =
|
|
73
|
-
name.trim().length > 0 &&
|
|
74
|
-
nameError === undefined &&
|
|
75
|
-
!createAccessCode.isLoading
|
|
103
|
+
name.trim().length > 0 && nameError === undefined && !isLoading
|
|
76
104
|
|
|
77
105
|
return (
|
|
78
106
|
<>
|
|
@@ -93,9 +121,38 @@ function Content({
|
|
|
93
121
|
helperText={nameError}
|
|
94
122
|
/>
|
|
95
123
|
</FormField>
|
|
124
|
+
<FormField>
|
|
125
|
+
<InputLabel>{t.timingInputLabel}</InputLabel>
|
|
126
|
+
<RadioField
|
|
127
|
+
value={type}
|
|
128
|
+
onChange={setType}
|
|
129
|
+
name='type'
|
|
130
|
+
options={[
|
|
131
|
+
{
|
|
132
|
+
label: t.typeOngoingLabel,
|
|
133
|
+
value: 'ongoing',
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
label: t.typeTimeBoundLabel,
|
|
137
|
+
value: 'time_bound',
|
|
138
|
+
},
|
|
139
|
+
]}
|
|
140
|
+
/>
|
|
141
|
+
<>
|
|
142
|
+
{type === 'time_bound' && (
|
|
143
|
+
<AccessCodeFormTimes
|
|
144
|
+
startDate={startDate}
|
|
145
|
+
endDate={endDate}
|
|
146
|
+
onEdit={() => {
|
|
147
|
+
setDatePickerVisible(true)
|
|
148
|
+
}}
|
|
149
|
+
/>
|
|
150
|
+
)}
|
|
151
|
+
</>
|
|
152
|
+
</FormField>
|
|
96
153
|
<div className='seam-actions'>
|
|
97
154
|
<Button onClick={onBack}>{t.cancel}</Button>
|
|
98
|
-
<Button variant='solid' disabled={!isFormValid} onMouseDown={
|
|
155
|
+
<Button variant='solid' disabled={!isFormValid} onMouseDown={submit}>
|
|
99
156
|
{t.save}
|
|
100
157
|
</Button>
|
|
101
158
|
</div>
|
|
@@ -104,10 +161,67 @@ function Content({
|
|
|
104
161
|
)
|
|
105
162
|
}
|
|
106
163
|
|
|
164
|
+
function useSubmit(params: {
|
|
165
|
+
name: string
|
|
166
|
+
type: AccessCode['type']
|
|
167
|
+
device: NonNullable<UseDeviceData>
|
|
168
|
+
startDate: string
|
|
169
|
+
endDate: string
|
|
170
|
+
timezone: string
|
|
171
|
+
onSuccess?: () => void
|
|
172
|
+
}): {
|
|
173
|
+
submit: () => void
|
|
174
|
+
isLoading: boolean
|
|
175
|
+
} {
|
|
176
|
+
const { name, type, device, startDate, endDate, timezone, onSuccess } = params
|
|
177
|
+
|
|
178
|
+
const { mutate, isLoading } = useCreateAccessCode()
|
|
179
|
+
const submit = (): void => {
|
|
180
|
+
if (name === '') {
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (isLoading) {
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (type === 'time_bound') {
|
|
189
|
+
mutate(
|
|
190
|
+
{
|
|
191
|
+
name,
|
|
192
|
+
device_id: device.device_id,
|
|
193
|
+
starts_at: createIsoDate(startDate, timezone),
|
|
194
|
+
ends_at: createIsoDate(endDate, timezone),
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
onSuccess,
|
|
198
|
+
}
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
mutate(
|
|
205
|
+
{
|
|
206
|
+
name,
|
|
207
|
+
device_id: device.device_id,
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
onSuccess,
|
|
211
|
+
}
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return { submit, isLoading }
|
|
216
|
+
}
|
|
217
|
+
|
|
107
218
|
const t = {
|
|
108
219
|
addNewAccessCode: 'Add new access code',
|
|
109
220
|
overCharacterLimitError: '60 characters max',
|
|
110
221
|
nameInputLabel: 'Name the new code',
|
|
111
222
|
cancel: 'Cancel',
|
|
112
223
|
save: 'Save',
|
|
224
|
+
timingInputLabel: 'Timing',
|
|
225
|
+
typeOngoingLabel: 'Ongoing',
|
|
226
|
+
typeTimeBoundLabel: 'Start/end times',
|
|
113
227
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import classNames from 'classnames'
|
|
2
|
+
|
|
3
|
+
import { getTimezoneLabel } from 'lib/dates.js'
|
|
4
|
+
import { ChevronRightIcon } from 'lib/icons/ChevronRight.js'
|
|
5
|
+
import { DateTimePicker } from 'lib/ui/DateTimePicker/DateTimePicker.js'
|
|
6
|
+
import { FormField } from 'lib/ui/FormField.js'
|
|
7
|
+
import { InputLabel } from 'lib/ui/InputLabel.js'
|
|
8
|
+
import { ContentHeader } from 'lib/ui/layout/ContentHeader.js'
|
|
9
|
+
|
|
10
|
+
interface AccessCodeFormDatePickerProps {
|
|
11
|
+
startDate: string
|
|
12
|
+
setStartDate: (date: string) => void
|
|
13
|
+
endDate: string
|
|
14
|
+
setEndDate: (date: string) => void
|
|
15
|
+
timezone: string
|
|
16
|
+
onChangeTimezone: () => void
|
|
17
|
+
className?: string
|
|
18
|
+
onBack?: () => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function AccessCodeFormDatePicker({
|
|
22
|
+
timezone,
|
|
23
|
+
onBack,
|
|
24
|
+
className,
|
|
25
|
+
startDate,
|
|
26
|
+
setStartDate,
|
|
27
|
+
endDate,
|
|
28
|
+
setEndDate,
|
|
29
|
+
onChangeTimezone,
|
|
30
|
+
}: AccessCodeFormDatePickerProps): JSX.Element {
|
|
31
|
+
return (
|
|
32
|
+
<div className={classNames('seam-schedule-picker', className)}>
|
|
33
|
+
<ContentHeader title={t.timingTitle} onBack={onBack} />
|
|
34
|
+
<div className='seam-content'>
|
|
35
|
+
<div className='seam-timezone'>
|
|
36
|
+
<span className='seam-label'>{t.selectedTimezoneLabel}</span>
|
|
37
|
+
<span className='seam-selected' onClick={onChangeTimezone}>
|
|
38
|
+
{getTimezoneLabel(timezone)}
|
|
39
|
+
<ChevronRightIcon />
|
|
40
|
+
</span>
|
|
41
|
+
</div>
|
|
42
|
+
<FormField>
|
|
43
|
+
<InputLabel>{t.startTimeLabel}</InputLabel>
|
|
44
|
+
<DateTimePicker
|
|
45
|
+
value={startDate == null ? '' : startDate}
|
|
46
|
+
onChange={setStartDate}
|
|
47
|
+
size='large'
|
|
48
|
+
/>
|
|
49
|
+
</FormField>
|
|
50
|
+
<FormField>
|
|
51
|
+
<InputLabel>{t.endTimeLabel}</InputLabel>
|
|
52
|
+
<DateTimePicker
|
|
53
|
+
value={endDate == null ? '' : endDate}
|
|
54
|
+
onChange={setEndDate}
|
|
55
|
+
size='large'
|
|
56
|
+
/>
|
|
57
|
+
</FormField>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const t = {
|
|
64
|
+
timingTitle: 'Timing',
|
|
65
|
+
selectedTimezoneLabel: 'All times in',
|
|
66
|
+
startTimeLabel: 'Start',
|
|
67
|
+
endTimeLabel: 'End',
|
|
68
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { formatDateTimeReadable } from 'lib/dates.js'
|
|
2
|
+
import { EditIcon } from 'lib/icons/Edit.js'
|
|
3
|
+
import { IconButton } from 'lib/ui/IconButton.js'
|
|
4
|
+
|
|
5
|
+
interface AccessCodeFormTimesProps {
|
|
6
|
+
startDate: string
|
|
7
|
+
endDate: string
|
|
8
|
+
onEdit: () => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function AccessCodeFormTimes({
|
|
12
|
+
startDate,
|
|
13
|
+
endDate,
|
|
14
|
+
onEdit,
|
|
15
|
+
}: AccessCodeFormTimesProps): JSX.Element {
|
|
16
|
+
return (
|
|
17
|
+
<div className='seam-selected-times'>
|
|
18
|
+
<div>
|
|
19
|
+
<span className='seam-label'>{t.startTimeLabel}</span>
|
|
20
|
+
<span className='seam-time'>{formatDateTimeReadable(startDate)}</span>
|
|
21
|
+
<span className='seam-label'>{t.endTimeLabel}</span>
|
|
22
|
+
<span className='seam-time'>{formatDateTimeReadable(endDate)}</span>
|
|
23
|
+
</div>
|
|
24
|
+
<IconButton onClick={onEdit} className='seam-show-date-picker-btn'>
|
|
25
|
+
<EditIcon />
|
|
26
|
+
</IconButton>
|
|
27
|
+
</div>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const t = {
|
|
32
|
+
startTimeLabel: 'Start',
|
|
33
|
+
endTimeLabel: 'End',
|
|
34
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
getBrowserTimezone,
|
|
5
|
+
getTimezoneLabel,
|
|
6
|
+
getTimezoneOffset,
|
|
7
|
+
getTimezones,
|
|
8
|
+
} from 'lib/dates.js'
|
|
9
|
+
import { Checkbox } from 'lib/ui/Checkbox.js'
|
|
10
|
+
import { ContentHeader } from 'lib/ui/layout/ContentHeader.js'
|
|
11
|
+
import { handleString } from 'lib/ui/TextField/TextField.js'
|
|
12
|
+
|
|
13
|
+
interface AccessCodeFormTimezonePickerProps {
|
|
14
|
+
value: string
|
|
15
|
+
onChange: (timezone: string) => void
|
|
16
|
+
onClose: () => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function AccessCodeFormTimezonePicker({
|
|
20
|
+
onChange,
|
|
21
|
+
value,
|
|
22
|
+
onClose,
|
|
23
|
+
}: AccessCodeFormTimezonePickerProps): JSX.Element {
|
|
24
|
+
const [manualTimezoneEnabled, setManualTimezoneEnabled] = useState(false)
|
|
25
|
+
|
|
26
|
+
const isBrowserTimezone = value === getBrowserTimezone()
|
|
27
|
+
const isManualTimezone = !isBrowserTimezone || manualTimezoneEnabled
|
|
28
|
+
|
|
29
|
+
const handleChangeManualTimezone = (enabled: boolean): void => {
|
|
30
|
+
setManualTimezoneEnabled(enabled)
|
|
31
|
+
if (!enabled) {
|
|
32
|
+
onChange(getBrowserTimezone())
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const title = isManualTimezone ? t.titleManual : t.titleAuto
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className='seam-timezone-picker'>
|
|
40
|
+
<ContentHeader title={title} onBack={onClose} />
|
|
41
|
+
<div className='seam-content'>
|
|
42
|
+
<Checkbox
|
|
43
|
+
label={t.setTimezoneManuallyLabel}
|
|
44
|
+
checked={isManualTimezone}
|
|
45
|
+
onChange={handleChangeManualTimezone}
|
|
46
|
+
className='seam-manual-timezone-checkbox'
|
|
47
|
+
/>
|
|
48
|
+
|
|
49
|
+
<select
|
|
50
|
+
value={value}
|
|
51
|
+
onChange={handleString(onChange)}
|
|
52
|
+
className='seam-timezone-select'
|
|
53
|
+
disabled={!isManualTimezone}
|
|
54
|
+
>
|
|
55
|
+
{getTimezones().map((timezone) => (
|
|
56
|
+
<option value={timezone} key={timezone}>
|
|
57
|
+
UTC {getTimezoneOffset(timezone)} {getTimezoneLabel(timezone)}
|
|
58
|
+
</option>
|
|
59
|
+
))}
|
|
60
|
+
</select>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const t = {
|
|
67
|
+
titleAuto: 'Time Zone (automatic)',
|
|
68
|
+
titleManual: 'Time Zone (manual)',
|
|
69
|
+
setTimezoneManuallyLabel: 'Set time zone manually',
|
|
70
|
+
}
|