Add user authentication and dynamic feature/category inputs

Implement Keycloak authentication, protected routes, and add combobox and autocomplete components for tool categories and features.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 776963d0-f75d-42e2-a57b-cc36bdff8495
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 0b145113-c016-4f54-b000-13bd3b0ba8f0
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/0683fb79-a27c-485c-9333-5f4b288c4567/776963d0-f75d-42e2-a57b-cc36bdff8495/z4uWN6A
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
cheffe01
2026-05-25 13:20:15 +00:00
parent e03c51a75e
commit 7abb048edc
22 changed files with 1027 additions and 68 deletions
@@ -0,0 +1,112 @@
import { useState } from "react";
import { Check, ChevronsUpDown } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { useListCategories } from "@workspace/api-client-react";
interface CategoryComboboxProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
}
export function CategoryCombobox({ value, onChange, placeholder = "Select or type a category..." }: CategoryComboboxProps) {
const [open, setOpen] = useState(false);
const [inputValue, setInputValue] = useState(value);
const categories = useListCategories();
const known: string[] = categories.data ?? [];
const filtered = inputValue.trim()
? known.filter((c) => c.toLowerCase().includes(inputValue.toLowerCase()))
: known;
const showCreateOption = inputValue.trim() !== "" && !known.some(
(c) => c.toLowerCase() === inputValue.toLowerCase()
);
function select(val: string) {
onChange(val);
setInputValue(val);
setOpen(false);
}
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="w-full justify-between font-normal h-9"
data-testid="button-category-combobox"
>
<span className={cn(!value && "text-muted-foreground")}>
{value || placeholder}
</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0" align="start">
<Command shouldFilter={false}>
<CommandInput
placeholder="Search or enter new category..."
value={inputValue}
onValueChange={(v) => {
setInputValue(v);
onChange(v);
}}
data-testid="input-category-search"
/>
<CommandList>
{filtered.length === 0 && !showCreateOption && (
<CommandEmpty>No categories found.</CommandEmpty>
)}
{filtered.length > 0 && (
<CommandGroup heading="Known categories">
{filtered.map((cat) => (
<CommandItem
key={cat}
value={cat}
onSelect={() => select(cat)}
data-testid={`item-category-${cat}`}
>
<Check
className={cn("mr-2 h-4 w-4", value === cat ? "opacity-100" : "opacity-0")}
/>
{cat}
</CommandItem>
))}
</CommandGroup>
)}
{showCreateOption && (
<CommandGroup heading="Create new">
<CommandItem
value={inputValue}
onSelect={() => select(inputValue.trim())}
data-testid="item-category-create-new"
>
<span className="text-primary font-medium">+ Create</span>
<span className="ml-2 text-muted-foreground">&ldquo;{inputValue.trim()}&rdquo;</span>
</CommandItem>
</CommandGroup>
)}
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}