33import { Card , CardContent , CardHeader , CardTitle } from '@/components/ui/card'
44import { Badge } from '@/components/ui/badge'
55import { Button } from '@/components/ui/button'
6- import { Smartphone , Battery , Signal , Copy , Plus , ExternalLink } from 'lucide-react'
6+ import {
7+ Smartphone ,
8+ Battery ,
9+ Signal ,
10+ Copy ,
11+ Plus ,
12+ ExternalLink ,
13+ Loader2 ,
14+ MoreVertical ,
15+ } from 'lucide-react'
716import { useToast } from '@/hooks/use-toast'
817import httpBrowserClient from '@/lib/httpBrowserClient'
918import { ApiEndpoints } from '@/config/api'
1019import { Routes } from '@/config/routes'
11- import { useQuery } from '@tanstack/react-query'
20+ import { useMutation , useQuery , useQueryClient } from '@tanstack/react-query'
1221import { useRef , useState } from 'react'
1322import {
1423 Dialog ,
@@ -19,6 +28,12 @@ import {
1928 DialogTitle ,
2029} from '@/components/ui/dialog'
2130import { Skeleton } from '@/components/ui/skeleton'
31+ import {
32+ DropdownMenu ,
33+ DropdownMenuContent ,
34+ DropdownMenuItem ,
35+ DropdownMenuTrigger ,
36+ } from '@/components/ui/dropdown-menu'
2237import { formatDeviceName } from '@/lib/utils'
2338import GenerateApiKey , {
2439 type GenerateApiKeyHandle ,
@@ -30,11 +45,20 @@ import {
3045 latestAppVersionCode ,
3146} from './update-app-helpers'
3247
48+ type DeviceRow = DeviceVersionCandidate & {
49+ createdAt : string
50+ status ?: string
51+ enabled ?: boolean
52+ }
53+
3354export default function DeviceList ( ) {
3455 const addDeviceKeyRef = useRef < GenerateApiKeyHandle > ( null )
3556 const [ addDeviceInstructionOpen , setAddDeviceInstructionOpen ] =
3657 useState ( false )
58+ const [ devicePendingDelete , setDevicePendingDelete ] =
59+ useState < DeviceRow | null > ( null )
3760 const { toast } = useToast ( )
61+ const queryClient = useQueryClient ( )
3862 const {
3963 isPending,
4064 error,
@@ -48,6 +72,35 @@ export default function DeviceList() {
4872 // select: (res) => res.data,
4973 } )
5074
75+ const {
76+ mutate : deleteDevice ,
77+ isPending : isDeletingDevice ,
78+ } = useMutation ( {
79+ mutationFn : ( id : string ) =>
80+ httpBrowserClient . delete ( ApiEndpoints . gateway . deleteDevice ( id ) ) ,
81+ onSuccess : ( ) => {
82+ setDevicePendingDelete ( null )
83+ toast ( {
84+ title : 'Device removed' ,
85+ } )
86+ void queryClient . invalidateQueries ( { queryKey : [ 'devices' ] } )
87+ } ,
88+ onError : ( err : unknown ) => {
89+ const message =
90+ err &&
91+ typeof err === 'object' &&
92+ 'message' in err &&
93+ typeof ( err as { message : unknown } ) . message === 'string'
94+ ? ( err as { message : string } ) . message
95+ : 'Something went wrong'
96+ toast ( {
97+ variant : 'destructive' ,
98+ title : 'Error removing device' ,
99+ description : message ,
100+ } )
101+ } ,
102+ } )
103+
51104 const handleCopyId = ( id : string ) => {
52105 navigator . clipboard . writeText ( id )
53106 toast ( {
@@ -77,8 +130,8 @@ export default function DeviceList() {
77130 { [ 1 , 2 , 3 ] . map ( ( i ) => (
78131 < Card key = { i } className = 'border-0 shadow-none' >
79132 < CardContent className = 'flex items-center p-3' >
80- < Skeleton className = 'h-6 w-6 rounded-full mr-3' />
81- < div className = 'flex-1' >
133+ < Skeleton className = 'h-6 w-6 rounded-full mr-3 shrink-0 ' />
134+ < div className = 'min-w-0 flex-1' >
82135 < div className = 'flex items-center justify-between' >
83136 < Skeleton className = 'h-4 w-[120px]' />
84137 < Skeleton className = 'h-4 w-[60px]' />
@@ -90,6 +143,7 @@ export default function DeviceList() {
90143 < Skeleton className = 'h-3 w-[200px]' />
91144 </ div >
92145 </ div >
146+ < Skeleton className = 'h-6 w-6 shrink-0' />
93147 </ CardContent >
94148 </ Card >
95149 ) ) }
@@ -110,9 +164,9 @@ export default function DeviceList() {
110164
111165 { devices ?. data ?. map ( ( device ) => (
112166 < Card key = { device . _id } className = 'border-0 shadow-none' >
113- < CardContent className = 'flex items-center p-3' >
114- < Smartphone className = 'h-6 w-6 mr-3 ' />
115- < div className = 'flex-1' >
167+ < CardContent className = 'flex items-center gap-1 p-3' >
168+ < Smartphone className = 'h-6 w-6 mr-2 shrink-0 ' />
169+ < div className = 'min-w-0 flex-1' >
116170 < div className = 'flex items-center justify-between' >
117171 < h3 className = 'font-semibold text-sm' >
118172 { formatDeviceName ( device ) }
@@ -196,6 +250,28 @@ export default function DeviceList() {
196250 </ div >
197251 ) }
198252 </ div >
253+ < DropdownMenu >
254+ < DropdownMenuTrigger asChild >
255+ < Button
256+ variant = 'ghost'
257+ size = 'icon'
258+ className = 'h-8 w-8 shrink-0'
259+ aria-label = 'Device actions'
260+ >
261+ < MoreVertical className = 'h-4 w-4' />
262+ </ Button >
263+ </ DropdownMenuTrigger >
264+ < DropdownMenuContent align = 'end' >
265+ < DropdownMenuItem
266+ className = 'text-destructive focus:text-destructive'
267+ onClick = { ( ) =>
268+ setDevicePendingDelete ( device as DeviceRow )
269+ }
270+ >
271+ Delete
272+ </ DropdownMenuItem >
273+ </ DropdownMenuContent >
274+ </ DropdownMenu >
199275 </ CardContent >
200276 </ Card >
201277 ) ) }
@@ -266,6 +342,46 @@ export default function DeviceList() {
266342 </ DialogFooter >
267343 </ DialogContent >
268344 </ Dialog >
345+
346+ < Dialog
347+ open = { ! ! devicePendingDelete }
348+ onOpenChange = { ( open ) => {
349+ if ( ! open ) setDevicePendingDelete ( null )
350+ } }
351+ >
352+ < DialogContent >
353+ < DialogHeader >
354+ < DialogTitle > Remove this device?</ DialogTitle >
355+ < DialogDescription >
356+ { devicePendingDelete
357+ ? `This removes ${ formatDeviceName ( devicePendingDelete ) } from your account. You will not be able to send or receive SMS through it until you register the app again.`
358+ : 'This removes the device from your account. You will not be able to send or receive SMS through it until you register the app again.' }
359+ </ DialogDescription >
360+ </ DialogHeader >
361+ < DialogFooter >
362+ < Button
363+ variant = 'outline'
364+ onClick = { ( ) => setDevicePendingDelete ( null ) }
365+ disabled = { isDeletingDevice }
366+ >
367+ Cancel
368+ </ Button >
369+ < Button
370+ variant = 'destructive'
371+ onClick = { ( ) =>
372+ devicePendingDelete &&
373+ deleteDevice ( devicePendingDelete . _id )
374+ }
375+ disabled = { isDeletingDevice }
376+ >
377+ { isDeletingDevice ? (
378+ < Loader2 className = 'mr-2 h-4 w-4 animate-spin' />
379+ ) : null }
380+ Remove
381+ </ Button >
382+ </ DialogFooter >
383+ </ DialogContent >
384+ </ Dialog >
269385 </ >
270386 )
271387}
0 commit comments