Currency Api Dashboard

TypeScript

Modern currency exchange rate dashboard with real-time market data, conversion tools, analytics, historical trends, and responsive admin interface.

Stars
18
Forks
1
Downloads
N/A
Open Issues
0
Files main

Repository Files

Loading file structure...
resources/js/components/ui/combobox.tsx
import React, { useState, useRef, useEffect } from 'react';
import { Check, ChevronsUpDown, Search } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';

interface ComboboxOption {
    value: string;
    label: string;
    searchTerms?: string;
}

interface ComboboxProps {
    options: ComboboxOption[];
    value: string;
    onChange: (value: string) => void;
    placeholder?: string;
    emptyText?: string;
    className?: string;
}

export function Combobox({
    options,
    value,
    onChange,
    placeholder = "Select option...",
    emptyText = "No results found.",
    className
}: ComboboxProps) {
    const [isOpen, setIsOpen] = useState(false);
    const [search, setSearch] = useState('');
    const containerRef = useRef<HTMLDivElement>(null);

    // Selected option label
    const selectedOption = options.find(opt => opt.value === value);

    // Filter options based on search term
    const filteredOptions = options.filter(opt => {
        const term = search.toLowerCase();
        const label = opt.label.toLowerCase();
        const val = opt.value.toLowerCase();
        const searchTerms = opt.searchTerms ? opt.searchTerms.toLowerCase() : '';
        return label.includes(term) || val.includes(term) || searchTerms.includes(term);
    });

    // Close popover when clicking outside
    useEffect(() => {
        function handleClickOutside(event: MouseEvent) {
            if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
                setIsOpen(false);
            }
        }
        document.addEventListener('mousedown', handleClickOutside);
        return () => document.removeEventListener('mousedown', handleClickOutside);
    }, []);

    // Reset search when popover closes
    useEffect(() => {
        if (!isOpen) {
            setSearch('');
        }
    }, [isOpen]);

    return (
        <div ref={containerRef} className={cn("relative w-full", className)}>
            <Button
                type="button"
                variant="outline"
                onClick={() => setIsOpen(!isOpen)}
                className="w-full justify-between bg-transparent hover:bg-transparent text-foreground border border-input h-9 px-3 py-1.5 text-sm font-normal text-left shadow-xs focus:ring-1 focus:ring-ring cursor-pointer"
            >
                <span className="truncate">
                    {selectedOption ? selectedOption.label : placeholder}
                </span>
                <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
            </Button>

            {isOpen && (
                <div className="absolute z-50 mt-1 w-full rounded-md border border-border bg-popover text-popover-foreground shadow-md animate-in fade-in slide-in-from-top-1 duration-150">
                    {/* Search Input */}
                    <div className="flex items-center border-b border-border px-3 py-2">
                        <Search className="mr-2 h-4 w-4 shrink-0 opacity-50 text-popover-foreground" />
                        <input
                            type="text"
                            placeholder="Search..."
                            value={search}
                            onChange={(e) => setSearch(e.target.value)}
                            className="w-full bg-transparent border-0 p-0 text-sm outline-hidden focus:ring-0 placeholder:text-muted-foreground text-popover-foreground"
                            autoFocus
                        />
                    </div>

                    {/* Options */}
                    <div className="max-h-60 overflow-y-auto p-1 space-y-0.5">
                        {filteredOptions.length === 0 ? (
                            <div className="py-2 px-3 text-sm text-muted-foreground text-center">
                                {emptyText}
                            </div>
                        ) : (
                            filteredOptions.map((opt) => {
                                const isSelected = opt.value === value;
                                return (
                                    <button
                                        key={opt.value}
                                        type="button"
                                        onClick={() => {
                                            onChange(opt.value);
                                            setIsOpen(false);
                                        }}
                                        className={cn(
                                            "w-full flex items-center justify-between px-3 py-2 text-sm rounded-sm text-left transition-colors cursor-pointer",
                                            isSelected 
                                                ? "bg-primary text-primary-foreground font-semibold"
                                                : "hover:bg-accent hover:text-accent-foreground text-popover-foreground"
                                        )}
                                    >
                                        <span className="truncate">{opt.label}</span>
                                        {isSelected && <Check className="h-4 w-4 shrink-0" />}
                                    </button>
                                );
                            })
                        )}
                    </div>
                </div>
            )}
        </div>
    );
}