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
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
'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;
|
||||
Reference in New Issue
Block a user