import { useEffect, useRef, useState } from "react"; import { useCategories, useCreateCategory } from "../hooks/useCategories"; import { LucideIcon } from "../lib/iconData"; import { IconPicker } from "./IconPicker"; interface CategoryPickerProps { value: number; onChange: (categoryId: number) => void; } export function CategoryPicker({ value, onChange }: CategoryPickerProps) { const { data: categories = [] } = useCategories(); const createCategory = useCreateCategory(); const [inputValue, setInputValue] = useState(""); const [isOpen, setIsOpen] = useState(false); const [highlightIndex, setHighlightIndex] = useState(-1); const [isCreating, setIsCreating] = useState(false); const [newCategoryIcon, setNewCategoryIcon] = useState("package"); const containerRef = useRef(null); const inputRef = useRef(null); const listRef = useRef(null); // Sync display value when value prop changes const selectedCategory = categories.find((c) => c.id === value); const filtered = categories.filter((c) => c.name.toLowerCase().includes(inputValue.toLowerCase()), ); const showCreateOption = inputValue.trim() !== "" && !categories.some( (c) => c.name.toLowerCase() === inputValue.trim().toLowerCase(), ); const totalOptions = filtered.length + (showCreateOption ? 1 : 0); useEffect(() => { function handleClickOutside(e: MouseEvent) { const target = e.target as Node; if ( containerRef.current && !containerRef.current.contains(target) && !(target instanceof Element && target.closest("[data-icon-picker]")) ) { setIsOpen(false); setIsCreating(false); setNewCategoryIcon("package"); // Reset input to selected category name if (selectedCategory) { setInputValue(""); } } } document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, [selectedCategory]); function handleSelect(categoryId: number) { onChange(categoryId); setInputValue(""); setIsOpen(false); setHighlightIndex(-1); } function handleStartCreate() { setIsCreating(true); } async function handleConfirmCreate() { const name = inputValue.trim(); if (!name) return; createCategory.mutate( { name, icon: newCategoryIcon }, { onSuccess: (newCat) => { setIsCreating(false); setNewCategoryIcon("package"); handleSelect(newCat.id); }, }, ); } function handleKeyDown(e: React.KeyboardEvent) { if (!isOpen) { if (e.key === "ArrowDown" || e.key === "Enter") { setIsOpen(true); e.preventDefault(); } return; } switch (e.key) { case "ArrowDown": e.preventDefault(); setHighlightIndex((i) => Math.min(i + 1, totalOptions - 1)); break; case "ArrowUp": e.preventDefault(); setHighlightIndex((i) => Math.max(i - 1, 0)); break; case "Enter": e.preventDefault(); if (isCreating) { handleConfirmCreate(); } else if (highlightIndex >= 0 && highlightIndex < filtered.length) { handleSelect(filtered[highlightIndex].id); } else if (showCreateOption && highlightIndex === filtered.length) { handleStartCreate(); } break; case "Escape": if (isCreating) { setIsCreating(false); setNewCategoryIcon("package"); } else { setIsOpen(false); setHighlightIndex(-1); setInputValue(""); } break; } } // Scroll highlighted option into view useEffect(() => { if (highlightIndex >= 0 && listRef.current) { const option = listRef.current.children[highlightIndex] as HTMLElement; option?.scrollIntoView({ block: "nearest" }); } }, [highlightIndex]); return (
{!isOpen && selectedCategory && (
)} = 0 ? `category-option-${highlightIndex}` : undefined } value={ isOpen ? inputValue : selectedCategory ? selectedCategory.name : "" } placeholder="Search or create category..." onChange={(e) => { setInputValue(e.target.value); setIsOpen(true); setHighlightIndex(-1); }} onFocus={() => { setIsOpen(true); setInputValue(""); }} onKeyDown={handleKeyDown} className={`w-full py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent ${ !isOpen && selectedCategory ? "pl-8 pr-3" : "px-3" }`} />
{isOpen && (
    {filtered.map((cat, i) => (
  • handleSelect(cat.id)} onMouseEnter={() => setHighlightIndex(i)} > {cat.name}
  • ))} {showCreateOption && !isCreating && (
  • setHighlightIndex(filtered.length)} > + Create "{inputValue.trim()}"
  • )} {isCreating && (
  • {inputValue.trim()}
  • )} {filtered.length === 0 && !showCreateOption && (
  • No categories found
  • )}
)}
); }