Skip to content

Commit eb38260

Browse files
committed
improve usability of config page
1 parent 0c0fb4c commit eb38260

File tree

1 file changed

+113
-65
lines changed

1 file changed

+113
-65
lines changed

app/routes/(authed)/config.tsx

Lines changed: 113 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,15 @@ import {
2121
Server,
2222
Database,
2323
FileCode,
24-
Plus
24+
Plus,
25+
Check
2526
} from "lucide-react";
2627
import { useState } from "react";
2728
import { AppSidebar } from "@/components/app-sidebar";
2829
import { AppNavbar } from "@/components/app-navbar";
2930
import { cn } from "@/lib/utils";
31+
import { ScrollArea } from "@/components/ui/scroll-area";
32+
import { Input } from "@/components/ui/input";
3033

3134
export 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

Comments
 (0)