@seamapi/react 0.16.1 → 0.18.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 (188) hide show
  1. package/README.md +28 -2
  2. package/index.css +294 -69
  3. package/index.css.map +1 -1
  4. package/index.min.css +1 -1
  5. package/index.min.css.map +1 -1
  6. package/lib/icons/LockLocked.d.ts +2 -0
  7. package/lib/icons/LockLocked.js +5 -0
  8. package/lib/icons/LockLocked.js.map +1 -0
  9. package/lib/icons/LockUnlocked.d.ts +2 -0
  10. package/lib/icons/LockUnlocked.js +5 -0
  11. package/lib/icons/LockUnlocked.js.map +1 -0
  12. package/lib/seam/access-codes/use-access-code.d.ts +4 -0
  13. package/lib/seam/access-codes/use-access-code.js +16 -0
  14. package/lib/seam/access-codes/use-access-code.js.map +1 -0
  15. package/lib/seam/access-codes/use-access-codes.d.ts +9 -0
  16. package/lib/seam/access-codes/use-access-codes.js +18 -0
  17. package/lib/seam/access-codes/use-access-codes.js.map +1 -0
  18. package/lib/seam/devices/types.d.ts +2 -0
  19. package/lib/seam/devices/types.js +2 -0
  20. package/lib/seam/devices/types.js.map +1 -0
  21. package/lib/seam/devices/use-device.d.ts +9 -0
  22. package/lib/seam/devices/use-device.js +56 -0
  23. package/lib/seam/devices/use-device.js.map +1 -0
  24. package/lib/seam/devices/use-devices.d.ts +4 -9
  25. package/lib/seam/devices/use-devices.js +2 -1
  26. package/lib/seam/devices/use-devices.js.map +1 -1
  27. package/lib/seam/index.d.ts +4 -1
  28. package/lib/seam/index.js +4 -1
  29. package/lib/seam/index.js.map +1 -1
  30. package/lib/seam/use-seam-client.js +3 -3
  31. package/lib/seam/use-seam-client.js.map +1 -1
  32. package/lib/seam/use-seam-query-result.js.map +1 -0
  33. package/lib/ui/AccessCodeDetails/AccessCodeDetails.d.ts +6 -0
  34. package/lib/ui/AccessCodeDetails/AccessCodeDetails.js +64 -0
  35. package/lib/ui/AccessCodeDetails/AccessCodeDetails.js.map +1 -0
  36. package/lib/ui/AccessCodeDetails/AccessCodeDevice.d.ts +5 -0
  37. package/lib/ui/AccessCodeDetails/AccessCodeDevice.js +23 -0
  38. package/lib/ui/AccessCodeDetails/AccessCodeDevice.js.map +1 -0
  39. package/lib/ui/AccessCodeTable/AccessCodeTable.d.ts +1 -1
  40. package/lib/ui/AccessCodeTable/AccessCodeTable.js +19 -5
  41. package/lib/ui/AccessCodeTable/AccessCodeTable.js.map +1 -1
  42. package/lib/ui/AccessCodeTable/AccessCodeTableContent.d.ts +1 -1
  43. package/lib/ui/AccessCodeTable/AccessCodeTableContent.js +7 -3
  44. package/lib/ui/AccessCodeTable/AccessCodeTableContent.js.map +1 -1
  45. package/lib/ui/AccessCodeTable/CodeDetails.js +1 -1
  46. package/lib/ui/AccessCodeTable/CodeDetails.js.map +1 -1
  47. package/lib/ui/Button.d.ts +8 -0
  48. package/lib/ui/Button.js +8 -0
  49. package/lib/ui/Button.js.map +1 -0
  50. package/lib/ui/DeviceDetails/DeviceDetails.d.ts +4 -3
  51. package/lib/ui/DeviceDetails/DeviceDetails.js +25 -9
  52. package/lib/ui/DeviceDetails/DeviceDetails.js.map +1 -1
  53. package/lib/ui/DeviceDetails/DeviceModel.d.ts +7 -0
  54. package/lib/ui/DeviceDetails/DeviceModel.js +18 -0
  55. package/lib/ui/DeviceDetails/DeviceModel.js.map +1 -0
  56. package/lib/ui/DeviceTable/DeviceTable.d.ts +7 -0
  57. package/lib/ui/DeviceTable/DeviceTable.js +49 -0
  58. package/lib/ui/DeviceTable/DeviceTable.js.map +1 -0
  59. package/lib/ui/IconButton.js +1 -1
  60. package/lib/ui/IconButton.js.map +1 -1
  61. package/lib/ui/Table/TableRow.d.ts +3 -1
  62. package/lib/ui/Table/TableRow.js +1 -1
  63. package/lib/ui/Table/TableRow.js.map +1 -1
  64. package/lib/ui/TextButton.d.ts +7 -0
  65. package/lib/ui/TextButton.js +5 -0
  66. package/lib/ui/TextButton.js.map +1 -0
  67. package/lib/ui/TextField/SearchTextField.d.ts +1 -1
  68. package/lib/ui/TextField/SearchTextField.js +1 -1
  69. package/lib/ui/TextField/SearchTextField.js.map +1 -1
  70. package/lib/ui/device/BatteryStatus.d.ts +5 -0
  71. package/lib/ui/{DeviceDetails → device}/BatteryStatus.js +5 -6
  72. package/lib/ui/device/BatteryStatus.js.map +1 -0
  73. package/lib/ui/{DeviceDetails → device}/DeviceImage.d.ts +2 -1
  74. package/lib/ui/{DeviceDetails → device}/DeviceImage.js +2 -2
  75. package/lib/ui/device/DeviceImage.js.map +1 -0
  76. package/lib/ui/device/LockStatus.d.ts +5 -0
  77. package/lib/ui/device/LockStatus.js +19 -0
  78. package/lib/ui/device/LockStatus.js.map +1 -0
  79. package/lib/ui/device/OnlineStatus.d.ts +5 -0
  80. package/lib/ui/{DeviceDetails → device}/OnlineStatus.js +4 -5
  81. package/lib/ui/device/OnlineStatus.js.map +1 -0
  82. package/lib/ui/index.d.ts +3 -1
  83. package/lib/ui/index.js +3 -1
  84. package/lib/ui/index.js.map +1 -1
  85. package/lib/ui/layout/ContentHeader.d.ts +2 -2
  86. package/lib/ui/layout/ContentHeader.js +5 -1
  87. package/lib/ui/layout/ContentHeader.js.map +1 -1
  88. package/package.json +4 -5
  89. package/src/icons/lock-locked.svg +8 -0
  90. package/src/icons/lock-unlocked.svg +9 -0
  91. package/src/lib/icons/LockLocked.tsx +32 -0
  92. package/src/lib/icons/LockUnlocked.tsx +34 -0
  93. package/src/lib/seam/access-codes/use-access-code.ts +24 -0
  94. package/src/lib/seam/access-codes/use-access-codes.ts +39 -0
  95. package/src/lib/seam/devices/types.ts +5 -0
  96. package/src/lib/seam/devices/use-device.ts +79 -0
  97. package/src/lib/seam/devices/use-devices.ts +13 -8
  98. package/src/lib/seam/index.ts +4 -1
  99. package/src/lib/seam/use-seam-client.ts +3 -3
  100. package/src/lib/ui/AccessCodeDetails/AccessCodeDetails.tsx +154 -0
  101. package/src/lib/ui/AccessCodeDetails/AccessCodeDevice.tsx +49 -0
  102. package/src/lib/ui/AccessCodeTable/AccessCodeTable.tsx +33 -12
  103. package/src/lib/ui/AccessCodeTable/AccessCodeTableContent.tsx +8 -11
  104. package/src/lib/ui/AccessCodeTable/CodeDetails.tsx +1 -1
  105. package/src/lib/ui/Button.tsx +26 -0
  106. package/src/lib/ui/DeviceDetails/DeviceDetails.tsx +49 -22
  107. package/src/lib/ui/DeviceDetails/DeviceModel.tsx +34 -0
  108. package/src/lib/ui/DeviceTable/DeviceTable.tsx +110 -0
  109. package/src/lib/ui/IconButton.tsx +1 -1
  110. package/src/lib/ui/Table/TableRow.tsx +8 -2
  111. package/src/lib/ui/TextButton.tsx +15 -0
  112. package/src/lib/ui/TextField/SearchTextField.tsx +1 -1
  113. package/src/lib/ui/{DeviceDetails → device}/BatteryStatus.tsx +12 -14
  114. package/src/lib/ui/device/DeviceImage.tsx +15 -0
  115. package/src/lib/ui/device/LockStatus.tsx +46 -0
  116. package/src/lib/ui/{DeviceDetails → device}/OnlineStatus.tsx +15 -11
  117. package/src/lib/ui/index.ts +3 -1
  118. package/src/lib/ui/layout/ContentHeader.tsx +9 -4
  119. package/src/styles/_access-code-details.scss +121 -0
  120. package/src/styles/_access-code-table.scss +34 -38
  121. package/src/styles/_buttons.scss +125 -8
  122. package/src/styles/_colors.scss +30 -0
  123. package/src/styles/_device-details.scss +21 -10
  124. package/src/styles/_device-table.scss +63 -0
  125. package/src/styles/_icons.scss +9 -0
  126. package/src/styles/_inputs.scss +3 -1
  127. package/src/styles/_layout.scss +5 -1
  128. package/src/styles/_main.scss +8 -14
  129. package/src/styles/_tables.scss +6 -0
  130. package/src/styles/_typography.scss +18 -2
  131. package/lib/hooks/use-access-codes.d.ts +0 -5
  132. package/lib/hooks/use-access-codes.js +0 -16
  133. package/lib/hooks/use-access-codes.js.map +0 -1
  134. package/lib/hooks/use-device.d.ts +0 -5
  135. package/lib/hooks/use-device.js +0 -17
  136. package/lib/hooks/use-device.js.map +0 -1
  137. package/lib/hooks/use-devices.d.ts +0 -1
  138. package/lib/hooks/use-devices.js +0 -2
  139. package/lib/hooks/use-devices.js.map +0 -1
  140. package/lib/hooks/use-seam-client.d.ts +0 -1
  141. package/lib/hooks/use-seam-client.js +0 -2
  142. package/lib/hooks/use-seam-client.js.map +0 -1
  143. package/lib/seam/devices/index.d.ts +0 -1
  144. package/lib/seam/devices/index.js +0 -2
  145. package/lib/seam/devices/index.js.map +0 -1
  146. package/lib/types/use-seam-query-result.js.map +0 -1
  147. package/lib/ui/AccessCodeTable/index.d.ts +0 -2
  148. package/lib/ui/AccessCodeTable/index.js +0 -3
  149. package/lib/ui/AccessCodeTable/index.js.map +0 -1
  150. package/lib/ui/DeviceDetails/BatteryStatus.d.ts +0 -4
  151. package/lib/ui/DeviceDetails/BatteryStatus.js.map +0 -1
  152. package/lib/ui/DeviceDetails/DeviceImage.js.map +0 -1
  153. package/lib/ui/DeviceDetails/ModelStatus.d.ts +0 -4
  154. package/lib/ui/DeviceDetails/ModelStatus.js +0 -13
  155. package/lib/ui/DeviceDetails/ModelStatus.js.map +0 -1
  156. package/lib/ui/DeviceDetails/OnlineStatus.d.ts +0 -4
  157. package/lib/ui/DeviceDetails/OnlineStatus.js.map +0 -1
  158. package/lib/ui/DeviceDetails/index.d.ts +0 -5
  159. package/lib/ui/DeviceDetails/index.js +0 -6
  160. package/lib/ui/DeviceDetails/index.js.map +0 -1
  161. package/lib/ui/DeviceManager/DeviceManager.d.ts +0 -6
  162. package/lib/ui/DeviceManager/DeviceManager.js +0 -24
  163. package/lib/ui/DeviceManager/DeviceManager.js.map +0 -1
  164. package/lib/ui/DeviceManager/index.d.ts +0 -1
  165. package/lib/ui/DeviceManager/index.js +0 -2
  166. package/lib/ui/DeviceManager/index.js.map +0 -1
  167. package/lib/ui/Table/index.d.ts +0 -5
  168. package/lib/ui/Table/index.js +0 -6
  169. package/lib/ui/Table/index.js.map +0 -1
  170. package/lib/ui/TextField/index.d.ts +0 -2
  171. package/lib/ui/TextField/index.js +0 -3
  172. package/lib/ui/TextField/index.js.map +0 -1
  173. package/src/lib/hooks/use-access-codes.ts +0 -21
  174. package/src/lib/hooks/use-device.ts +0 -26
  175. package/src/lib/hooks/use-devices.ts +0 -1
  176. package/src/lib/hooks/use-seam-client.ts +0 -1
  177. package/src/lib/seam/devices/index.ts +0 -1
  178. package/src/lib/ui/AccessCodeTable/index.ts +0 -2
  179. package/src/lib/ui/DeviceDetails/DeviceImage.tsx +0 -12
  180. package/src/lib/ui/DeviceDetails/ModelStatus.tsx +0 -25
  181. package/src/lib/ui/DeviceDetails/index.ts +0 -5
  182. package/src/lib/ui/DeviceManager/DeviceManager.tsx +0 -52
  183. package/src/lib/ui/DeviceManager/index.ts +0 -1
  184. package/src/lib/ui/Table/index.ts +0 -5
  185. package/src/lib/ui/TextField/index.ts +0 -2
  186. /package/lib/{types → seam}/use-seam-query-result.d.ts +0 -0
  187. /package/lib/{types → seam}/use-seam-query-result.js +0 -0
  188. /package/src/lib/{types → seam}/use-seam-query-result.ts +0 -0
@@ -0,0 +1,79 @@
1
+ import { useQuery } from '@tanstack/react-query'
2
+ import type {
3
+ CommonDeviceProperties,
4
+ Device,
5
+ DeviceGetRequest,
6
+ DeviceGetResponse,
7
+ LockDevice,
8
+ SeamError,
9
+ } from 'seamapi'
10
+
11
+ import { useSeamClient } from 'lib/seam/use-seam-client.js'
12
+ import type { UseSeamQueryResult } from 'lib/seam/use-seam-query-result.js'
13
+
14
+ export type UseDeviceParams = DeviceGetRequest | string
15
+ export type UseDeviceData = Device<CommonDeviceProperties> | null
16
+
17
+ export function useDevice(
18
+ params: DeviceGetRequest
19
+ ): UseSeamQueryResult<'device', UseDeviceData> {
20
+ const normalizedParams =
21
+ typeof params === 'string' ? { device_id: params } : params
22
+
23
+ const { client } = useSeamClient()
24
+ const { data, ...rest } = useQuery<
25
+ DeviceGetResponse['device'] | null,
26
+ SeamError
27
+ >({
28
+ enabled: client != null,
29
+ queryKey: ['devices', 'get', normalizedParams],
30
+ queryFn: async () => {
31
+ if (client == null) return null
32
+ return await client?.devices.get(normalizedParams)
33
+ },
34
+ })
35
+
36
+ return { ...rest, device: data }
37
+ }
38
+
39
+ export function useFakeDevice(_params: DeviceGetRequest) {
40
+ const device: LockDevice = {
41
+ device_id: 'f9a9ab36-9e14-4390-a88c-b4c78304c6aa',
42
+ device_type: 'august_lock',
43
+ capabilities_supported: ['access_code', 'lock'],
44
+ properties: {
45
+ locked: true,
46
+ online: true,
47
+ door_open: false,
48
+ manufacturer: 'august',
49
+ battery_level: 0.9999532347993827,
50
+ serial_number: '00000004-992d-45a0-bea1-9128fdcd8d12',
51
+ august_metadata: {
52
+ lock_id: 'lock-1',
53
+ house_id: 'house-1',
54
+ lock_name: 'FRONT DOOR',
55
+ has_keypad: true,
56
+ house_name: 'My House',
57
+ },
58
+ supported_code_lengths: [6],
59
+ name: 'FRONT DOOR',
60
+ battery: {
61
+ level: 0.9999532347993827,
62
+ status: 'full',
63
+ },
64
+ image_url:
65
+ 'https://connect.getseam.com/assets/images/devices/august_wifi-smart-lock-3rd-gen_silver_front.png',
66
+ },
67
+ location: {
68
+ timezone: 'America/Los_Angeles',
69
+ location_name: 'My House',
70
+ },
71
+ connected_account_id: '6fc0f854-cb5a-461a-af93-e543ce1f730f',
72
+ workspace_id: 'a9598de8-e627-4d01-b119-61ab7b9e07bd',
73
+ created_at: '2023-05-08T22:37:24.206Z',
74
+ errors: [],
75
+ warnings: [],
76
+ }
77
+
78
+ return { isLoading: false, device }
79
+ }
@@ -1,20 +1,21 @@
1
- import { useQuery, type UseQueryResult } from '@tanstack/react-query'
1
+ import { useQuery } from '@tanstack/react-query'
2
2
  import type {
3
+ CommonDeviceProperties,
4
+ Device,
3
5
  DevicesListRequest,
4
6
  DevicesListResponse,
5
7
  SeamError,
6
8
  } from 'seamapi'
7
9
 
8
10
  import { useSeamClient } from 'lib/seam/use-seam-client.js'
11
+ import type { UseSeamQueryResult } from 'lib/seam/use-seam-query-result.js'
9
12
 
10
13
  export type UseDevicesParams = DevicesListRequest
11
- export type UseDevicesData = DevicesListResponse['devices']
14
+ export type UseDevicesData = Array<Device<CommonDeviceProperties>>
12
15
 
13
- type Result = UseQueryResult<UseDevicesData, SeamError>
14
-
15
- type UseDevicesResult = Omit<Result, 'data'> & { devices: Result['data'] }
16
-
17
- export function useDevices(params: DevicesListRequest): UseDevicesResult {
16
+ export function useDevices(
17
+ params?: UseDevicesParams
18
+ ): UseSeamQueryResult<'devices', UseDevicesData> {
18
19
  const { client } = useSeamClient()
19
20
  const { data, ...rest } = useQuery<DevicesListResponse['devices'], SeamError>(
20
21
  {
@@ -22,7 +23,11 @@ export function useDevices(params: DevicesListRequest): UseDevicesResult {
22
23
  queryKey: ['devices', 'list', params],
23
24
  queryFn: async () => {
24
25
  if (client == null) return []
25
- return await client?.devices.list(params)
26
+ const devices = await client?.devices.list(params)
27
+ return devices.sort(
28
+ (a, b) =>
29
+ new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
30
+ )
26
31
  },
27
32
  }
28
33
  )
@@ -1,2 +1,5 @@
1
- export * from './devices/index.js'
1
+ export * from './access-codes/use-access-code.js'
2
+ export * from './access-codes/use-access-codes.js'
3
+ export * from './devices/use-device.js'
4
+ export * from './devices/use-devices.js'
2
5
  export * from './use-seam-client.js'
@@ -84,11 +84,11 @@ This is not recommended because the client session is now bound to this machine
84
84
  return userIdentifierKey
85
85
  }
86
86
 
87
- const fingerprintValue =
87
+ const fingerprint =
88
88
  globalThis?.localStorage?.getItem('seam_user_fingerprint') ??
89
89
  `fingerprint_${uuidv4()}`
90
90
 
91
- globalThis?.localStorage?.setItem('seam_user_fingerprint', fingerprintValue)
91
+ globalThis?.localStorage?.setItem('seam_user_fingerprint', fingerprint)
92
92
 
93
- return fingerprintValue
93
+ return fingerprint
94
94
  }
@@ -0,0 +1,154 @@
1
+ import { DateTime } from 'luxon'
2
+ import { useState } from 'react'
3
+ import type { AccessCode, CommonDeviceProperties, Device } from 'seamapi'
4
+
5
+ import { AccessCodeDevice } from 'lib/ui/AccessCodeDetails/AccessCodeDevice.js'
6
+ import { DeviceDetails } from 'lib/ui/DeviceDetails/DeviceDetails.js'
7
+ import { ContentHeader } from 'lib/ui/layout/ContentHeader.js'
8
+
9
+ export interface AccessCodeDetailsProps {
10
+ accessCode: AccessCode
11
+ onBack?: () => void
12
+ }
13
+
14
+ export function AccessCodeDetails({
15
+ accessCode,
16
+ onBack,
17
+ }: AccessCodeDetailsProps) {
18
+ const name = accessCode.name ?? t.fallbackName
19
+ const [selectedDevice, selectDevice] =
20
+ useState<Device<CommonDeviceProperties> | null>(null)
21
+
22
+ if (selectedDevice) {
23
+ return (
24
+ <DeviceDetails
25
+ device={selectedDevice}
26
+ onBack={() => {
27
+ selectDevice(null)
28
+ }}
29
+ />
30
+ )
31
+ }
32
+
33
+ return (
34
+ <div className='seam-access-code-details'>
35
+ <ContentHeader title='Access code' onBack={onBack} />
36
+ <div className='seam-summary'>
37
+ <div className='seam-top'>
38
+ <span className='seam-label'>{t.accessCode}</span>
39
+ <h5 className='seam-access-code-name'>{name}</h5>
40
+ <div className='seam-code'>{accessCode.code}</div>
41
+ <div className='seam-duration'>
42
+ <Duration accessCode={accessCode} />
43
+ </div>
44
+ </div>
45
+ <AccessCodeDevice
46
+ deviceId={accessCode.device_id}
47
+ onSelectDevice={selectDevice}
48
+ />
49
+ </div>
50
+ <div className='seam-details'>
51
+ <div className='seam-row'>
52
+ <div className='seam-heading'>{t.id}:</div>
53
+ <div className='seam-content'>{accessCode.access_code_id}</div>
54
+ </div>
55
+ <div className='seam-row'>
56
+ <div className='seam-heading'>{t.created}:</div>
57
+ <div className='seam-content'>
58
+ {formatDate(accessCode.created_at)}
59
+ </div>
60
+ </div>
61
+
62
+ <div className='seam-row seam-schedule'>
63
+ <div className='seam-heading'>{t.timing}:</div>
64
+ <div className='seam-content'>
65
+ <ScheduleInfo accessCode={accessCode} />
66
+ </div>
67
+ </div>
68
+ </div>
69
+ </div>
70
+ )
71
+ }
72
+
73
+ function ScheduleInfo({ accessCode }: { accessCode: AccessCode }) {
74
+ if (accessCode.type === 'ongoing') {
75
+ return <span>{t.ongoing}</span>
76
+ }
77
+ return (
78
+ <div className='seam-times'>
79
+ <div>
80
+ <div className='seam-label'>{t.start}</div>
81
+ <div className='seam-date'>{formatDate(accessCode.starts_at)}</div>
82
+ <div className='seam-time'>{formatTime(accessCode.starts_at)}</div>
83
+ </div>
84
+ <div>
85
+ <div className='seam-label'>{t.end}</div>
86
+ <div className='seam-date'>{formatDate(accessCode.ends_at)}</div>
87
+ <div className='seam-time'>{formatTime(accessCode.ends_at)}</div>
88
+ </div>
89
+ </div>
90
+ )
91
+ }
92
+
93
+ function Duration(props: { accessCode: AccessCode }): JSX.Element {
94
+ const { accessCode } = props
95
+ if (accessCode.type === 'ongoing') {
96
+ return (
97
+ <span>
98
+ <span className='seam-label'>Active</span> (ongoing)
99
+ </span>
100
+ )
101
+ }
102
+
103
+ const hasStarted = new Date(accessCode.starts_at).valueOf() < Date.now()
104
+ if (hasStarted) {
105
+ return (
106
+ <span>
107
+ <span className='seam-label'>Active</span> until{' '}
108
+ {formatDurationDate(accessCode.ends_at)} at{' '}
109
+ {formatTime(accessCode.ends_at)}
110
+ </span>
111
+ )
112
+ }
113
+
114
+ return (
115
+ <span>
116
+ Starts {formatDurationDate(accessCode.starts_at)} as{' '}
117
+ {formatTime(accessCode.starts_at)}
118
+ </span>
119
+ )
120
+ }
121
+
122
+ function formatDurationDate(date: string): string {
123
+ return DateTime.fromISO(date).toLocaleString({
124
+ month: 'short',
125
+ day: 'numeric',
126
+ })
127
+ }
128
+
129
+ function formatTime(date: string): string {
130
+ return DateTime.fromISO(date).toLocaleString({
131
+ hour: 'numeric',
132
+ minute: '2-digit',
133
+ })
134
+ }
135
+
136
+ function formatDate(date: string): string {
137
+ return DateTime.fromISO(date).toLocaleString({
138
+ weekday: 'short',
139
+ month: 'long',
140
+ day: 'numeric',
141
+ year: 'numeric',
142
+ })
143
+ }
144
+
145
+ const t = {
146
+ accessCode: 'Access code',
147
+ fallbackName: 'Code',
148
+ id: 'ID',
149
+ created: 'Created',
150
+ timing: 'Timing',
151
+ ongoing: 'Ongoing',
152
+ start: 'Start',
153
+ end: 'End',
154
+ }
@@ -0,0 +1,49 @@
1
+ import type { CommonDeviceProperties, Device } from 'seamapi'
2
+
3
+ import { isLockDevice } from 'lib/seam/devices/types.js'
4
+ import { useFakeDevice } from 'lib/seam/devices/use-device.js'
5
+ import { Button } from 'lib/ui/Button.js'
6
+ import { DeviceImage } from 'lib/ui/device/DeviceImage.js'
7
+ import { getDeviceModel } from 'lib/ui/DeviceDetails/DeviceModel.js'
8
+ import { TextButton } from 'lib/ui/TextButton.js'
9
+
10
+ export function AccessCodeDevice({
11
+ deviceId,
12
+ onSelectDevice,
13
+ }: {
14
+ deviceId: string
15
+ onSelectDevice: (device: Device<CommonDeviceProperties>) => void
16
+ }) {
17
+ // TODO: Replace with `useDevice()` once ready
18
+ const { isLoading, device } = useFakeDevice({ device_id: deviceId })
19
+
20
+ // TODO: Do we want to return a skeleton loader here instead?
21
+ if (isLoading || !device || !isLockDevice(device)) {
22
+ return null
23
+ }
24
+
25
+ const model = getDeviceModel(device)
26
+
27
+ return (
28
+ <div className='seam-access-code-device'>
29
+ <div className='seam-device-image'>
30
+ <DeviceImage device={device} />
31
+ </div>
32
+ <div className='seam-body'>
33
+ <div className='seam-model'>{model}</div>
34
+ <TextButton
35
+ onClick={() => {
36
+ onSelectDevice(device)
37
+ }}
38
+ >
39
+ {t.deviceDetails}
40
+ </TextButton>
41
+ </div>
42
+ <Button>Unlock</Button>
43
+ </div>
44
+ )
45
+ }
46
+
47
+ const t = {
48
+ deviceDetails: 'Device details',
49
+ }
@@ -1,32 +1,48 @@
1
- import {
2
- TableBody,
3
- TableCell,
4
- TableHeader,
5
- TableRow,
6
- TableTitle,
7
- } from 'lib/ui/Table/index.js'
1
+ import { useState } from 'react'
2
+ import type { AccessCode } from 'seamapi'
8
3
 
9
- import { useAccessCodes } from 'lib/hooks/use-access-codes.js'
10
4
  import { AccessCodeKeyIcon } from 'lib/icons/AccessCodeKey.js'
5
+ import { useAccessCodes } from 'lib/seam/access-codes/use-access-codes.js'
6
+ import { AccessCodeDetails } from 'lib/ui/AccessCodeDetails/AccessCodeDetails.js'
11
7
  import { CodeDetails } from 'lib/ui/AccessCodeTable/CodeDetails.js'
12
8
  import { ContentHeader } from 'lib/ui/layout/ContentHeader.js'
9
+ import { TableBody } from 'lib/ui/Table/TableBody.js'
10
+ import { TableCell } from 'lib/ui/Table/TableCell.js'
11
+ import { TableHeader } from 'lib/ui/Table/TableHeader.js'
12
+ import { TableRow } from 'lib/ui/Table/TableRow.js'
13
+ import { TableTitle } from 'lib/ui/Table/TableTitle.js'
13
14
  import { Caption } from 'lib/ui/typography/Caption.js'
14
15
  import { Title } from 'lib/ui/typography/Title.js'
15
16
 
16
17
  export function AccessCodeTable(props: {
17
18
  deviceId: string
18
- onClickBack?: () => void
19
+ onBack?: () => void
19
20
  }): JSX.Element {
20
21
  const { accessCodes } = useAccessCodes({
21
22
  device_id: props.deviceId,
22
23
  })
23
- const { onClickBack } = props
24
+ const { onBack } = props
25
+
26
+ const [selectedAccessCode, selectAccessCode] = useState<AccessCode | null>(
27
+ null
28
+ )
29
+
30
+ if (selectedAccessCode) {
31
+ return (
32
+ <AccessCodeDetails
33
+ accessCode={selectedAccessCode}
34
+ onBack={() => {
35
+ selectAccessCode(null)
36
+ }}
37
+ />
38
+ )
39
+ }
24
40
 
25
41
  if (!accessCodes) return <>{null}</>
26
42
 
27
43
  return (
28
44
  <div className='seam-access-code-table'>
29
- <ContentHeader onClickBack={onClickBack} />
45
+ <ContentHeader onBack={onBack} />
30
46
  <TableHeader>
31
47
  <TableTitle>
32
48
  {t.accessCodes} <Caption>({accessCodes.length})</Caption>
@@ -34,7 +50,12 @@ export function AccessCodeTable(props: {
34
50
  </TableHeader>
35
51
  <TableBody>
36
52
  {accessCodes.map((code) => (
37
- <TableRow key={code.access_code_id}>
53
+ <TableRow
54
+ key={code.access_code_id}
55
+ onClick={() => {
56
+ selectAccessCode(code)
57
+ }}
58
+ >
38
59
  <TableCell className='seam-icon-cell'>
39
60
  <div>
40
61
  <AccessCodeKeyIcon />
@@ -1,32 +1,29 @@
1
1
  import type { AccessCode } from 'seamapi'
2
2
 
3
- import {
4
- TableBody,
5
- TableCell,
6
- TableHeader,
7
- TableRow,
8
- TableTitle,
9
- } from 'lib/ui/Table/index.js'
10
-
11
3
  import { AccessCodeKeyIcon } from 'lib/icons/AccessCodeKey.js'
12
4
  import { CodeDetails } from 'lib/ui/AccessCodeTable/CodeDetails.js'
13
5
  import { ContentHeader } from 'lib/ui/layout/ContentHeader.js'
6
+ import { TableBody } from 'lib/ui/Table/TableBody.js'
7
+ import { TableCell } from 'lib/ui/Table/TableCell.js'
8
+ import { TableHeader } from 'lib/ui/Table/TableHeader.js'
9
+ import { TableRow } from 'lib/ui/Table/TableRow.js'
10
+ import { TableTitle } from 'lib/ui/Table/TableTitle.js'
14
11
  import { Caption } from 'lib/ui/typography/Caption.js'
15
12
  import { Title } from 'lib/ui/typography/Title.js'
16
13
 
17
14
  export interface AccessCodeTableContentProps {
18
15
  accessCodes: AccessCode[]
19
- onClickBack?: () => void
16
+ onBack?: () => void
20
17
  }
21
18
 
22
19
  export function AccessCodeTableContent(
23
20
  props: AccessCodeTableContentProps
24
21
  ): JSX.Element {
25
- const { accessCodes, onClickBack } = props
22
+ const { accessCodes, onBack } = props
26
23
 
27
24
  return (
28
25
  <div className='seam-access-code-table'>
29
- <ContentHeader onClickBack={onClickBack} />
26
+ <ContentHeader onBack={onBack} />
30
27
  <TableHeader>
31
28
  <TableTitle>
32
29
  {t.accessCodes} <Caption>({accessCodes.length})</Caption>
@@ -7,7 +7,7 @@ export function CodeDetails(props: { accessCode: AccessCode }): JSX.Element {
7
7
  const { accessCode } = props
8
8
 
9
9
  return (
10
- <div className='seam-access-code-details'>
10
+ <div className='seam-code-details'>
11
11
  Unit 110
12
12
  <DotDivider />
13
13
  <Duration accessCode={accessCode} />
@@ -0,0 +1,26 @@
1
+ import classNames from 'classnames'
2
+
3
+ interface ButtonProps {
4
+ children?: string
5
+ variant?: 'solid' | 'outline' | 'neutral'
6
+ size?: 'small' | 'medium' | 'large'
7
+ disabled?: boolean
8
+ }
9
+
10
+ export function Button({
11
+ variant = 'outline',
12
+ children,
13
+ size = 'medium',
14
+ disabled,
15
+ }: ButtonProps) {
16
+ return (
17
+ <button
18
+ className={classNames(`seam-btn seam-btn-${variant} seam-btn-${size}`, {
19
+ 'seam-btn-disabled': disabled,
20
+ })}
21
+ disabled={disabled}
22
+ >
23
+ {children}
24
+ </button>
25
+ )
26
+ }
@@ -1,40 +1,55 @@
1
- import { type LockDevice } from 'seamapi'
1
+ import { type CommonDeviceProperties, type Device } from 'seamapi'
2
2
 
3
3
  import { ChevronRightIcon } from 'lib/icons/ChevronRight.js'
4
+ import { useAccessCodes } from 'lib/seam/access-codes/use-access-codes.js'
5
+ import { isLockDevice } from 'lib/seam/devices/types.js'
4
6
  import { AccessCodeTable } from 'lib/ui/AccessCodeTable/AccessCodeTable.js'
5
- import { BatteryStatus } from 'lib/ui/DeviceDetails/BatteryStatus.js'
6
- import { DeviceImage } from 'lib/ui/DeviceDetails/DeviceImage.js'
7
- import { ModelStatus } from 'lib/ui/DeviceDetails/ModelStatus.js'
8
- import { OnlineStatus } from 'lib/ui/DeviceDetails/OnlineStatus.js'
7
+ import { Button } from 'lib/ui/Button.js'
8
+ import { BatteryStatus } from 'lib/ui/device/BatteryStatus.js'
9
+ import { DeviceImage } from 'lib/ui/device/DeviceImage.js'
10
+ import { OnlineStatus } from 'lib/ui/device/OnlineStatus.js'
11
+ import { DeviceModel } from 'lib/ui/DeviceDetails/DeviceModel.js'
9
12
  import { ContentHeader } from 'lib/ui/layout/ContentHeader.js'
10
13
  import useToggle from 'lib/use-toggle.js'
11
14
 
12
15
  export interface DeviceDetailsProps {
13
- device: LockDevice
16
+ device: Device<CommonDeviceProperties>
17
+ onBack?: () => void
14
18
  }
15
19
 
16
- export function DeviceDetails(props: DeviceDetailsProps): JSX.Element {
17
- const { device } = props
20
+ export function DeviceDetails(props: DeviceDetailsProps): JSX.Element | null {
21
+ const { device, onBack } = props
18
22
 
19
- const lockStatus = device.properties.locked ? t.locked : t.unlocked
23
+ const [showingAccessCodes, toggleAccessCodes] = useToggle()
20
24
 
21
- const accessCodeLength =
22
- device.properties?.schlage_metadata?.access_code_length
25
+ const { isLoading, accessCodes } = useAccessCodes({
26
+ device_id: device.device_id,
27
+ })
23
28
 
24
- const [showingAccessCodes, toggleAccessCodes] = useToggle()
29
+ if (isLoading) {
30
+ return null
31
+ }
25
32
 
26
33
  if (showingAccessCodes) {
27
34
  return (
28
- <AccessCodeTable
29
- deviceId={device.device_id}
30
- onClickBack={toggleAccessCodes}
31
- />
35
+ <AccessCodeTable deviceId={device.device_id} onBack={toggleAccessCodes} />
32
36
  )
33
37
  }
34
38
 
39
+ if (!isLockDevice(device)) {
40
+ return null
41
+ }
42
+
43
+ const lockStatus = device.properties.locked ? t.locked : t.unlocked
44
+
45
+ const accessCodeCount = accessCodes?.length
46
+
47
+ const accessCodeLength =
48
+ device.properties?.schlage_metadata?.access_code_length
49
+
35
50
  return (
36
51
  <div className='seam-device-details'>
37
- <ContentHeader title='Device' />
52
+ <ContentHeader title='Device' onBack={onBack} />
38
53
  <div className='seam-body'>
39
54
  <div className='seam-summary'>
40
55
  <div className='seam-content'>
@@ -45,9 +60,11 @@ export function DeviceDetails(props: DeviceDetailsProps): JSX.Element {
45
60
  <span className='seam-label'>{t.device}</span>
46
61
  <h4 className='seam-device-name'>{device.properties.name}</h4>
47
62
  <div className='seam-properties'>
63
+ <span className='seam-label'>{t.status}:</span>{' '}
48
64
  <OnlineStatus device={device} />
65
+ <span className='seam-label'>{t.power}:</span>{' '}
49
66
  <BatteryStatus device={device} />
50
- <ModelStatus device={device} />
67
+ <DeviceModel device={device} />
51
68
  </div>
52
69
  </div>
53
70
  </div>
@@ -57,15 +74,22 @@ export function DeviceDetails(props: DeviceDetailsProps): JSX.Element {
57
74
  className='seam-content seam-access-codes'
58
75
  onClick={toggleAccessCodes}
59
76
  >
60
- <span className='seam-value'>49 {t.accessCodes}</span>
77
+ <span className='seam-value'>
78
+ {accessCodeCount} {t.accessCodes}
79
+ </span>
61
80
  <ChevronRightIcon />
62
81
  </div>
63
82
  </div>
64
83
 
65
84
  <div className='seam-box'>
66
- <div className='seam-content'>
67
- <span className='seam-label'>{t.lockStatus}</span>
68
- <span className='seam-value'>{lockStatus}</span>
85
+ <div className='seam-content seam-lock-status'>
86
+ <div>
87
+ <span className='seam-label'>{t.lockStatus}</span>
88
+ <span className='seam-value'>{lockStatus}</span>
89
+ </div>
90
+ <div className='seam-right'>
91
+ <Button size='small'>{t.unlock}</Button>
92
+ </div>
69
93
  </div>
70
94
  <AccessCodeLength accessCodeLength={accessCodeLength} />
71
95
  </div>
@@ -94,10 +118,13 @@ function AccessCodeLength(props: {
94
118
 
95
119
  const t = {
96
120
  device: 'Device',
121
+ unlock: 'Unlock',
97
122
  locked: 'Locked',
98
123
  unlocked: 'Unlocked',
99
124
  accessCodes: 'access codes',
100
125
  codeLength: 'Code length',
101
126
  digits: 'digits',
102
127
  lockStatus: 'Lock status',
128
+ status: 'Status',
129
+ power: 'Power',
103
130
  }
@@ -0,0 +1,34 @@
1
+ import type { LockDevice } from 'seamapi'
2
+
3
+ interface DeviceModelProps {
4
+ device: LockDevice
5
+ }
6
+
7
+ export function DeviceModel({ device }: DeviceModelProps): JSX.Element | null {
8
+ const model = getDeviceModel(device)
9
+ if (!model) {
10
+ return null
11
+ }
12
+
13
+ return (
14
+ <>
15
+ <span className='seam-label'>{t.model}:</span>{' '}
16
+ <div className='seam-device-model'>{model}</div>
17
+ </>
18
+ )
19
+ }
20
+
21
+ export const getDeviceModel = ({
22
+ properties,
23
+ }: LockDevice): string | undefined => {
24
+ return (
25
+ properties?.august_metadata?.model ??
26
+ properties?.schlage_metadata?.model ??
27
+ t.fallbackModel
28
+ )
29
+ }
30
+
31
+ const t = {
32
+ model: 'Model',
33
+ fallbackModel: 'Unknown Lock',
34
+ }