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(
+
+ )
+
+ expect(screen.getByText('Select an option')).toBeInTheDocument()
+ })
+
+ it('should open dropdown on click', async () => {
+ const user = userEvent.setup()
+
+ 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(
+
+ )
+
+ const trigger = screen.getByRole('button')
+ await user.click(trigger)
+
+ await user.click(screen.getByText('Option 2'))
+
+ expect(mockOnChange).toHaveBeenCalledWith('option2')
+ })
+
+ it('should display selected value', 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(
+
+ )
+
+ const trigger = screen.getByRole('button')
+ await user.click(trigger)
+ expect(screen.getByText('Option 1')).toBeInTheDocument()
+
+ // Click outside
+ await user.click(screen.getByTestId('outside'))
+
+ // Dropdown should be closed
+ expect(screen.queryByText('Option 1')).not.toBeInTheDocument()
+ })
+
+ it('should be disabled when disabled prop is true', () => {
+ render(
+
+ )
+
+ expect(screen.getByRole('button')).toBeDisabled()
+ })
+
+ it('should highlight selected option in dropdown', async () => {
+ const user = userEvent.setup()
+
+ const { container } = render(
+
+ )
+
+ const trigger = screen.getByRole('button')
+ await user.click(trigger)
+
+ // Find the dropdown option with the selected styling
+ const options = container.querySelectorAll('[class*="bg-orange-500/20"]')
+ expect(options.length).toBeGreaterThan(0)
+ expect(options[0].textContent).toBe('Option 2')
+ })
+
+ it('should apply custom className', () => {
+ 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 (
+
+ )
+ }
+)
+Textarea.displayName = "Textarea"
+
+export { Textarea }