Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add snackbars for toggling lock status #678

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 58 additions & 24 deletions src/lib/seam/components/AccessCodeDetails/AccessCodeDevice.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { useState } from 'react'

import { useDevice } from 'lib/seam/devices/use-device.js'
import { isLockDevice, type LockDevice } from 'lib/seam/locks/lock-device.js'
import { useToggleLock } from 'lib/seam/locks/use-toggle-lock.js'
import { Button } from 'lib/ui/Button.js'
import { DeviceImage } from 'lib/ui/device/DeviceImage.js'
import { Snackbar, type SnackbarVariant } from 'lib/ui/Snackbar/Snackbar.js'
import { TextButton } from 'lib/ui/TextButton.js'

export function AccessCodeDevice({
Expand Down Expand Up @@ -49,40 +52,71 @@ function Content(props: {
onSelectDevice: (deviceId: string) => void
}): JSX.Element {
const { device, disableLockUnlock, onSelectDevice } = props
const toggleLock = useToggleLock()
const [snackbarVisible, setSnackbarVisible] = useState(false)
const [snackbarVariant, setSnackbarVariant] =
useState<SnackbarVariant>('success')

const toggleLock = useToggleLock({
onSuccess: () => {
setSnackbarVisible(true)
setSnackbarVariant('success')
},
onError: () => {
setSnackbarVisible(true)
setSnackbarVariant('error')
},
})

const toggleLockLabel = device.properties.locked ? t.unlock : t.lock

return (
<div className='seam-access-code-device'>
<div className='seam-device-image'>
<DeviceImage device={device} />
</div>
<div className='seam-body'>
<div>{device.properties.name}</div>
<TextButton
onClick={() => {
onSelectDevice(device.device_id)
}}
>
{t.deviceDetails}
</TextButton>
<>
<Snackbar
variant={snackbarVariant}
visible={snackbarVisible}
onClose={() => {
setSnackbarVisible(false)
}}
message={
snackbarVariant === 'success'
? t.successfullyUpdated
: t.failedToUpdate
}
autoDismiss
/>

<div className='seam-access-code-device'>
<div className='seam-device-image'>
<DeviceImage device={device} />
</div>
<div className='seam-body'>
<div>{device.properties.name}</div>
<TextButton
onClick={() => {
onSelectDevice(device.device_id)
}}
>
{t.deviceDetails}
</TextButton>
</div>
{!disableLockUnlock && device.properties.online && (
<Button
onClick={() => {
toggleLock.mutate(device)
}}
>
{toggleLockLabel}
</Button>
)}
</div>
{!disableLockUnlock && device.properties.online && (
<Button
onClick={() => {
toggleLock.mutate(device)
}}
>
{toggleLockLabel}
</Button>
)}
</div>
</>
)
}

const t = {
deviceDetails: 'Device details',
unlock: 'Unlock',
lock: 'Lock',
successfullyUpdated: 'Lock status has been successfully updated',
failedToUpdate: 'Failed to update lock status',
}
176 changes: 105 additions & 71 deletions src/lib/seam/components/DeviceDetails/LockDeviceDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import classNames from 'classnames'
import { useState } from 'react'

import { ChevronRightIcon } from 'lib/icons/ChevronRight.js'
import { useAccessCodes } from 'lib/seam/access-codes/use-access-codes.js'
Expand All @@ -16,6 +17,7 @@ import { DeviceImage } from 'lib/ui/device/DeviceImage.js'
import { EditableDeviceName } from 'lib/ui/device/EditableDeviceName.js'
import { OnlineStatus } from 'lib/ui/device/OnlineStatus.js'
import { ContentHeader } from 'lib/ui/layout/ContentHeader.js'
import { Snackbar, type SnackbarVariant } from 'lib/ui/Snackbar/Snackbar.js'
import { useToggle } from 'lib/ui/use-toggle.js'

interface LockDeviceDetailsProps extends NestedSpecificDeviceDetailsProps {
Expand All @@ -38,11 +40,25 @@ export function LockDeviceDetails({
onEditName,
}: LockDeviceDetailsProps): JSX.Element | null {
const [accessCodesOpen, toggleAccessCodesOpen] = useToggle()
const toggleLock = useToggleLock()
const { accessCodes } = useAccessCodes({
device_id: device.device_id,
})

const [snackbarVisible, setSnackbarVisible] = useState(false)
const [snackbarVariant, setSnackbarVariant] =
useState<SnackbarVariant>('success')

const toggleLock = useToggleLock({
onSuccess: () => {
setSnackbarVisible(true)
setSnackbarVariant('success')
},
onError: () => {
setSnackbarVisible(true)
setSnackbarVariant('error')
},
})

const lockStatus = device.properties.locked ? t.locked : t.unlocked
const toggleLockLabel = device.properties.locked ? t.unlock : t.lock

Expand Down Expand Up @@ -88,87 +104,103 @@ export function LockDeviceDetails({
]

return (
<div className={classNames('seam-device-details', className)}>
<ContentHeader title='Device' onBack={onBack} />
<div className='seam-body'>
<div className='seam-summary'>
<div className='seam-content'>
<div className='seam-image'>
<DeviceImage device={device} />
</div>
<div className='seam-info'>
<span className='seam-label'>{t.device}</span>
<EditableDeviceName
tagName='h4'
value={device.properties.name}
className='seam-device-name'
onEdit={onEditName}
/>
<div className='seam-properties'>
<span className='seam-label'>{t.status}:</span>{' '}
<OnlineStatus device={device} />
{device.properties.online && (
<>
<span className='seam-label'>{t.power}:</span>{' '}
<BatteryStatusIndicator device={device} />
</>
)}
<DeviceModel device={device} />
<>
<Snackbar
variant={snackbarVariant}
visible={snackbarVisible}
onClose={() => {
setSnackbarVisible(false)
}}
message={
snackbarVariant === 'success'
? t.successfullyUpdated
: t.failedToUpdate
}
autoDismiss
/>

<div className={classNames('seam-device-details', className)}>
<ContentHeader title='Device' onBack={onBack} />
<div className='seam-body'>
<div className='seam-summary'>
<div className='seam-content'>
<div className='seam-image'>
<DeviceImage device={device} />
</div>
<div className='seam-info'>
<span className='seam-label'>{t.device}</span>
<EditableDeviceName
tagName='h4'
value={device.properties.name}
className='seam-device-name'
onEdit={onEditName}
/>
<div className='seam-properties'>
<span className='seam-label'>{t.status}:</span>{' '}
<OnlineStatus device={device} />
{device.properties.online && (
<>
<span className='seam-label'>{t.power}:</span>{' '}
<BatteryStatusIndicator device={device} />
</>
)}
<DeviceModel device={device} />
</div>
</div>
</div>
<Alerts alerts={alerts} className='seam-alerts-space-top' />
</div>
<Alerts alerts={alerts} className='seam-alerts-space-top' />
</div>
<div className='seam-box'>
<div
className='seam-content seam-access-codes'
onClick={toggleAccessCodesOpen}
>
<span className='seam-value'>
{accessCodeCount} {t.accessCodes}
</span>
<ChevronRightIcon />
<div className='seam-box'>
<div
className='seam-content seam-access-codes'
onClick={toggleAccessCodesOpen}
>
<span className='seam-value'>
{accessCodeCount} {t.accessCodes}
</span>
<ChevronRightIcon />
</div>
</div>
</div>

<div className='seam-box'>
{device.properties.locked && device.properties.online && (
<div className='seam-content seam-lock-status'>
<div>
<span className='seam-label'>{t.lockStatus}</span>
<span className='seam-value'>{lockStatus}</span>
</div>
<div className='seam-right'>
{!disableLockUnlock &&
device.capabilities_supported.includes('lock') && (
<Button
size='small'
onClick={() => {
toggleLock.mutate(device)
}}
>
{toggleLockLabel}
</Button>
)}
<div className='seam-box'>
{device.properties.locked && device.properties.online && (
<div className='seam-content seam-lock-status'>
<div>
<span className='seam-label'>{t.lockStatus}</span>
<span className='seam-value'>{lockStatus}</span>
</div>
<div className='seam-right'>
{!disableLockUnlock &&
device.capabilities_supported.includes('lock') && (
<Button
size='small'
onClick={() => {
toggleLock.mutate(device)
}}
>
{toggleLockLabel}
</Button>
)}
</div>
</div>
</div>
)}
)}

<AccessCodeLength
supportedCodeLengths={
device.properties?.supported_code_lengths ?? []
<AccessCodeLength
supportedCodeLengths={
device.properties?.supported_code_lengths ?? []
}
/>
</div>
<DeviceInfo
device={device}
disableConnectedAccountInformation={
disableConnectedAccountInformation
}
disableResourceIds={disableResourceIds}
/>
</div>
<DeviceInfo
device={device}
disableConnectedAccountInformation={
disableConnectedAccountInformation
}
disableResourceIds={disableResourceIds}
/>
</div>
</div>
</>
)
}

Expand Down Expand Up @@ -208,4 +240,6 @@ const t = {
lockStatus: 'Lock status',
status: 'Status',
power: 'Power',
successfullyUpdated: 'Lock status has been successfully updated',
failedToUpdate: 'Failed to update lock status',
}
16 changes: 13 additions & 3 deletions src/lib/seam/locks/use-toggle-lock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ import {

import { NullSeamClientError, useSeamClient } from 'lib/seam/use-seam-client.js'

export type UseToggleLockParams = never

export type UseToggleLockData = undefined

export type UseToggleLockMutationVariables = Pick<Device, 'device_id'> & {
Expand All @@ -29,7 +27,14 @@ type MutationError =
| SeamActionAttemptFailedError<ToggleLockActionAttempt>
| SeamActionAttemptTimeoutError<ToggleLockActionAttempt>

export function useToggleLock(): UseMutationResult<
interface UseToggleLockParams {
onError?: () => void
onSuccess?: () => void
}

export function useToggleLock(
params: UseToggleLockParams = {}
): UseMutationResult<
UseToggleLockData,
MutationError,
UseToggleLockMutationVariables
Expand Down Expand Up @@ -92,12 +97,17 @@ export function useToggleLock(): UseMutationResult<
)
},
onError: async (_error, variables) => {
params.onError?.()

await queryClient.invalidateQueries({
queryKey: ['devices', 'list'],
})
await queryClient.invalidateQueries({
queryKey: ['devices', 'get', { device_id: variables.device_id }],
})
},
onSuccess() {
params.onSuccess?.()
},
})
}
4 changes: 2 additions & 2 deletions src/lib/ui/Snackbar/Snackbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { CheckGreenIcon } from 'lib/icons/CheckGreen.js'
import { CloseWhiteIcon } from 'lib/icons/CloseWhite.js'
import { ExclamationCircleIcon } from 'lib/icons/ExclamationCircle.js'

type SnackbarVariant = 'success' | 'error'
export type SnackbarVariant = 'success' | 'error'

interface SnackbarProps {
export interface SnackbarProps {
message: string
variant: SnackbarVariant
visible: boolean
Expand Down
Loading