index.tsx raw

   1  import { cn } from '@/lib/utils'
   2  import { QrCodeIcon, SearchIcon, X } from 'lucide-react'
   3  import { ComponentProps, forwardRef, useEffect, useState } from 'react'
   4  import QrScannerModal from '../QrScannerModal'
   5  
   6  type SearchInputProps = ComponentProps<'input'> & {
   7    onQrScan?: (value: string) => void
   8  }
   9  
  10  const SearchInput = forwardRef<HTMLInputElement, SearchInputProps>(
  11    ({ value, onChange, className, onQrScan, ...props }, ref) => {
  12      const [displayClear, setDisplayClear] = useState(false)
  13      const [inputRef, setInputRef] = useState<HTMLInputElement | null>(null)
  14      const [showQrScanner, setShowQrScanner] = useState(false)
  15  
  16      useEffect(() => {
  17        setDisplayClear(!!value)
  18      }, [value])
  19  
  20      function setRefs(el: HTMLInputElement) {
  21        setInputRef(el)
  22        if (typeof ref === 'function') {
  23          ref(el)
  24        } else if (ref) {
  25          ;(ref as React.MutableRefObject<HTMLInputElement | null>).current = el
  26        }
  27      }
  28  
  29      const handleQrScan = (result: string) => {
  30        // Strip nostr: prefix if present
  31        const value = result.startsWith('nostr:') ? result.slice(6) : result
  32        onQrScan?.(value)
  33      }
  34  
  35      return (
  36        <>
  37          <div
  38            tabIndex={0}
  39            className={cn(
  40              'flex h-9 w-full items-center rounded-xl border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-all duration-200 md:text-sm [&:has(:focus-visible)]:ring-ring [&:has(:focus-visible)]:ring-2 [&:has(:focus-visible)]:outline-none hover:border-ring/50',
  41              className
  42            )}
  43          >
  44            <SearchIcon className="size-4 shrink-0 opacity-50" onClick={() => inputRef?.focus()} />
  45            <input
  46              {...props}
  47              name="search-input"
  48              ref={setRefs}
  49              value={value}
  50              onChange={onChange}
  51              className="size-full mx-2 border-none bg-transparent focus:outline-none placeholder:text-muted-foreground"
  52            />
  53            {onQrScan && (
  54              <button
  55                type="button"
  56                className="text-muted-foreground hover:text-foreground transition-colors size-5 shrink-0 flex items-center justify-center mr-1"
  57                onMouseDown={(e) => e.preventDefault()}
  58                onClick={() => setShowQrScanner(true)}
  59              >
  60                <QrCodeIcon className="size-4" />
  61              </button>
  62            )}
  63            {displayClear && (
  64              <button
  65                type="button"
  66                className="rounded-full bg-foreground/40 hover:bg-foreground transition-opacity size-5 shrink-0 flex flex-col items-center justify-center"
  67                onMouseDown={(e) => e.preventDefault()}
  68                onClick={() => onChange?.({ target: { value: '' } } as any)}
  69              >
  70                <X className="!size-3 shrink-0 text-background" strokeWidth={4} />
  71              </button>
  72            )}
  73          </div>
  74          {showQrScanner && (
  75            <QrScannerModal onScan={handleQrScan} onClose={() => setShowQrScanner(false)} />
  76          )}
  77        </>
  78      )
  79    }
  80  )
  81  SearchInput.displayName = 'SearchInput'
  82  export default SearchInput
  83