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 3 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
62 changes: 38 additions & 24 deletions src/lib/seam/components/AccessCodeDetails/AccessCodeDevice.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
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 { useToggleLockSnackbar } from 'lib/seam/locks/use-toggle-lock-snackbar.js'
import { Button } from 'lib/ui/Button.js'
import { DeviceImage } from 'lib/ui/device/DeviceImage.js'
import { TextButton } from 'lib/ui/TextButton.js'
Expand Down Expand Up @@ -49,35 +50,48 @@ function Content(props: {
onSelectDevice: (deviceId: string) => void
}): JSX.Element {
const { device, disableLockUnlock, onSelectDevice } = props
const toggleLock = useToggleLock()

const { SnackbarNode, showToggleSnackbar } = useToggleLockSnackbar()
const toggleLock = useToggleLock({
onSuccess: () => {
showToggleSnackbar('success')
},
onError: () => {
showToggleSnackbar('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>
<>
{SnackbarNode}

<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>
</>
)
}

Expand Down
156 changes: 85 additions & 71 deletions src/lib/seam/components/DeviceDetails/LockDeviceDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { DeviceModel } from 'lib/seam/components/DeviceDetails/DeviceModel.js'
import { deviceErrorFilter, deviceWarningFilter } from 'lib/seam/filters.js'
import type { LockDevice } from 'lib/seam/locks/lock-device.js'
import { useToggleLock } from 'lib/seam/locks/use-toggle-lock.js'
import { useToggleLockSnackbar } from 'lib/seam/locks/use-toggle-lock-snackbar.js'
import { Alerts } from 'lib/ui/Alert/Alerts.js'
import { Button } from 'lib/ui/Button.js'
import { BatteryStatusIndicator } from 'lib/ui/device/BatteryStatusIndicator.js'
Expand Down Expand Up @@ -38,11 +39,20 @@ export function LockDeviceDetails({
onEditName,
}: LockDeviceDetailsProps): JSX.Element | null {
const [accessCodesOpen, toggleAccessCodesOpen] = useToggle()
const toggleLock = useToggleLock()
const { accessCodes } = useAccessCodes({
device_id: device.device_id,
})

const { SnackbarNode, showToggleSnackbar } = useToggleLockSnackbar()
const toggleLock = useToggleLock({
onSuccess: () => {
showToggleSnackbar('success')
},
onError: () => {
showToggleSnackbar('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 +98,91 @@ 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} />
<>
{SnackbarNode}

<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
43 changes: 43 additions & 0 deletions src/lib/seam/locks/use-toggle-lock-snackbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { useCallback, useMemo, useState } from 'react'

import { Snackbar, type SnackbarVariant } from 'lib/ui/Snackbar/Snackbar.js'

export interface UseToggleLockSnackbarContext {
showToggleSnackbar: (variant: SnackbarVariant) => void
SnackbarNode: JSX.Element
}

export function useToggleLockSnackbar(): UseToggleLockSnackbarContext {
const [visible, setVisible] = useState(false)
const [variant, setVariant] = useState<SnackbarVariant>('success')

const SnackbarNode = useMemo(
() => (
<Snackbar
variant={variant}
visible={visible}
onClose={() => {
setVisible(false)
}}
message={
variant === 'success' ? t.successfullyUpdated : t.failedToUpdate
kadiryazici marked this conversation as resolved.
Show resolved Hide resolved
}
autoDismiss
/>
),
[visible, variant]
)

return {
showToggleSnackbar: useCallback((variant) => {
setVariant(variant)
setVisible(true)
}, []),
SnackbarNode,
kadiryazici marked this conversation as resolved.
Show resolved Hide resolved
}
}

const t = {
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