@@ -3,103 +3,110 @@ import type { databases } from '~/drizzle'
33import { SafeURL } from '@conar/shared/utils/safe-url'
44import { Badge } from '@conar/ui/components/badge'
55import { Button } from '@conar/ui/components/button'
6+ import { ButtonGroup } from '@conar/ui/components/button-group'
67import { DropdownMenu , DropdownMenuContent , DropdownMenuItem , DropdownMenuTrigger } from '@conar/ui/components/dropdown-menu'
7- import { Skeleton } from '@conar/ui/components/skeleton'
88import { copy } from '@conar/ui/lib/copy'
99import { cn } from '@conar/ui/lib/utils'
1010import { RiDeleteBinLine , RiEditLine , RiFileCopyLine , RiMoreLine } from '@remixicon/react'
1111import { useLiveQuery } from '@tanstack/react-db'
1212import { Link } from '@tanstack/react-router'
13- import { useMemo , useRef } from 'react'
13+ import { AnimatePresence , motion } from 'motion/react'
14+ import { useRef , useState } from 'react'
1415import { DatabaseIcon , databasesCollection , useDatabaseLinkParams } from '~/entities/database'
1516import { RemoveConnectionDialog } from './remove-connection-dialog'
1617import { RenameConnectionDialog } from './rename-connection-dialog'
1718
1819function DatabaseCard ( { database, onRemove, onRename } : { database : typeof databases . $inferSelect , onRemove : ( ) => void , onRename : ( ) => void } ) {
19- const connectionString = useMemo ( ( ) => {
20- const url = new SafeURL ( database . connectionString )
20+ const url = new SafeURL ( database . connectionString )
2121
22- if ( database . isPasswordExists || url . password ) {
23- url . password = '*' . repeat ( url . password . length || 6 )
24- }
22+ if ( database . isPasswordExists || url . password ) {
23+ url . password = '*' . repeat ( url . password . length || 6 )
24+ }
2525
26- return url . toString ( )
27- } , [ database . connectionString , database . isPasswordExists ] )
26+ const connectionString = url . toString ( )
2827
2928 const params = useDatabaseLinkParams ( database . id )
3029
3130 return (
32- < Link
33- className = { cn (
34- 'relative flex items-center justify-between gap-4' ,
35- 'rounded-lg p-5 bg-muted/30 border overflow-hidden border-border/50 border-l-4 group' ,
36- database . color
37- ? 'hover:border-(--color)/60 border-l-(--color)/60'
38- : 'hover:border-primary/60' ,
39- ) }
40- style = { database . color ? { '--color' : database . color } : { } }
41- { ...params }
31+ < motion . div
32+ layout
33+ initial = { { opacity : 0 , scale : 0.75 } }
34+ animate = { { opacity : 1 , scale : 1 } }
35+ exit = { { opacity : 0 , scale : 0.75 } }
36+ transition = { { duration : 0.15 } }
4237 >
43- < div
38+ < Link
4439 className = { cn (
45- 'size-12 shrink-0 rounded-lg p-3' ,
46- 'bg-muted/70' ,
40+ 'relative flex items-center justify-between gap-4' ,
41+ 'rounded-lg p-5 bg-muted/30 border overflow-hidden border-border/50 border-l-4 group' ,
42+ database . color
43+ ? 'hover:border-(--color)/60 border-l-(--color)/60'
44+ : 'hover:border-primary/60' ,
4745 ) }
46+ style = { database . color ? { '--color' : database . color } : { } }
47+ { ...params }
4848 >
49- < DatabaseIcon
50- type = { database . type }
51- className = "size-full"
52- />
53- </ div >
54- < div className = "flex flex-1 flex-col min-w-0" >
55- < div className = "font-medium tracking-tight truncate flex items-center gap-2" >
56- < span className = { database . color ? 'text-(--color) group-hover:text-(--color)/80' : '' } >
57- { database . name }
58- </ span >
59- { database . label && (
60- < Badge variant = "secondary" >
61- { database . label }
62- </ Badge >
49+ < div
50+ className = { cn (
51+ 'size-12 shrink-0 rounded-lg p-3' ,
52+ 'bg-muted/70' ,
6353 ) }
54+ >
55+ < DatabaseIcon
56+ type = { database . type }
57+ className = "size-full"
58+ />
6459 </ div >
65- < div data-mask className = "text-xs text-muted-foreground font-mono truncate" > { connectionString . replaceAll ( '*' , '•' ) } </ div >
66- </ div >
67- < DropdownMenu >
68- < DropdownMenuTrigger className = "rounded-md p-2 hover:bg-accent-foreground/5" >
69- < RiMoreLine className = "size-4" />
70- </ DropdownMenuTrigger >
71- < DropdownMenuContent align = "end" >
72- < DropdownMenuItem
73- onClick = { ( e ) => {
74- e . stopPropagation ( )
75- copy ( database . connectionString , 'Connection string copied to clipboard' )
76- } }
77- >
78- < RiFileCopyLine className = "size-4 opacity-50" />
79- Copy Connection String
80- </ DropdownMenuItem >
81- < DropdownMenuItem
82- onClick = { ( e ) => {
83- e . stopPropagation ( )
84- onRename ( )
85- } }
86- >
87- < RiEditLine className = "size-4 opacity-50" />
88- Rename
89- </ DropdownMenuItem >
90- < DropdownMenuItem
91- className = "text-destructive focus:text-destructive"
92- onClick = { ( e ) => {
93- e . stopPropagation ( )
94- onRemove ( )
95- } }
96- >
97- < RiDeleteBinLine className = "size-4" />
98- Remove
99- </ DropdownMenuItem >
100- </ DropdownMenuContent >
101- </ DropdownMenu >
102- </ Link >
60+ < div className = "flex flex-1 flex-col min-w-0" >
61+ < div className = "font-medium tracking-tight truncate flex items-center gap-2" >
62+ < span className = { database . color ? 'text-(--color) group-hover:text-(--color)/80' : '' } >
63+ { database . name }
64+ </ span >
65+ { database . label && (
66+ < Badge variant = "secondary" >
67+ { database . label }
68+ </ Badge >
69+ ) }
70+ </ div >
71+ < div data-mask className = "text-xs text-muted-foreground font-mono truncate" > { connectionString . replaceAll ( '*' , '•' ) } </ div >
72+ </ div >
73+ < DropdownMenu >
74+ < DropdownMenuTrigger className = "rounded-md p-2 hover:bg-accent-foreground/5" >
75+ < RiMoreLine className = "size-4" />
76+ </ DropdownMenuTrigger >
77+ < DropdownMenuContent align = "end" >
78+ < DropdownMenuItem
79+ onClick = { ( e ) => {
80+ e . stopPropagation ( )
81+ copy ( database . connectionString , 'Connection string copied to clipboard' )
82+ } }
83+ >
84+ < RiFileCopyLine className = "size-4 opacity-50" />
85+ Copy Connection String
86+ </ DropdownMenuItem >
87+ < DropdownMenuItem
88+ onClick = { ( e ) => {
89+ e . stopPropagation ( )
90+ onRename ( )
91+ } }
92+ >
93+ < RiEditLine className = "size-4 opacity-50" />
94+ Rename
95+ </ DropdownMenuItem >
96+ < DropdownMenuItem
97+ className = "text-destructive focus:text-destructive"
98+ onClick = { ( e ) => {
99+ e . stopPropagation ( )
100+ onRemove ( )
101+ } }
102+ >
103+ < RiDeleteBinLine className = "size-4" />
104+ Remove
105+ </ DropdownMenuItem >
106+ </ DropdownMenuContent >
107+ </ DropdownMenu >
108+ </ Link >
109+ </ motion . div >
103110 )
104111}
105112
@@ -121,52 +128,66 @@ export function Empty() {
121128 )
122129}
123130
124- function DatabaseCardSkeleton ( ) {
125- return (
126- < div className = "relative flex items-center justify-between gap-4 rounded-lg bg-card p-5" >
127- < Skeleton className = "size-14 shrink-0 rounded-full" />
128- < div className = "flex flex-1 flex-col gap-2 min-w-0" >
129- < Skeleton className = "h-5 w-1/3" />
130- < Skeleton className = "h-4 w-2/3" />
131- </ div >
132- </ div >
133- )
134- }
135-
136131export function DatabasesList ( ) {
137132 const { data : databases } = useLiveQuery ( q => q
138133 . from ( { databases : databasesCollection } )
139134 . orderBy ( ( { databases } ) => databases . createdAt , 'desc' ) )
140135 const renameDialogRef = useRef < ComponentRef < typeof RenameConnectionDialog > > ( null )
141136 const removeDialogRef = useRef < ComponentRef < typeof RemoveConnectionDialog > > ( null )
137+ const [ selectedLabel , setSelectedLabel ] = useState < string | null > ( null )
138+
139+ const availableLabels = Array . from ( new Set ( databases . map ( db => db . label ) . filter ( Boolean ) as string [ ] ) ) . sort ( )
140+
141+ const filteredDatabases = selectedLabel
142+ ? databases . filter ( db => db . label === selectedLabel )
143+ : databases
142144
143145 return (
144146 < div className = "flex flex-col gap-6" >
145147 < RemoveConnectionDialog ref = { removeDialogRef } />
146148 < RenameConnectionDialog ref = { renameDialogRef } />
149+ { availableLabels . length > 0 && (
150+ < ButtonGroup >
151+ < Button
152+ variant = { selectedLabel === null ? 'default' : 'outline' }
153+ size = "xs"
154+ onClick = { ( ) => setSelectedLabel ( null ) }
155+ className = "border!"
156+ >
157+ All
158+ </ Button >
159+ { availableLabels . map ( label => (
160+ < Button
161+ key = { label }
162+ variant = { selectedLabel === label ? 'default' : 'outline' }
163+ size = "xs"
164+ onClick = { ( ) => setSelectedLabel ( label ) }
165+ className = "border!"
166+ >
167+ { label }
168+ </ Button >
169+ ) ) }
170+ </ ButtonGroup >
171+ ) }
147172 < div className = "flex flex-col gap-2" >
148- { ! databases
173+ { filteredDatabases . length > 0
149174 ? (
150- < >
151- < DatabaseCardSkeleton />
152- < DatabaseCardSkeleton />
153- < DatabaseCardSkeleton />
154- </ >
175+ < AnimatePresence initial = { false } mode = "popLayout" >
176+ { filteredDatabases . map ( database => (
177+ < DatabaseCard
178+ key = { database . id }
179+ database = { database }
180+ onRemove = { ( ) => {
181+ removeDialogRef . current ?. remove ( database )
182+ } }
183+ onRename = { ( ) => {
184+ renameDialogRef . current ?. rename ( database )
185+ } }
186+ />
187+ ) ) }
188+ </ AnimatePresence >
155189 )
156- : databases ?. length
157- ? databases . map ( database => (
158- < DatabaseCard
159- key = { database . id }
160- database = { database }
161- onRemove = { ( ) => {
162- removeDialogRef . current ?. remove ( database )
163- } }
164- onRename = { ( ) => {
165- renameDialogRef . current ?. rename ( database )
166- } }
167- />
168- ) )
169- : < Empty /> }
190+ : < Empty /> }
170191 </ div >
171192 </ div >
172193 )
0 commit comments