f72b775379
- 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
120 lines
3.5 KiB
TypeScript
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;
|