@@ -21,12 +21,15 @@ import {
2121 Server ,
2222 Database ,
2323 FileCode ,
24- Plus
24+ Plus ,
25+ Check
2526} from "lucide-react" ;
2627import { useState } from "react" ;
2728import { AppSidebar } from "@/components/app-sidebar" ;
2829import { AppNavbar } from "@/components/app-navbar" ;
2930import { cn } from "@/lib/utils" ;
31+ import { ScrollArea } from "@/components/ui/scroll-area" ;
32+ import { Input } from "@/components/ui/input" ;
3033
3134export const Route = createFileRoute ( "/(authed)/config" ) ( {
3235 component : ConfigComponent ,
@@ -71,6 +74,8 @@ function ConfigComponent() {
7174 . map ( product => `${ service . id } -${ product . id } ` )
7275 )
7376 ) ) ;
77+ const [ searchQuery , setSearchQuery ] = useState ( "" ) ;
78+ const [ productSearchQueries , setProductSearchQueries ] = useState < Record < string , string > > ( { } ) ;
7479
7580 const toggleService = ( serviceId : string ) => {
7681 const newExpanded = new Set ( expandedServices ) ;
@@ -114,6 +119,28 @@ function ConfigComponent() {
114119 return < Cloud className = "h-5 w-5" /> ;
115120 } ;
116121
122+ const filteredServices = services . filter ( service =>
123+ service . name . toLowerCase ( ) . includes ( searchQuery . toLowerCase ( ) ) ||
124+ service . description . toLowerCase ( ) . includes ( searchQuery . toLowerCase ( ) ) ||
125+ service . products . some ( product =>
126+ product . name . toLowerCase ( ) . includes ( searchQuery . toLowerCase ( ) )
127+ )
128+ ) ;
129+
130+ const getFilteredProducts = ( service : typeof services [ 0 ] ) => {
131+ const productSearch = productSearchQueries [ service . id ] || "" ;
132+ return service . products . filter ( product =>
133+ product . name . toLowerCase ( ) . includes ( productSearch . toLowerCase ( ) )
134+ ) ;
135+ } ;
136+
137+ const handleProductSearch = ( serviceId : string , query : string ) => {
138+ setProductSearchQueries ( prev => ( {
139+ ...prev ,
140+ [ serviceId ] : query
141+ } ) ) ;
142+ } ;
143+
117144 return (
118145 < AppSidebar >
119146 < div className = "min-h-screen bg-background" >
@@ -128,73 +155,94 @@ function ConfigComponent() {
128155 </ p >
129156 </ div >
130157
131- < div className = "grid gap-6" >
132- { services . map ( ( service ) => (
133- < Collapsible
134- key = { service . id }
135- open = { expandedServices . has ( service . id ) }
136- onOpenChange = { ( ) => toggleService ( service . id ) }
137- >
138- < Card className = "overflow-hidden" >
139- < CollapsibleTrigger className = "w-full text-left" >
140- < CardHeader className = "p-6" >
141- < div className = "flex items-center justify-between" >
142- < div >
143- < h3 className = "text-xl font-semibold" > { service . name } </ h3 >
144- < p className = "text-sm text-muted-foreground mt-1" >
145- { service . description }
146- </ p >
147- </ div >
148- < Button variant = "ghost" size = "icon" >
149- { expandedServices . has ( service . id ) ? (
150- < ChevronUp className = "h-4 w-4" />
151- ) : (
152- < ChevronDown className = "h-4 w-4" />
153- ) }
154- </ Button >
155- </ div >
156- </ CardHeader >
157- </ CollapsibleTrigger >
158-
159- < CollapsibleContent >
160- < CardContent className = "p-6 pt-0" >
161- < div className = "grid grid-cols-4 gap-4" >
162- { service . products . map ( ( product ) => {
163- const isEnabled = enabledProducts . has ( `${ service . id } -${ product . id } ` ) ;
164- return (
165- < Button
166- key = { product . id }
167- variant = { isEnabled ? "default" : "outline" }
168- className = { cn (
169- "flex flex-col items-center justify-center gap-2 h-24 w-full transition-[background-color] duration-75" ,
170- isEnabled && "bg-primary text-primary-foreground" ,
171- ! isEnabled && "hover:bg-primary/10"
158+ < div className = "space-y-4" >
159+ < Input
160+ placeholder = "Search services and products..."
161+ value = { searchQuery }
162+ onChange = { ( e ) => setSearchQuery ( e . target . value ) }
163+ className = "max-w-md"
164+ />
165+
166+ < ScrollArea className = "h-[calc(100vh-300px)]" >
167+ < div className = "space-y-4" >
168+ { filteredServices . map ( ( service ) => (
169+ < Collapsible
170+ key = { service . id }
171+ open = { expandedServices . has ( service . id ) }
172+ onOpenChange = { ( ) => toggleService ( service . id ) }
173+ >
174+ < Card className = "overflow-hidden" >
175+ < CollapsibleTrigger className = "w-full text-left" >
176+ < CardHeader className = "p-6" >
177+ < div className = "flex items-center justify-between" >
178+ < div >
179+ < h3 className = "text-xl font-semibold" > { service . name } </ h3 >
180+ < p className = "text-sm text-muted-foreground mt-1" >
181+ { service . description }
182+ </ p >
183+ </ div >
184+ < Button variant = "ghost" size = "icon" >
185+ { expandedServices . has ( service . id ) ? (
186+ < ChevronUp className = "h-4 w-4" />
187+ ) : (
188+ < ChevronDown className = "h-4 w-4" />
172189 ) }
173- onClick = { ( ) => toggleProduct ( service . id , product . id ) }
174- >
175- { getIconForProduct ( service . id , product . id ) }
176- < span className = "text-sm font-medium text-center" > { product . name } </ span >
177190 </ Button >
178- ) ;
179- } ) }
180- </ div >
181- </ CardContent >
182- </ CollapsibleContent >
191+ </ div >
192+ </ CardHeader >
193+ </ CollapsibleTrigger >
194+
195+ < CollapsibleContent >
196+ < CardContent className = "p-0" >
197+ < div className = "py-2 px-6" >
198+ < Input
199+ placeholder = { `Search ${ service . name } products...` }
200+ value = { productSearchQueries [ service . id ] || "" }
201+ onChange = { ( e ) => handleProductSearch ( service . id , e . target . value ) }
202+ className = "w-full"
203+ onClick = { ( e ) => e . stopPropagation ( ) }
204+ />
205+ </ div >
206+ < div className = "divide-y" >
207+ { getFilteredProducts ( service ) . map ( ( product ) => {
208+ const isEnabled = enabledProducts . has ( `${ service . id } -${ product . id } ` ) ;
209+ return (
210+ < button
211+ key = { product . id }
212+ className = { cn (
213+ "w-full px-6 py-4 flex items-center justify-between hover:bg-accent/5 transition-colors" ,
214+ isEnabled && "bg-accent/10"
215+ ) }
216+ onClick = { ( ) => toggleProduct ( service . id , product . id ) }
217+ >
218+ < div className = "flex items-center gap-3" >
219+ { getIconForProduct ( service . id , product . id ) }
220+ < span className = "font-medium" > { product . name } </ span >
221+ </ div >
222+ { isEnabled && < Check className = "h-4 w-4 text-accent" /> }
223+ </ button >
224+ ) ;
225+ } ) }
226+ </ div >
227+ </ CardContent >
228+ </ CollapsibleContent >
229+ </ Card >
230+ </ Collapsible >
231+ ) ) }
232+
233+ < Card className = "overflow-hidden hover:bg-accent/50 transition-colors cursor-pointer" >
234+ < CardHeader className = "flex flex-row items-center justify-center p-6" >
235+ < div className = "flex flex-col items-center text-center" >
236+ < Plus className = "h-5 w-5 mb-2 text-muted-foreground" />
237+ < h3 className = "text-xl font-semibold" > Add a new service</ h3 >
238+ < p className = "text-sm text-muted-foreground mt-1" >
239+ Configure a new service integration
240+ </ p >
241+ </ div >
242+ </ CardHeader >
183243 </ Card >
184- </ Collapsible >
185- ) ) }
186-
187- < Card className = "overflow-hidden hover:bg-accent/50 transition-colors cursor-pointer" >
188- < CardHeader className = "flex flex-row items-center justify-center p-6" >
189- < div className = "flex flex-col items-center text-center" >
190- < Plus className = "h-5 w-5 mb-2 text-muted-foreground" />
191- < h3 className = "text-xl font-semibold" > Add a new service</ h3 >
192- < p className = "text-sm text-muted-foreground mt-1" >
193- Configure a new service integration
194- </ p >
195- </ div >
196- </ CardHeader >
197- </ Card >
244+ </ div >
245+ </ ScrollArea >
198246 </ div >
199247 </ div >
200248 </ main >
0 commit comments