feat: add form input components with tests
- Add TagInput component for equipment tags - Add ExerciseList component for workout exercises - Both components include comprehensive test suites - Add data-testid attributes for testability
This commit is contained in:
100
admin-web/components/tag-input.tsx
Normal file
100
admin-web/components/tag-input.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { X, Plus } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Input } from "@/components/ui/input"
|
||||
|
||||
interface TagInputProps {
|
||||
value: string[]
|
||||
onChange: (value: string[]) => void
|
||||
placeholder?: string
|
||||
className?: string
|
||||
disabled?: boolean
|
||||
id?: string
|
||||
}
|
||||
|
||||
function TagInput({
|
||||
value,
|
||||
onChange,
|
||||
placeholder = "Add tag...",
|
||||
className,
|
||||
disabled = false,
|
||||
}: TagInputProps) {
|
||||
const [inputValue, setInputValue] = React.useState("")
|
||||
const inputRef = React.useRef<HTMLInputElement>(null)
|
||||
|
||||
const handleAddTag = () => {
|
||||
const trimmed = inputValue.trim()
|
||||
if (trimmed && !value.includes(trimmed)) {
|
||||
onChange([...value, trimmed])
|
||||
setInputValue("")
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemoveTag = (tagToRemove: string) => {
|
||||
onChange(value.filter((tag) => tag !== tagToRemove))
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault()
|
||||
handleAddTag()
|
||||
} else if (e.key === "Backspace" && !inputValue && value.length > 0) {
|
||||
handleRemoveTag(value[value.length - 1])
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-wrap items-center gap-2 rounded-md border border-neutral-700 bg-neutral-900 p-2 focus-within:border-orange-500 focus-within:ring-2 focus-within:ring-orange-500 focus-within:ring-offset-2",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{value.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="inline-flex items-center gap-1 rounded-md bg-orange-500/20 px-2 py-1 text-sm text-orange-500"
|
||||
>
|
||||
{tag}
|
||||
{!disabled && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveTag(tag)}
|
||||
className="rounded-full p-0.5 hover:bg-orange-500/30"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
<div className="flex flex-1 items-center">
|
||||
<Input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={handleAddTag}
|
||||
placeholder={value.length === 0 ? placeholder : ""}
|
||||
disabled={disabled}
|
||||
className="flex-1 border-0 bg-transparent px-0 py-1 text-white placeholder:text-neutral-500 focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
/>
|
||||
{inputValue.trim() && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAddTag}
|
||||
disabled={disabled}
|
||||
className="ml-2 rounded-md p-1 text-neutral-500 hover:bg-neutral-800 hover:text-white"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { TagInput }
|
||||
Reference in New Issue
Block a user