diff --git a/admin-web/components/ui/select.test.tsx b/admin-web/components/ui/select.test.tsx new file mode 100644 index 0000000..14bb9fb --- /dev/null +++ b/admin-web/components/ui/select.test.tsx @@ -0,0 +1,148 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { render, screen, fireEvent } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { Select } from './select' + +describe('Select', () => { + const mockOptions = [ + { value: 'option1', label: 'Option 1' }, + { value: 'option2', label: 'Option 2' }, + { value: 'option3', label: 'Option 3' }, + ] + + const mockOnChange = vi.fn() + + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should render with placeholder', () => { + render( + + ) + + const trigger = screen.getByRole('button') + await user.click(trigger) + + expect(screen.getByText('Option 1')).toBeInTheDocument() + expect(screen.getByText('Option 2')).toBeInTheDocument() + expect(screen.getByText('Option 3')).toBeInTheDocument() + }) + + it('should select option and close dropdown', async () => { + const user = userEvent.setup() + + render( + + ) + + expect(screen.getByText('Option 2')).toBeInTheDocument() + }) + + it('should close dropdown on outside click', async () => { + const user = userEvent.setup() + + render( +
+ + ) + + expect(screen.getByRole('button')).toBeDisabled() + }) + + it('should highlight selected option in dropdown', async () => { + const user = userEvent.setup() + + const { container } = render( + + ) + + expect(screen.getByRole('button').parentElement).toHaveClass('custom-class') + }) +}) diff --git a/admin-web/components/ui/select.tsx b/admin-web/components/ui/select.tsx new file mode 100644 index 0000000..3c67f29 --- /dev/null +++ b/admin-web/components/ui/select.tsx @@ -0,0 +1,100 @@ +"use client" + +import * as React from "react" +import { ChevronDown } from "lucide-react" + +import { cn } from "@/lib/utils" + +interface SelectOption { + value: T + label: string +} + +interface SelectProps { + value?: T + onValueChange?: (value: T) => void + options: SelectOption[] + placeholder?: string + disabled?: boolean + className?: string + id?: string +} + +function Select({ + value, + onValueChange, + options, + placeholder = "Select an option", + disabled = false, + className, + id, +}: SelectProps) { + const [isOpen, setIsOpen] = React.useState(false) + const containerRef = React.useRef(null) + + const selectedOption = options.find((option) => option.value === value) + + React.useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + containerRef.current && + !containerRef.current.contains(event.target as Node) + ) { + setIsOpen(false) + } + } + + document.addEventListener("mousedown", handleClickOutside) + return () => document.removeEventListener("mousedown", handleClickOutside) + }, []) + + const handleSelect = (optionValue: string) => { + onValueChange?.(optionValue) + setIsOpen(false) + } + + return ( +
+ + + {isOpen && ( +
+ {options.map((option) => ( +
handleSelect(option.value)} + className={cn( + "relative flex cursor-pointer select-none items-center px-3 py-2 text-sm text-white hover:bg-neutral-800", + option.value === value && "bg-orange-500/20 text-orange-500" + )} + > + {option.label} +
+ ))} +
+ )} +
+ ) +} + +export { Select } +export type { SelectOption } diff --git a/admin-web/components/ui/switch.test.tsx b/admin-web/components/ui/switch.test.tsx new file mode 100644 index 0000000..b447e6e --- /dev/null +++ b/admin-web/components/ui/switch.test.tsx @@ -0,0 +1,121 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { Switch } from './switch' + +describe('Switch', () => { + const mockOnChange = vi.fn() + + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should render unchecked by default', () => { + render( + + ) + + const switchButton = screen.getByRole('switch') + expect(switchButton).toHaveAttribute('aria-checked', 'false') + }) + + it('should render checked when checked prop is true', () => { + render( + + ) + + const switchButton = screen.getByRole('switch') + expect(switchButton).toHaveAttribute('aria-checked', 'true') + }) + + it('should toggle on click', async () => { + const user = userEvent.setup() + + render( + + ) + + const switchButton = screen.getByRole('switch') + await user.click(switchButton) + + expect(mockOnChange).toHaveBeenCalledWith(true) + }) + + it('should toggle off when clicked while checked', async () => { + const user = userEvent.setup() + + render( + + ) + + const switchButton = screen.getByRole('switch') + await user.click(switchButton) + + expect(mockOnChange).toHaveBeenCalledWith(false) + }) + + it('should be disabled when disabled prop is true', () => { + render( + + ) + + expect(screen.getByRole('switch')).toBeDisabled() + }) + + it('should not call onChange when disabled', async () => { + const user = userEvent.setup() + + render( + + ) + + const switchButton = screen.getByRole('switch') + await user.click(switchButton) + + expect(mockOnChange).not.toHaveBeenCalled() + }) + + it('should have correct id attribute', () => { + render( + + ) + + expect(screen.getByRole('switch')).toHaveAttribute('id', 'test-switch') + }) + + it('should apply custom className', () => { + render( + + ) + + expect(screen.getByRole('switch')).toHaveClass('custom-class') + }) +}) diff --git a/admin-web/components/ui/switch.tsx b/admin-web/components/ui/switch.tsx new file mode 100644 index 0000000..a65a852 --- /dev/null +++ b/admin-web/components/ui/switch.tsx @@ -0,0 +1,52 @@ +"use client" + +import * as React from "react" + +import { cn } from "@/lib/utils" + +interface SwitchProps { + checked?: boolean + onCheckedChange?: (checked: boolean) => void + disabled?: boolean + className?: string + id?: string +} + +function Switch({ + checked = false, + onCheckedChange, + disabled = false, + className, + id, +}: SwitchProps) { + const handleClick = () => { + if (!disabled) { + onCheckedChange?.(!checked) + } + } + + return ( + + ) +} + +export { Switch } diff --git a/admin-web/components/ui/textarea.tsx b/admin-web/components/ui/textarea.tsx new file mode 100644 index 0000000..36d349d --- /dev/null +++ b/admin-web/components/ui/textarea.tsx @@ -0,0 +1,26 @@ +"use client" + +import * as React from "react" + +import { cn } from "@/lib/utils" + +interface TextareaProps + extends React.TextareaHTMLAttributes {} + +const Textarea = React.forwardRef( + ({ className, ...props }, ref) => { + return ( +