@seamapi/react 0.16.0 → 0.17.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 (197) hide show
  1. package/README.md +5 -2
  2. package/index.css +403 -71
  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/NavigationProvider.d.ts +27 -0
  7. package/lib/NavigationProvider.js +36 -0
  8. package/lib/NavigationProvider.js.map +1 -0
  9. package/lib/NavigationView.d.ts +5 -0
  10. package/lib/NavigationView.js +25 -0
  11. package/lib/NavigationView.js.map +1 -0
  12. package/lib/icons/LockLocked.d.ts +2 -0
  13. package/lib/icons/LockLocked.js +5 -0
  14. package/lib/icons/LockLocked.js.map +1 -0
  15. package/lib/icons/LockUnlocked.d.ts +2 -0
  16. package/lib/icons/LockUnlocked.js +5 -0
  17. package/lib/icons/LockUnlocked.js.map +1 -0
  18. package/lib/seam/access-codes/use-access-code.d.ts +8 -0
  19. package/lib/seam/access-codes/use-access-code.js +32 -0
  20. package/lib/seam/access-codes/use-access-code.js.map +1 -0
  21. package/lib/seam/access-codes/use-access-codes.d.ts +9 -0
  22. package/lib/seam/access-codes/use-access-codes.js +18 -0
  23. package/lib/seam/access-codes/use-access-codes.js.map +1 -0
  24. package/lib/seam/devices/types.d.ts +2 -0
  25. package/lib/seam/devices/types.js +2 -0
  26. package/lib/seam/devices/types.js.map +1 -0
  27. package/lib/seam/devices/use-device.d.ts +9 -0
  28. package/lib/seam/devices/use-device.js +56 -0
  29. package/lib/seam/devices/use-device.js.map +1 -0
  30. package/lib/seam/devices/use-devices.d.ts +4 -9
  31. package/lib/seam/devices/use-devices.js +2 -1
  32. package/lib/seam/devices/use-devices.js.map +1 -1
  33. package/lib/seam/index.d.ts +4 -1
  34. package/lib/seam/index.js +4 -1
  35. package/lib/seam/index.js.map +1 -1
  36. package/lib/seam/use-seam-client.js +12 -9
  37. package/lib/seam/use-seam-client.js.map +1 -1
  38. package/lib/seam/use-seam-query-result.js.map +1 -0
  39. package/lib/ui/AccessCodeDetails/AccessCodeDetails.d.ts +4 -0
  40. package/lib/ui/AccessCodeDetails/AccessCodeDetails.js +63 -0
  41. package/lib/ui/AccessCodeDetails/AccessCodeDetails.js.map +1 -0
  42. package/lib/ui/AccessCodeDetails/AccessCodeDevice.d.ts +3 -0
  43. package/lib/ui/AccessCodeDetails/AccessCodeDevice.js +25 -0
  44. package/lib/ui/AccessCodeDetails/AccessCodeDevice.js.map +1 -0
  45. package/lib/ui/AccessCodeTable/AccessCodeTable.d.ts +0 -1
  46. package/lib/ui/AccessCodeTable/AccessCodeTable.js +14 -4
  47. package/lib/ui/AccessCodeTable/AccessCodeTable.js.map +1 -1
  48. package/lib/ui/AccessCodeTable/AccessCodeTableContent.d.ts +5 -0
  49. package/lib/ui/AccessCodeTable/AccessCodeTableContent.js +19 -0
  50. package/lib/ui/AccessCodeTable/AccessCodeTableContent.js.map +1 -0
  51. package/lib/ui/AccessCodeTable/CodeDetails.js +1 -1
  52. package/lib/ui/AccessCodeTable/CodeDetails.js.map +1 -1
  53. package/lib/ui/Button.d.ts +8 -0
  54. package/lib/ui/Button.js +8 -0
  55. package/lib/ui/Button.js.map +1 -0
  56. package/lib/ui/DeviceDetails/DeviceDetails.d.ts +2 -3
  57. package/lib/ui/DeviceDetails/DeviceDetails.js +32 -13
  58. package/lib/ui/DeviceDetails/DeviceDetails.js.map +1 -1
  59. package/lib/ui/DeviceDetails/DeviceModel.d.ts +7 -0
  60. package/lib/ui/DeviceDetails/DeviceModel.js +18 -0
  61. package/lib/ui/DeviceDetails/DeviceModel.js.map +1 -0
  62. package/lib/ui/DeviceTable/DeviceTable.d.ts +7 -0
  63. package/lib/ui/DeviceTable/DeviceTable.js +43 -0
  64. package/lib/ui/DeviceTable/DeviceTable.js.map +1 -0
  65. package/lib/ui/IconButton.js +1 -1
  66. package/lib/ui/IconButton.js.map +1 -1
  67. package/lib/ui/Table/TableRow.d.ts +3 -1
  68. package/lib/ui/Table/TableRow.js +1 -1
  69. package/lib/ui/Table/TableRow.js.map +1 -1
  70. package/lib/ui/TextButton.d.ts +7 -0
  71. package/lib/ui/TextButton.js +5 -0
  72. package/lib/ui/TextButton.js.map +1 -0
  73. package/lib/ui/TextField/SearchTextField.d.ts +1 -1
  74. package/lib/ui/TextField/SearchTextField.js +1 -1
  75. package/lib/ui/TextField/SearchTextField.js.map +1 -1
  76. package/lib/ui/device/BatteryStatus.d.ts +5 -0
  77. package/lib/ui/{DeviceDetails → device}/BatteryStatus.js +5 -6
  78. package/lib/ui/device/BatteryStatus.js.map +1 -0
  79. package/lib/ui/{DeviceDetails → device}/DeviceImage.d.ts +2 -1
  80. package/lib/ui/{DeviceDetails → device}/DeviceImage.js +2 -2
  81. package/lib/ui/device/DeviceImage.js.map +1 -0
  82. package/lib/ui/device/LockStatus.d.ts +5 -0
  83. package/lib/ui/device/LockStatus.js +19 -0
  84. package/lib/ui/device/LockStatus.js.map +1 -0
  85. package/lib/ui/device/OnlineStatus.d.ts +5 -0
  86. package/lib/ui/{DeviceDetails → device}/OnlineStatus.js +4 -5
  87. package/lib/ui/device/OnlineStatus.js.map +1 -0
  88. package/lib/ui/index.d.ts +3 -1
  89. package/lib/ui/index.js +3 -1
  90. package/lib/ui/index.js.map +1 -1
  91. package/lib/ui/layout/ContentHeader.d.ts +1 -2
  92. package/lib/ui/layout/ContentHeader.js +7 -1
  93. package/lib/ui/layout/ContentHeader.js.map +1 -1
  94. package/package.json +8 -7
  95. package/src/icons/lock-locked.svg +8 -0
  96. package/src/icons/lock-unlocked.svg +9 -0
  97. package/src/lib/NavigationProvider.tsx +86 -0
  98. package/src/lib/NavigationView.tsx +32 -0
  99. package/src/lib/icons/LockLocked.tsx +32 -0
  100. package/src/lib/icons/LockUnlocked.tsx +34 -0
  101. package/src/lib/seam/access-codes/use-access-code.ts +45 -0
  102. package/src/lib/seam/access-codes/use-access-codes.ts +39 -0
  103. package/src/lib/seam/devices/types.ts +5 -0
  104. package/src/lib/seam/devices/use-device.ts +79 -0
  105. package/src/lib/seam/devices/use-devices.ts +13 -8
  106. package/src/lib/seam/index.ts +4 -1
  107. package/src/lib/seam/use-seam-client.ts +12 -9
  108. package/src/lib/ui/AccessCodeDetails/AccessCodeDetails.tsx +144 -0
  109. package/src/lib/ui/AccessCodeDetails/AccessCodeDevice.tsx +43 -0
  110. package/src/lib/ui/AccessCodeTable/AccessCodeTable.tsx +20 -16
  111. package/src/lib/ui/AccessCodeTable/AccessCodeTableContent.tsx +57 -0
  112. package/src/lib/ui/AccessCodeTable/CodeDetails.tsx +1 -1
  113. package/src/lib/ui/Button.tsx +26 -0
  114. package/src/lib/ui/DeviceDetails/DeviceDetails.tsx +56 -29
  115. package/src/lib/ui/DeviceDetails/DeviceModel.tsx +34 -0
  116. package/src/lib/ui/DeviceTable/DeviceTable.tsx +94 -0
  117. package/src/lib/ui/IconButton.tsx +1 -1
  118. package/src/lib/ui/Table/TableRow.tsx +8 -2
  119. package/src/lib/ui/TextButton.tsx +15 -0
  120. package/src/lib/ui/TextField/SearchTextField.tsx +1 -1
  121. package/src/lib/ui/{DeviceDetails → device}/BatteryStatus.tsx +12 -14
  122. package/src/lib/ui/device/DeviceImage.tsx +15 -0
  123. package/src/lib/ui/device/LockStatus.tsx +46 -0
  124. package/src/lib/ui/{DeviceDetails → device}/OnlineStatus.tsx +15 -11
  125. package/src/lib/ui/index.ts +3 -1
  126. package/src/lib/ui/layout/ContentHeader.tsx +11 -6
  127. package/src/styles/_access-code-details.scss +121 -0
  128. package/src/styles/_access-code-table.scss +34 -38
  129. package/src/styles/_buttons.scss +125 -8
  130. package/src/styles/_colors.scss +30 -0
  131. package/src/styles/_device-details.scss +28 -9
  132. package/src/styles/_device-table.scss +63 -0
  133. package/src/styles/_icons.scss +9 -0
  134. package/src/styles/_inputs.scss +3 -1
  135. package/src/styles/_layout.scss +5 -2
  136. package/src/styles/_main.scss +10 -16
  137. package/src/styles/_reset.scss +114 -0
  138. package/src/styles/_tables.scss +6 -0
  139. package/src/styles/_typography.scss +18 -2
  140. package/lib/hooks/use-access-codes.d.ts +0 -5
  141. package/lib/hooks/use-access-codes.js +0 -16
  142. package/lib/hooks/use-access-codes.js.map +0 -1
  143. package/lib/hooks/use-device.d.ts +0 -5
  144. package/lib/hooks/use-device.js +0 -17
  145. package/lib/hooks/use-device.js.map +0 -1
  146. package/lib/hooks/use-devices.d.ts +0 -1
  147. package/lib/hooks/use-devices.js +0 -2
  148. package/lib/hooks/use-devices.js.map +0 -1
  149. package/lib/hooks/use-seam-client.d.ts +0 -1
  150. package/lib/hooks/use-seam-client.js +0 -2
  151. package/lib/hooks/use-seam-client.js.map +0 -1
  152. package/lib/seam/devices/index.d.ts +0 -1
  153. package/lib/seam/devices/index.js +0 -2
  154. package/lib/seam/devices/index.js.map +0 -1
  155. package/lib/types/use-seam-query-result.js.map +0 -1
  156. package/lib/ui/AccessCodeTable/index.d.ts +0 -2
  157. package/lib/ui/AccessCodeTable/index.js +0 -3
  158. package/lib/ui/AccessCodeTable/index.js.map +0 -1
  159. package/lib/ui/DeviceDetails/BatteryStatus.d.ts +0 -4
  160. package/lib/ui/DeviceDetails/BatteryStatus.js.map +0 -1
  161. package/lib/ui/DeviceDetails/DeviceImage.js.map +0 -1
  162. package/lib/ui/DeviceDetails/ModelStatus.d.ts +0 -4
  163. package/lib/ui/DeviceDetails/ModelStatus.js +0 -13
  164. package/lib/ui/DeviceDetails/ModelStatus.js.map +0 -1
  165. package/lib/ui/DeviceDetails/OnlineStatus.d.ts +0 -4
  166. package/lib/ui/DeviceDetails/OnlineStatus.js.map +0 -1
  167. package/lib/ui/DeviceDetails/index.d.ts +0 -5
  168. package/lib/ui/DeviceDetails/index.js +0 -6
  169. package/lib/ui/DeviceDetails/index.js.map +0 -1
  170. package/lib/ui/DeviceManager/DeviceManager.d.ts +0 -6
  171. package/lib/ui/DeviceManager/DeviceManager.js +0 -24
  172. package/lib/ui/DeviceManager/DeviceManager.js.map +0 -1
  173. package/lib/ui/DeviceManager/index.d.ts +0 -1
  174. package/lib/ui/DeviceManager/index.js +0 -2
  175. package/lib/ui/DeviceManager/index.js.map +0 -1
  176. package/lib/ui/Table/index.d.ts +0 -5
  177. package/lib/ui/Table/index.js +0 -6
  178. package/lib/ui/Table/index.js.map +0 -1
  179. package/lib/ui/TextField/index.d.ts +0 -2
  180. package/lib/ui/TextField/index.js +0 -3
  181. package/lib/ui/TextField/index.js.map +0 -1
  182. package/src/lib/hooks/use-access-codes.ts +0 -21
  183. package/src/lib/hooks/use-device.ts +0 -26
  184. package/src/lib/hooks/use-devices.ts +0 -1
  185. package/src/lib/hooks/use-seam-client.ts +0 -1
  186. package/src/lib/seam/devices/index.ts +0 -1
  187. package/src/lib/ui/AccessCodeTable/index.ts +0 -2
  188. package/src/lib/ui/DeviceDetails/DeviceImage.tsx +0 -12
  189. package/src/lib/ui/DeviceDetails/ModelStatus.tsx +0 -25
  190. package/src/lib/ui/DeviceDetails/index.ts +0 -5
  191. package/src/lib/ui/DeviceManager/DeviceManager.tsx +0 -52
  192. package/src/lib/ui/DeviceManager/index.ts +0 -1
  193. package/src/lib/ui/Table/index.ts +0 -5
  194. package/src/lib/ui/TextField/index.ts +0 -2
  195. /package/lib/{types → seam}/use-seam-query-result.d.ts +0 -0
  196. /package/lib/{types → seam}/use-seam-query-result.js +0 -0
  197. /package/src/lib/{types → seam}/use-seam-query-result.ts +0 -0
@@ -0,0 +1,43 @@
1
+ import { useNavigation } from 'lib/NavigationProvider.js'
2
+ import { isLockDevice } from 'lib/seam/devices/types.js'
3
+ import { useFakeDevice } from 'lib/seam/devices/use-device.js'
4
+ import { Button } from 'lib/ui/Button.js'
5
+ import { DeviceImage } from 'lib/ui/device/DeviceImage.js'
6
+ import { getDeviceModel } from 'lib/ui/DeviceDetails/DeviceModel.js'
7
+ import { TextButton } from 'lib/ui/TextButton.js'
8
+
9
+ export default function AccessCodeDevice({ deviceId }: { deviceId: string }) {
10
+ // TODO: Replace with `useDevice()` once ready
11
+ const { isLoading, device } = useFakeDevice({ device_id: deviceId })
12
+ const { show } = useNavigation()
13
+
14
+ // TODO: Do we want to return a skeleton loader here instead?
15
+ if (isLoading || !device || !isLockDevice(device)) {
16
+ return null
17
+ }
18
+
19
+ const model = getDeviceModel(device)
20
+
21
+ return (
22
+ <div className='seam-access-code-device'>
23
+ <div className='seam-device-image'>
24
+ <DeviceImage device={device} />
25
+ </div>
26
+ <div className='seam-body'>
27
+ <div className='seam-model'>{model}</div>
28
+ <TextButton
29
+ onClick={() => {
30
+ show({ name: 'device_detail', deviceId: device.device_id })
31
+ }}
32
+ >
33
+ {t.deviceDetails}
34
+ </TextButton>
35
+ </div>
36
+ <Button>Unlock</Button>
37
+ </div>
38
+ )
39
+ }
40
+
41
+ const t = {
42
+ deviceDetails: 'Device details',
43
+ }
@@ -1,32 +1,28 @@
1
- import {
2
- TableBody,
3
- TableCell,
4
- TableHeader,
5
- TableRow,
6
- TableTitle,
7
- } from 'lib/ui/Table/index.js'
8
-
9
- import { useAccessCodes } from 'lib/hooks/use-access-codes.js'
10
1
  import { AccessCodeKeyIcon } from 'lib/icons/AccessCodeKey.js'
2
+ import { useNavigation } from 'lib/NavigationProvider.js'
3
+ import { useAccessCodes } from 'lib/seam/access-codes/use-access-codes.js'
11
4
  import { CodeDetails } from 'lib/ui/AccessCodeTable/CodeDetails.js'
12
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'
13
11
  import { Caption } from 'lib/ui/typography/Caption.js'
14
12
  import { Title } from 'lib/ui/typography/Title.js'
15
13
 
16
- export function AccessCodeTable(props: {
17
- deviceId: string
18
- onClickBack?: () => void
19
- }): JSX.Element {
14
+ export function AccessCodeTable(props: { deviceId: string }): JSX.Element {
20
15
  const { accessCodes } = useAccessCodes({
21
16
  device_id: props.deviceId,
22
17
  })
23
- const { onClickBack } = props
18
+
19
+ const { show } = useNavigation()
24
20
 
25
21
  if (!accessCodes) return <>{null}</>
26
22
 
27
23
  return (
28
24
  <div className='seam-access-code-table'>
29
- <ContentHeader onClickBack={onClickBack} />
25
+ <ContentHeader />
30
26
  <TableHeader>
31
27
  <TableTitle>
32
28
  {t.accessCodes} <Caption>({accessCodes.length})</Caption>
@@ -34,7 +30,15 @@ export function AccessCodeTable(props: {
34
30
  </TableHeader>
35
31
  <TableBody>
36
32
  {accessCodes.map((code) => (
37
- <TableRow key={code.access_code_id}>
33
+ <TableRow
34
+ key={code.access_code_id}
35
+ onClick={() => {
36
+ show({
37
+ name: 'access_code_detail',
38
+ accessCodeId: code.access_code_id,
39
+ })
40
+ }}
41
+ >
38
42
  <TableCell className='seam-icon-cell'>
39
43
  <div>
40
44
  <AccessCodeKeyIcon />
@@ -0,0 +1,57 @@
1
+ import type { AccessCode } from 'seamapi'
2
+
3
+ import { AccessCodeKeyIcon } from 'lib/icons/AccessCodeKey.js'
4
+ import { CodeDetails } from 'lib/ui/AccessCodeTable/CodeDetails.js'
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'
11
+ import { Caption } from 'lib/ui/typography/Caption.js'
12
+ import { Title } from 'lib/ui/typography/Title.js'
13
+
14
+ export interface AccessCodeTableContentProps {
15
+ accessCodes: AccessCode[]
16
+ }
17
+
18
+ export function AccessCodeTableContent(
19
+ props: AccessCodeTableContentProps
20
+ ): JSX.Element {
21
+ const { accessCodes } = props
22
+
23
+ return (
24
+ <div className='seam-access-code-table'>
25
+ <ContentHeader />
26
+ <TableHeader>
27
+ <TableTitle>
28
+ {t.accessCodes} <Caption>({accessCodes.length})</Caption>
29
+ </TableTitle>
30
+ </TableHeader>
31
+ <TableBody>
32
+ {accessCodes.map((code) => (
33
+ <TableRow key={code.access_code_id}>
34
+ <TableCell className='seam-icon-cell'>
35
+ <div>
36
+ <AccessCodeKeyIcon />
37
+ </div>
38
+ </TableCell>
39
+ <TableCell className='seam-name-cell'>
40
+ <Title>{code.name}</Title>
41
+ <CodeDetails accessCode={code} />
42
+ </TableCell>
43
+ <TableCell className='seam-action-cell'>
44
+ {/* <IconButton>
45
+ <DotsEllipsisMoreIcon />
46
+ </IconButton> */}
47
+ </TableCell>
48
+ </TableRow>
49
+ ))}
50
+ </TableBody>
51
+ </div>
52
+ )
53
+ }
54
+
55
+ const t = {
56
+ accessCodes: 'Access Codes',
57
+ }
@@ -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,37 +1,49 @@
1
- import { type LockDevice } from 'seamapi'
2
-
3
1
  import { ChevronRightIcon } from 'lib/icons/ChevronRight.js'
4
- 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'
2
+ import { useNavigation } from 'lib/NavigationProvider.js'
3
+ import { useAccessCodes } from 'lib/seam/access-codes/use-access-codes.js'
4
+ import { isLockDevice } from 'lib/seam/devices/types.js'
5
+ import { useFakeDevice } from 'lib/seam/devices/use-device.js'
6
+ import { Button } from 'lib/ui/Button.js'
7
+ import { BatteryStatus } from 'lib/ui/device/BatteryStatus.js'
8
+ import { DeviceImage } from 'lib/ui/device/DeviceImage.js'
9
+ import { OnlineStatus } from 'lib/ui/device/OnlineStatus.js'
10
+ import { DeviceModel } from 'lib/ui/DeviceDetails/DeviceModel.js'
9
11
  import { ContentHeader } from 'lib/ui/layout/ContentHeader.js'
10
- import useToggle from 'lib/use-toggle.js'
11
12
 
12
13
  export interface DeviceDetailsProps {
13
- device: LockDevice
14
+ deviceId: string
14
15
  }
15
16
 
16
- export function DeviceDetails(props: DeviceDetailsProps): JSX.Element {
17
- const { device } = props
17
+ export function DeviceDetails(props: DeviceDetailsProps): JSX.Element | null {
18
+ const { deviceId } = props
18
19
 
19
- const lockStatus = device.properties.locked ? t.locked : t.unlocked
20
+ const { isLoading: isLoadingDevice, device } = useFakeDevice({
21
+ device_id: deviceId,
22
+ })
20
23
 
21
- const accessCodeLength =
22
- device.properties?.schlage_metadata?.access_code_length
24
+ const { isLoading: isLoadingAccessCodes, accessCodes } = useAccessCodes({
25
+ device_id: deviceId,
26
+ })
27
+
28
+ const isLoading = isLoadingDevice || isLoadingAccessCodes
23
29
 
24
- const [showingAccessCodes, toggleAccessCodes] = useToggle()
30
+ const { show } = useNavigation()
25
31
 
26
- if (showingAccessCodes) {
27
- return (
28
- <AccessCodeTable
29
- deviceId={device.device_id}
30
- onClickBack={toggleAccessCodes}
31
- />
32
- )
32
+ if (isLoading || !device) {
33
+ return null
33
34
  }
34
35
 
36
+ if (!isLockDevice(device)) {
37
+ return null
38
+ }
39
+
40
+ const lockStatus = device.properties.locked ? t.locked : t.unlocked
41
+
42
+ const accessCodeCount = accessCodes?.length
43
+
44
+ const accessCodeLength =
45
+ device.properties?.schlage_metadata?.access_code_length
46
+
35
47
  return (
36
48
  <div className='seam-device-details'>
37
49
  <ContentHeader title='Device' />
@@ -41,13 +53,15 @@ export function DeviceDetails(props: DeviceDetailsProps): JSX.Element {
41
53
  <div className='seam-image'>
42
54
  <DeviceImage device={device} />
43
55
  </div>
44
-
45
56
  <div className='seam-info'>
57
+ <span className='seam-label'>{t.device}</span>
46
58
  <h4 className='seam-device-name'>{device.properties.name}</h4>
47
59
  <div className='seam-properties'>
60
+ <span className='seam-label'>{t.status}:</span>{' '}
48
61
  <OnlineStatus device={device} />
62
+ <span className='seam-label'>{t.power}:</span>{' '}
49
63
  <BatteryStatus device={device} />
50
- <ModelStatus device={device} />
64
+ <DeviceModel device={device} />
51
65
  </div>
52
66
  </div>
53
67
  </div>
@@ -55,17 +69,26 @@ export function DeviceDetails(props: DeviceDetailsProps): JSX.Element {
55
69
  <div className='seam-box'>
56
70
  <div
57
71
  className='seam-content seam-access-codes'
58
- onClick={toggleAccessCodes}
72
+ onClick={() => {
73
+ show({ name: 'access_code_table', deviceId: device.device_id })
74
+ }}
59
75
  >
60
- <span className='seam-value'>49 {t.accessCodes}</span>
76
+ <span className='seam-value'>
77
+ {accessCodeCount} {t.accessCodes}
78
+ </span>
61
79
  <ChevronRightIcon />
62
80
  </div>
63
81
  </div>
64
82
 
65
83
  <div className='seam-box'>
66
- <div className='seam-content'>
67
- <span className='seam-label'>{t.lockStatus}</span>
68
- <span className='seam-value'>{lockStatus}</span>
84
+ <div className='seam-content seam-lock-status'>
85
+ <div>
86
+ <span className='seam-label'>{t.lockStatus}</span>
87
+ <span className='seam-value'>{lockStatus}</span>
88
+ </div>
89
+ <div className='seam-right'>
90
+ <Button size='small'>{t.unlock}</Button>
91
+ </div>
69
92
  </div>
70
93
  <AccessCodeLength accessCodeLength={accessCodeLength} />
71
94
  </div>
@@ -93,10 +116,14 @@ function AccessCodeLength(props: {
93
116
  }
94
117
 
95
118
  const t = {
119
+ device: 'Device',
120
+ unlock: 'Unlock',
96
121
  locked: 'Locked',
97
122
  unlocked: 'Unlocked',
98
123
  accessCodes: 'access codes',
99
124
  codeLength: 'Code length',
100
125
  digits: 'digits',
101
126
  lockStatus: 'Lock status',
127
+ status: 'Status',
128
+ power: 'Power',
102
129
  }
@@ -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
+ }
@@ -0,0 +1,94 @@
1
+ import { useNavigation } from 'lib/NavigationProvider.js'
2
+ import { isLockDevice } from 'lib/seam/devices/types.js'
3
+ import {
4
+ useDevices,
5
+ type UseDevicesData,
6
+ type UseDevicesParams,
7
+ } from 'lib/seam/devices/use-devices.js'
8
+ import { BatteryStatus } from 'lib/ui/device/BatteryStatus.js'
9
+ import { DeviceImage } from 'lib/ui/device/DeviceImage.js'
10
+ import { LockStatus } from 'lib/ui/device/LockStatus.js'
11
+ import { OnlineStatus } from 'lib/ui/device/OnlineStatus.js'
12
+ import { getDeviceModel } from 'lib/ui/DeviceDetails/DeviceModel.js'
13
+ import { ContentHeader } from 'lib/ui/layout/ContentHeader.js'
14
+ import { TableBody } from 'lib/ui/Table/TableBody.js'
15
+ import { TableCell } from 'lib/ui/Table/TableCell.js'
16
+ import { TableHeader } from 'lib/ui/Table/TableHeader.js'
17
+ import { TableRow } from 'lib/ui/Table/TableRow.js'
18
+ import { TableTitle } from 'lib/ui/Table/TableTitle.js'
19
+ import { Caption } from 'lib/ui/typography/Caption.js'
20
+ import { Title } from 'lib/ui/typography/Title.js'
21
+
22
+ export type DeviceTableProps = Props & UseDevicesParams
23
+
24
+ interface Props {
25
+ onBack?: () => void
26
+ }
27
+
28
+ export function DeviceTable({ onBack, ...props }: DeviceTableProps) {
29
+ const { devices, isLoading, isError, error } = useDevices(props)
30
+ const { show } = useNavigation()
31
+
32
+ if (isLoading) return <p>...</p>
33
+ if (isError) return <p>{error?.message}</p>
34
+ if (devices == null) return null
35
+
36
+ const deviceCount = devices.length
37
+
38
+ return (
39
+ <div className='seam-device-table'>
40
+ <ContentHeader />
41
+ <TableHeader>
42
+ <TableTitle>
43
+ {t.devices} <Caption>({deviceCount})</Caption>
44
+ </TableTitle>
45
+ </TableHeader>
46
+ <TableBody>
47
+ {devices.map((device) => (
48
+ <DeviceRow
49
+ device={device}
50
+ key={device.device_id}
51
+ onClick={() => {
52
+ show({ name: 'device_detail', deviceId: device.device_id })
53
+ }}
54
+ />
55
+ ))}
56
+ </TableBody>
57
+ </div>
58
+ )
59
+ }
60
+
61
+ function DeviceRow(props: {
62
+ device: UseDevicesData[number]
63
+ onClick: () => void
64
+ }) {
65
+ const { device, onClick } = props
66
+
67
+ if (!isLockDevice(device)) return null
68
+
69
+ const deviceModel = getDeviceModel(device) ?? t.unknownLock
70
+
71
+ return (
72
+ <TableRow key={device.device_id} onClick={onClick}>
73
+ <TableCell className='seam-image-cell'>
74
+ <DeviceImage device={device} />
75
+ </TableCell>
76
+ <TableCell className='seam-body-cell'>
77
+ <Title>{device.properties.name}</Title>
78
+ <div className='seam-bottom'>
79
+ <span className='seam-device-model'>{deviceModel}</span>
80
+ <div className='seam-device-statuses'>
81
+ <OnlineStatus device={device} />
82
+ <BatteryStatus device={device} />
83
+ <LockStatus device={device} />
84
+ </div>
85
+ </div>
86
+ </TableCell>
87
+ </TableRow>
88
+ )
89
+ }
90
+
91
+ const t = {
92
+ devices: 'Devices',
93
+ unknownLock: 'Unknown Lock',
94
+ }
@@ -1,5 +1,5 @@
1
1
  import type { ButtonProps } from 'lib/ui/types.js'
2
2
 
3
3
  export function IconButton(props: ButtonProps): JSX.Element {
4
- return <button {...props} className='seam-button seam-icon-button' />
4
+ return <button {...props} className='seam-icon-btn' />
5
5
  }
@@ -1,5 +1,11 @@
1
1
  import type { DivProps } from 'lib/ui/types.js'
2
2
 
3
- export function TableRow(props: DivProps): JSX.Element {
4
- return <div className='seam-table-row'>{props.children}</div>
3
+ export function TableRow(
4
+ props: DivProps & { onClick?: () => void }
5
+ ): JSX.Element {
6
+ return (
7
+ <div className='seam-table-row' {...props}>
8
+ {props.children}
9
+ </div>
10
+ )
5
11
  }
@@ -0,0 +1,15 @@
1
+ import type { ButtonProps } from 'lib/ui/types.js'
2
+
3
+ interface TextButtonProps extends ButtonProps {
4
+ color?: 'primary' | 'inherit'
5
+ disabled?: boolean
6
+ }
7
+
8
+ export function TextButton({
9
+ color = 'primary',
10
+ ...buttonProps
11
+ }: TextButtonProps): JSX.Element {
12
+ return (
13
+ <button className={`seam-text-btn seam-color-${color}`} {...buttonProps} />
14
+ )
15
+ }
@@ -1,6 +1,6 @@
1
1
  import classNames from 'classnames'
2
2
 
3
- import { TextField, type TextFieldProps } from 'lib/ui/TextField/index.js'
3
+ import { TextField, type TextFieldProps } from 'lib/ui/TextField/TextField.js'
4
4
 
5
5
  export function SearchTextField({
6
6
  className,
@@ -5,32 +5,31 @@ import { BatteryLevelFullIcon } from 'lib/icons/BatteryLevelFull.js'
5
5
  import { BatteryLevelHighIcon } from 'lib/icons/BatteryLevelHigh.js'
6
6
  import { BatteryLevelLowIcon } from 'lib/icons/BatteryLevelLow.js'
7
7
 
8
- export function BatteryStatus(props: {
8
+ export interface BatteryStatusProps {
9
9
  device: LockDevice
10
- }): JSX.Element | null {
10
+ }
11
+
12
+ export function BatteryStatus(props: BatteryStatusProps) {
11
13
  const {
12
14
  device: {
13
15
  properties: { battery_level: batteryLevel },
14
16
  },
15
17
  } = props
16
18
 
17
- if (batteryLevel == null) {
18
- return null
19
- }
20
-
21
19
  return (
22
- <>
23
- <span className='seam-label'>{t.power}:</span>{' '}
24
- <div className='seam-battery-status'>
25
- <Status batteryLevel={batteryLevel} />
26
- </div>
27
- </>
20
+ <div className='seam-battery-status'>
21
+ <Content batteryLevel={batteryLevel} />
22
+ </div>
28
23
  )
29
24
  }
30
25
 
31
- function Status(props: { batteryLevel: number }): JSX.Element {
26
+ function Content(props: { batteryLevel?: number }) {
32
27
  const { batteryLevel } = props
33
28
 
29
+ if (batteryLevel == null) {
30
+ return null
31
+ }
32
+
34
33
  if (batteryLevel > batteryThreshold.full) {
35
34
  return (
36
35
  <>
@@ -73,7 +72,6 @@ const batteryThreshold = {
73
72
  }
74
73
 
75
74
  const t = {
76
- power: 'Power',
77
75
  full: 'Good',
78
76
  high: 'Good',
79
77
  low: 'Low',
@@ -0,0 +1,15 @@
1
+ import type { HTMLAttributes } from 'react'
2
+ import type { LockDevice } from 'seamapi'
3
+
4
+ export function DeviceImage(
5
+ props: { device: LockDevice } & HTMLAttributes<HTMLImageElement>
6
+ ): JSX.Element {
7
+ const { device, ...imageProps } = props
8
+
9
+ const url = device.properties.image_url ?? fallbackImageUrl
10
+
11
+ return <img src={url} alt={device.properties.name} {...imageProps} />
12
+ }
13
+
14
+ const fallbackImageUrl =
15
+ 'https://connect.getseam.com/assets/images/devices/unknown-lock.png'
@@ -0,0 +1,46 @@
1
+ import type { LockDevice } from 'seamapi'
2
+
3
+ import { LockLockedIcon } from 'lib/icons/LockLocked.js'
4
+ import { LockUnlockedIcon } from 'lib/icons/LockUnlocked.js'
5
+
6
+ export interface LockStatusProps {
7
+ device: LockDevice
8
+ }
9
+
10
+ export function LockStatus(props: LockStatusProps): JSX.Element {
11
+ const {
12
+ device: {
13
+ properties: { locked },
14
+ },
15
+ } = props
16
+
17
+ return (
18
+ <div className='seam-lock-status'>
19
+ <Content isLocked={locked} />
20
+ </div>
21
+ )
22
+ }
23
+
24
+ function Content(props: { isLocked: boolean }) {
25
+ const { isLocked } = props
26
+ if (isLocked) {
27
+ return (
28
+ <>
29
+ <LockLockedIcon className='seam-lock-icon' />
30
+ <span className='seam-status-text'>{t.locked}</span>
31
+ </>
32
+ )
33
+ }
34
+
35
+ return (
36
+ <>
37
+ <LockUnlockedIcon className='seam-lock-icon' />
38
+ <span className='seam-status-text'>{t.unlocked}</span>
39
+ </>
40
+ )
41
+ }
42
+
43
+ const t = {
44
+ locked: 'Locked',
45
+ unlocked: 'Unlocked',
46
+ }
@@ -3,21 +3,26 @@ import type { LockDevice } from 'seamapi'
3
3
  import { OnlineStatusDeviceOfflineIcon } from 'lib/icons/OnlineStatusDeviceOffline.js'
4
4
  import { OnlineStatusOnlineIcon } from 'lib/icons/OnlineStatusOnline.js'
5
5
 
6
- export function OnlineStatus(props: { device: LockDevice }): JSX.Element {
7
- const { device } = props
6
+ export interface OnlineStatusProps {
7
+ device: LockDevice
8
+ }
9
+
10
+ export function OnlineStatus(props: OnlineStatusProps): JSX.Element {
11
+ const {
12
+ device: {
13
+ properties: { online },
14
+ },
15
+ } = props
16
+
8
17
  return (
9
- <>
10
- <span className='seam-label'>{t.status}:</span>{' '}
11
- <div className='seam-online-status'>
12
- <Status isOnline={device.properties.online} />
13
- </div>
14
- </>
18
+ <div className='seam-online-status'>
19
+ <Content isOnline={online} />
20
+ </div>
15
21
  )
16
22
  }
17
23
 
18
- function Status(props: { isOnline: boolean }): JSX.Element {
24
+ function Content(props: { isOnline: boolean }) {
19
25
  const { isOnline } = props
20
-
21
26
  if (isOnline) {
22
27
  return (
23
28
  <>
@@ -38,7 +43,6 @@ function Status(props: { isOnline: boolean }): JSX.Element {
38
43
  }
39
44
 
40
45
  const t = {
41
- status: 'Status',
42
46
  online: 'Online',
43
47
  deviceOffline: 'Device Offline',
44
48
  }