Skip to content

Commit a401d2c

Browse files
committed
[TASK-tsk_3b1abf5387c04f75f0814556] fix: remove unused SearchState interface
1 parent 03fa7b6 commit a401d2c

1 file changed

Lines changed: 206 additions & 0 deletions

File tree

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import { useState, useRef, useCallback, useEffect } from 'react';
2+
import { api, type SearchResult } from '../api.js';
3+
4+
// ─── Types ────────────────────────────────────────────────────────
5+
6+
export type SearchScope = 'all' | 'channel' | 'direct';
7+
8+
export interface SearchFilters {
9+
scope: SearchScope;
10+
agentId?: string;
11+
channel?: string;
12+
sessionId?: string;
13+
from?: string;
14+
to?: string;
15+
}
16+
17+
type SearchStatus = 'idle' | 'searching' | 'results' | 'empty' | 'error';
18+
19+
// ─── Storage helpers ──────────────────────────────────────────────
20+
21+
const RECENT_SEARCHES_KEY = 'markus_recent_searches';
22+
const MAX_RECENT = 10;
23+
24+
function getRecentSearches(): string[] {
25+
try {
26+
return JSON.parse(localStorage.getItem(RECENT_SEARCHES_KEY) || '[]');
27+
} catch {
28+
return [];
29+
}
30+
}
31+
32+
function saveSearchQuery(query: string): void {
33+
const recent = getRecentSearches();
34+
const filtered = recent.filter(q => q !== query);
35+
const updated = [query, ...filtered].slice(0, MAX_RECENT);
36+
localStorage.setItem(RECENT_SEARCHES_KEY, JSON.stringify(updated));
37+
}
38+
39+
function removeSearchQuery(query: string): void {
40+
const recent = getRecentSearches();
41+
const updated = recent.filter(q => q !== query);
42+
localStorage.setItem(RECENT_SEARCHES_KEY, JSON.stringify(updated));
43+
}
44+
45+
// ─── Hook ─────────────────────────────────────────────────────────
46+
47+
export function useMessageSearch(initialFilters?: Partial<SearchFilters>) {
48+
const [query, setQuery] = useState('');
49+
const [status, setStatus] = useState<SearchStatus>('idle');
50+
const [results, setResults] = useState<SearchResult[]>([]);
51+
const [total, setTotal] = useState(0);
52+
const [speedMs, setSpeedMs] = useState(0);
53+
const [error, setError] = useState<string | null>(null);
54+
const [selectedIndex, setSelectedIndex] = useState(-1);
55+
const [filters, setFilters] = useState<SearchFilters>({
56+
scope: 'all',
57+
...initialFilters,
58+
});
59+
const [recentSearches, setRecentSearches] = useState<string[]>(getRecentSearches);
60+
61+
const debounceRef = useRef<ReturnType<typeof setTimeout>>(undefined);
62+
const abortRef = useRef<AbortController | undefined>(undefined);
63+
64+
// Cleanup on unmount
65+
useEffect(() => {
66+
return () => {
67+
if (debounceRef.current) clearTimeout(debounceRef.current);
68+
abortRef.current?.abort();
69+
};
70+
}, []);
71+
72+
const executeSearch = useCallback(async (q: string, searchFilters: SearchFilters) => {
73+
if (q.length < 2) {
74+
setResults([]);
75+
setTotal(0);
76+
setStatus('idle');
77+
return;
78+
}
79+
80+
// Abort any in-flight request
81+
abortRef.current?.abort();
82+
const controller = new AbortController();
83+
abortRef.current = controller;
84+
85+
setStatus('searching');
86+
setError(null);
87+
setSelectedIndex(-1);
88+
89+
try {
90+
const response = await api.messages.search(q, {
91+
scope: searchFilters.scope,
92+
channel: searchFilters.channel,
93+
agentId: searchFilters.agentId,
94+
sessionId: searchFilters.sessionId,
95+
from: searchFilters.from,
96+
to: searchFilters.to,
97+
limit: 30,
98+
});
99+
100+
// If aborted, ignore result
101+
if (controller.signal.aborted) return;
102+
103+
const resultList = response.results ?? [];
104+
setResults(resultList);
105+
setTotal(response.total ?? resultList.length);
106+
setSpeedMs(response.speedMs ?? 0);
107+
setStatus(resultList.length > 0 ? 'results' : 'empty');
108+
109+
// Save to recent searches on successful search
110+
if (resultList.length > 0 || q.length >= 2) {
111+
saveSearchQuery(q);
112+
setRecentSearches(getRecentSearches());
113+
}
114+
} catch (err: unknown) {
115+
if (controller.signal.aborted) return;
116+
setResults([]);
117+
setTotal(0);
118+
setError(err instanceof Error ? err.message : 'Search failed');
119+
setStatus('error');
120+
}
121+
}, []);
122+
123+
const handleQueryChange = useCallback((q: string) => {
124+
setQuery(q);
125+
if (debounceRef.current) clearTimeout(debounceRef.current);
126+
debounceRef.current = setTimeout(() => executeSearch(q, filters), 300);
127+
}, [executeSearch, filters]);
128+
129+
const handleFilterChange = useCallback((newFilters: Partial<SearchFilters>) => {
130+
const merged = { ...filters, ...newFilters };
131+
setFilters(merged);
132+
// Re-search immediately with new filters (no debounce)
133+
setQuery(q => {
134+
executeSearch(q, merged);
135+
return q;
136+
});
137+
}, [filters, executeSearch]);
138+
139+
const clearQuery = useCallback(() => {
140+
setQuery('');
141+
setResults([]);
142+
setTotal(0);
143+
setStatus('idle');
144+
setError(null);
145+
setSelectedIndex(-1);
146+
abortRef.current?.abort();
147+
}, []);
148+
149+
const reset = useCallback(() => {
150+
clearQuery();
151+
setFilters({ scope: 'all', ...initialFilters });
152+
}, [clearQuery, initialFilters]);
153+
154+
const clearRecentSearches = useCallback(() => {
155+
localStorage.removeItem(RECENT_SEARCHES_KEY);
156+
setRecentSearches([]);
157+
}, []);
158+
159+
const removeRecentSearch = useCallback((q: string) => {
160+
removeSearchQuery(q);
161+
setRecentSearches(getRecentSearches());
162+
}, []);
163+
164+
// Keyboard navigation
165+
const navigateUp = useCallback(() => {
166+
setSelectedIndex(prev => Math.max(0, prev - 1));
167+
}, []);
168+
169+
const navigateDown = useCallback(() => {
170+
setSelectedIndex(prev => Math.min(results.length - 1, prev + 1));
171+
}, [results.length]);
172+
173+
// Trigger search with existing query (for filter changes from imperative calls)
174+
const triggerSearch = useCallback(() => {
175+
if (query.length >= 2) {
176+
executeSearch(query, filters);
177+
}
178+
}, [query, filters, executeSearch]);
179+
180+
return {
181+
// State
182+
query,
183+
status,
184+
results,
185+
total,
186+
speedMs,
187+
error,
188+
selectedIndex,
189+
filters,
190+
recentSearches,
191+
192+
// Actions
193+
setQuery: handleQueryChange,
194+
setFilters: handleFilterChange,
195+
clearQuery,
196+
reset,
197+
clearRecentSearches,
198+
removeRecentSearch,
199+
navigateUp,
200+
navigateDown,
201+
triggerSearch,
202+
203+
// Re-expose for imperative use
204+
executeSearch: (q: string) => executeSearch(q, filters),
205+
};
206+
}

0 commit comments

Comments
 (0)