@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.
Files changed (89) hide show
  1. package/README.md +1 -1
  2. package/dist/elements.js +5937 -5482
  3. package/dist/elements.js.map +1 -1
  4. package/dist/index.css +106 -0
  5. package/dist/index.css.map +1 -1
  6. package/dist/index.min.css +1 -1
  7. package/dist/index.min.css.map +1 -1
  8. package/lib/dates.d.ts +50 -0
  9. package/lib/dates.js +93 -1
  10. package/lib/dates.js.map +1 -1
  11. package/lib/icons/CheckboxBlank.d.ts +2 -0
  12. package/lib/icons/CheckboxBlank.js +5 -0
  13. package/lib/icons/CheckboxBlank.js.map +1 -0
  14. package/lib/icons/CheckboxFilled.d.ts +2 -0
  15. package/lib/icons/CheckboxFilled.js +5 -0
  16. package/lib/icons/CheckboxFilled.js.map +1 -0
  17. package/lib/icons/Edit.d.ts +2 -0
  18. package/lib/icons/Edit.js +7 -0
  19. package/lib/icons/Edit.js.map +1 -0
  20. package/lib/icons/RadioChecked.d.ts +2 -0
  21. package/lib/icons/RadioChecked.js +5 -0
  22. package/lib/icons/RadioChecked.js.map +1 -0
  23. package/lib/icons/RadioUnchecked.d.ts +2 -0
  24. package/lib/icons/RadioUnchecked.js +5 -0
  25. package/lib/icons/RadioUnchecked.js.map +1 -0
  26. package/lib/seam/components/AccessCodeForm/AccessCodeForm.js +66 -12
  27. package/lib/seam/components/AccessCodeForm/AccessCodeForm.js.map +1 -1
  28. package/lib/seam/components/AccessCodeForm/AccessCodeFormDatePicker.d.ts +13 -0
  29. package/lib/seam/components/AccessCodeForm/AccessCodeFormDatePicker.js +18 -0
  30. package/lib/seam/components/AccessCodeForm/AccessCodeFormDatePicker.js.map +1 -0
  31. package/lib/seam/components/AccessCodeForm/AccessCodeFormTimes.d.ts +8 -0
  32. package/lib/seam/components/AccessCodeForm/AccessCodeFormTimes.js +12 -0
  33. package/lib/seam/components/AccessCodeForm/AccessCodeFormTimes.js.map +1 -0
  34. package/lib/seam/components/AccessCodeForm/AccessCodeFormTimezonePicker.d.ts +8 -0
  35. package/lib/seam/components/AccessCodeForm/AccessCodeFormTimezonePicker.js +25 -0
  36. package/lib/seam/components/AccessCodeForm/AccessCodeFormTimezonePicker.js.map +1 -0
  37. package/lib/seam/components/DeviceTable/DeviceTable.d.ts +2 -1
  38. package/lib/seam/components/DeviceTable/DeviceTable.js +2 -2
  39. package/lib/seam/components/DeviceTable/DeviceTable.js.map +1 -1
  40. package/lib/ui/Checkbox.d.ts +9 -0
  41. package/lib/ui/Checkbox.js +17 -0
  42. package/lib/ui/Checkbox.js.map +1 -0
  43. package/lib/ui/DateTimePicker/DateTimePicker.d.ts +5 -0
  44. package/lib/ui/DateTimePicker/DateTimePicker.js +8 -0
  45. package/lib/ui/DateTimePicker/DateTimePicker.js.map +1 -0
  46. package/lib/ui/FormField.js +2 -1
  47. package/lib/ui/FormField.js.map +1 -1
  48. package/lib/ui/Menu/Menu.d.ts +2 -2
  49. package/lib/ui/Menu/Menu.js +1 -5
  50. package/lib/ui/Menu/Menu.js.map +1 -1
  51. package/lib/ui/RadioField/Radio.d.ts +7 -0
  52. package/lib/ui/RadioField/Radio.js +19 -0
  53. package/lib/ui/RadioField/Radio.js.map +1 -0
  54. package/lib/ui/RadioField/RadioField.d.ts +15 -0
  55. package/lib/ui/RadioField/RadioField.js +20 -0
  56. package/lib/ui/RadioField/RadioField.js.map +1 -0
  57. package/lib/ui/TextField/TextField.d.ts +4 -2
  58. package/lib/ui/TextField/TextField.js +2 -2
  59. package/lib/ui/TextField/TextField.js.map +1 -1
  60. package/lib/ui/layout/ContentHeader.d.ts +2 -1
  61. package/lib/version.d.ts +1 -1
  62. package/lib/version.js +1 -1
  63. package/package.json +9 -4
  64. package/src/lib/dates.ts +120 -1
  65. package/src/lib/icons/CheckboxBlank.tsx +22 -0
  66. package/src/lib/icons/CheckboxFilled.tsx +23 -0
  67. package/src/lib/icons/Edit.tsx +36 -0
  68. package/src/lib/icons/RadioChecked.tsx +21 -0
  69. package/src/lib/icons/RadioUnchecked.tsx +23 -0
  70. package/src/lib/seam/components/AccessCodeForm/AccessCodeForm.tsx +137 -23
  71. package/src/lib/seam/components/AccessCodeForm/AccessCodeFormDatePicker.tsx +68 -0
  72. package/src/lib/seam/components/AccessCodeForm/AccessCodeFormTimes.tsx +34 -0
  73. package/src/lib/seam/components/AccessCodeForm/AccessCodeFormTimezonePicker.tsx +70 -0
  74. package/src/lib/seam/components/DeviceTable/DeviceTable.element.ts +1 -0
  75. package/src/lib/seam/components/DeviceTable/DeviceTable.tsx +9 -5
  76. package/src/lib/ui/Checkbox.tsx +44 -0
  77. package/src/lib/ui/DateTimePicker/DateTimePicker.tsx +30 -0
  78. package/src/lib/ui/FormField.tsx +2 -1
  79. package/src/lib/ui/Menu/Menu.tsx +4 -8
  80. package/src/lib/ui/RadioField/Radio.tsx +45 -0
  81. package/src/lib/ui/RadioField/RadioField.tsx +55 -0
  82. package/src/lib/ui/TextField/TextField.tsx +12 -2
  83. package/src/lib/ui/layout/ContentHeader.tsx +1 -1
  84. package/src/lib/version.ts +1 -1
  85. package/src/styles/_access-code-form.scss +88 -0
  86. package/src/styles/_checkbox.scss +25 -0
  87. package/src/styles/_inputs.scss +3 -0
  88. package/src/styles/_main.scss +4 -0
  89. 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.30.2",
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 --ext .js,.cjs,.mjs,.ts,.tsx .",
80
+ "lint": "eslint --ignore-path .gitignore .",
81
81
  "prelint": "prettier --check --ignore-path .gitignore .",
82
82
  "postlint": "stylelint '**/*.scss'",
83
- "prepack": "echo \"const seamapiReactVersion = '$(jq -r .version < package.json)'\n\nexport default seamapiReactVersion\" > src/lib/version.ts && tsc --project tsconfig.version.json",
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 --ext .js,.cjs,.mjs,.ts,.tsx .",
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 createAccessCode = useCreateAccessCode()
47
-
48
- const save = (): void => {
49
- if (name === '') {
50
- return
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
- if (createAccessCode.isLoading) {
54
- return
55
- }
74
+ if (timezonePickerVisible) {
75
+ return (
76
+ <AccessCodeFormTimezonePicker
77
+ value={timezone}
78
+ onChange={setTimezone}
79
+ onClose={toggleTimezonePicker}
80
+ />
81
+ )
82
+ }
56
83
 
57
- createAccessCode.mutate(
58
- {
59
- name,
60
- device_id: device.device_id,
61
- },
62
- {
63
- onSuccess: () => {
64
- onBack?.()
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={save}>
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
+ }