import { useState, useRef, useEffect, KeyboardEvent } from 'react'; import { X, Hash, AlertTriangle } from 'lucide-react'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; import { cn } from '@/lib/utils'; import { searchTags, normalizeTag, isCuratedTag, MAX_TAGS, } from '@/lib/skateTags'; interface TagInputProps { tags: string[]; onChange: (tags: string[]) => void; placeholder?: string; maxTags?: number; className?: string; disabled?: boolean; } export function TagInput({ tags, onChange, placeholder = 'Add tags...', maxTags = MAX_TAGS, className, disabled = false, }: TagInputProps) { const [inputValue, setInputValue] = useState(''); const [suggestions, setSuggestions] = useState([]); const [showSuggestions, setShowSuggestions] = useState(false); const [selectedIndex, setSelectedIndex] = useState(-1); const inputRef = useRef(null); const containerRef = useRef(null); // Update suggestions when input changes useEffect(() => { if (inputValue.trim()) { const results = searchTags(inputValue, 8).filter( (tag) => !tags.includes(tag) ); setSuggestions(results); setShowSuggestions(results.length > 0); setSelectedIndex(-1); } else { setSuggestions([]); setShowSuggestions(false); } }, [inputValue, tags]); // Close suggestions when clicking outside useEffect(() => { function handleClickOutside(event: MouseEvent) { if ( containerRef.current && !containerRef.current.contains(event.target as Node) ) { setShowSuggestions(false); } } document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); const addTag = (tag: string) => { const normalized = normalizeTag(tag); if (normalized && !tags.includes(normalized) && tags.length < maxTags) { onChange([...tags, normalized]); setInputValue(''); setShowSuggestions(false); inputRef.current?.focus(); } }; const removeTag = (tagToRemove: string) => { onChange(tags.filter((tag) => tag !== tagToRemove)); }; const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Enter') { e.preventDefault(); if (selectedIndex >= 0 && selectedIndex < suggestions.length) { addTag(suggestions[selectedIndex]); } else if (inputValue.trim()) { addTag(inputValue); } } else if (e.key === 'ArrowDown') { e.preventDefault(); setSelectedIndex((prev) => prev < suggestions.length - 1 ? prev + 1 : prev ); } else if (e.key === 'ArrowUp') { e.preventDefault(); setSelectedIndex((prev) => (prev > 0 ? prev - 1 : -1)); } else if (e.key === 'Escape') { setShowSuggestions(false); } else if (e.key === 'Backspace' && !inputValue && tags.length > 0) { removeTag(tags[tags.length - 1]); } else if (e.key === ',' || e.key === ' ') { if (inputValue.trim()) { e.preventDefault(); addTag(inputValue); } } }; const isAtLimit = tags.length >= maxTags; return (
{/* Tags display */}
{tags.map((tag) => { const isCurated = isCuratedTag(tag); return ( {tag} {!isCurated && ( )} ); })}
{/* Input */}
setInputValue(e.target.value)} onKeyDown={handleKeyDown} placeholder={isAtLimit ? `Max ${maxTags} tags reached` : placeholder} disabled={disabled || isAtLimit} className="pl-9" />
{/* Tag count */}

{tags.length}/{maxTags} tags {tags.some((t) => !isCuratedTag(t)) && ( Custom tags detected )}

{/* Autocomplete suggestions */} {showSuggestions && suggestions.length > 0 && (
{suggestions.map((suggestion, index) => ( ))}
)}
); } export default TagInput;