Files
IPFS-portal/src/components/search/SearchInput.tsx
T
maikrolf f72b775379 refactor: codebase opschoning — type safety, error handling, central config, page splits
- categorie A: alle catch(e: any) → catch(e: unknown) met instanceof check
- categorie B: stille .catch() logging toegevoegd (SWRegister, admin, upload, auth)
- categorie C: hardcoded 192.168.1.176 IPs vervangen door env var defaults
- categorie D: page files >250L gesplitst (history, settings, explorer, admin/payment,
  ipns, users, upload, dashboard) in helpers/components
- categorie E: eslint-disable vervangen in SearchInput.tsx
- src/lib/config.ts centrale config module (localhost defaults)
- CSP in next.config.ts dynamisch via env var
- deploy.sh: geen hardcoded IP meer (REQUIRED arg)
- lib/api/ lib/auth/ lib/wallet/ lib/search-index/ lib/storage/ gesplitst in modules
- ongebruikte bestanden verwijderd: api.ts, auth.tsx, wallet.ts, search-index.ts,
  PaymentPanel.tsx, SearchBar.tsx, proxy.ts, serve-static.js
2026-07-19 15:35:18 +02:00

120 lines
3.5 KiB
TypeScript

'use client';
/* ── SearchInput ──
*
* Controlled search input with search icon, debounced search,
* loading spinner, clear button, and keyboard navigation handler.
*/
import { useState, useRef, useEffect, forwardRef, useImperativeHandle } from "react";
import { Search, X, Loader2 } from "lucide-react";
/* ── Types ── */
export interface SearchInputHandle {
clear: () => void;
focus: () => void;
blur: () => void;
}
interface SearchInputProps {
/** Called (debounced) when the user types */
onSearch: (query: string) => void;
/** Keyboard navigation handler (from parent orchestrator) */
onKeyDown: (e: React.KeyboardEvent) => void;
/** Called when input receives focus */
onFocus?: () => void;
/** Whether a search is in progress */
loading: boolean;
/** Whether there are results to show on focus */
hasResults: boolean;
/** Placeholder text */
placeholder?: string;
/** Auto-focus on mount */
autoFocus?: boolean;
}
/* ── Component ── */
const SearchInput = forwardRef<SearchInputHandle, SearchInputProps>(function SearchInput(
{ onSearch, onKeyDown, onFocus, loading, hasResults, placeholder = "Search files, CIDs, and content…", autoFocus = false },
ref,
) {
const [query, setQuery] = useState("");
const inputRef = useRef<HTMLInputElement>(null);
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
/* Expose clear + focus to parent */
useImperativeHandle(ref, () => ({
clear() {
setQuery("");
},
focus() {
inputRef.current?.focus();
},
blur() {
inputRef.current?.blur();
},
}));
/* ── Debounced input ── */
function handleChange(value: string) {
setQuery(value);
if (debounceRef.current) clearTimeout(debounceRef.current);
debounceRef.current = setTimeout(() => onSearch(value), 250);
}
/* ── Cleanup ── */
useEffect(() => {
if (autoFocus) inputRef.current?.focus();
return () => {
if (debounceRef.current) clearTimeout(debounceRef.current);
};
}, [autoFocus]);
/* ── Render ── */
return (
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-surface-500 pointer-events-none" />
<input
ref={inputRef}
value={query}
onChange={(e) => handleChange(e.target.value)}
onKeyDown={onKeyDown}
onFocus={() => {
if (hasResults) onFocus?.();
}}
placeholder={placeholder}
className="w-full pl-10 pr-10 py-2.5 rounded-xl bg-surface-900 border border-surface-700 text-sm text-surface-200 placeholder:text-surface-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-500/50 focus-visible:border-brand-500 transition-colors"
autoFocus={autoFocus}
role="combobox"
aria-expanded={hasResults}
aria-haspopup="listbox"
aria-autocomplete="list"
aria-controls="search-results-listbox"
/>
{loading && (
<Loader2 className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-surface-500 animate-spin" />
)}
{!loading && query && (
<button
onClick={() => {
setQuery("");
if (debounceRef.current) clearTimeout(debounceRef.current);
inputRef.current?.focus();
}}
className="absolute right-3 top-1/2 -translate-y-1/2 p-0.5 rounded hover:bg-surface-700 transition-colors"
>
<X className="w-4 h-4 text-surface-500" />
</button>
)}
</div>
);
});
export default SearchInput;