klg-asutk-app/components/GlobalSearch.tsx
Yuriy aa052763f6 Безопасность и качество: 8 исправлений + обновления
- .env.example: полный шаблон, защита секретов
- .gitignore: явное исключение .env.* и секретов
- layout.tsx: XSS — заменён dangerouslySetInnerHTML на next/script для SW
- ESLint: no-console error (allow warn/error), ignore scripts/
- scripts/remove-console-logs.js: очистка console.log без glob
- backend/routes/modules: README с планом рефакторинга крупных файлов
- SECURITY.md: гид по секретам, XSS, CORS, auth, линту
- .husky/pre-commit: запуск npm run lint

+ прочие правки приложения и бэкенда

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 21:29:16 +03:00

64 lines
2.5 KiB
TypeScript

'use client';
import { useState, useCallback, useRef, useEffect } from 'react';
import { useRouter } from 'next/navigation';
const TYPE_ICONS: Record<string, string> = {
directive: '⚠️', bulletin: '📢', component: '🔩', work_order: '📐',
defect: '🛠️', specialist: '🎓', aircraft: '✈️',
};
export default function GlobalSearch() {
const [query, setQuery] = useState('');
const [results, setResults] = useState<any[]>([]);
const [open, setOpen] = useState(false);
const router = useRouter();
const ref = useRef<HTMLDivElement>(null);
const timer = useRef<NodeJS.Timeout>();
const search = useCallback((q: string) => {
if (q.length < 2) { setResults([]); return; }
clearTimeout(timer.current);
timer.current = setTimeout(async () => {
try {
const r = await fetch(`/api/v1/search/global?q=${encodeURIComponent(q)}`);
const data = await r.json();
setResults(data.results || []);
setOpen(true);
} catch { setResults([]); }
}, 300);
}, []);
useEffect(() => {
const handler = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
};
document.addEventListener('mousedown', handler);
return () => document.removeEventListener('mousedown', handler);
}, []);
return (
<div ref={ref} className="relative">
<input type="text" placeholder="🔍 Поиск..." value={query}
onChange={e => { setQuery(e.target.value); search(e.target.value); }}
onFocus={() => results.length > 0 && setOpen(true)}
className="w-full px-3 py-2 text-sm rounded-lg bg-gray-100 border-0 focus:bg-white focus:ring-2 focus:ring-blue-500 transition-all" />
{open && results.length > 0 && (
<div className="absolute top-full left-0 right-0 mt-1 bg-white border rounded-lg shadow-xl z-50 max-h-80 overflow-y-auto">
{results.map((r, i) => (
<button key={i} onClick={() => { router.push(r.url); setOpen(false); setQuery(''); }}
className="w-full text-left px-3 py-2 hover:bg-blue-50 border-b border-gray-50 transition-colors">
<div className="flex items-center gap-2">
<span>{TYPE_ICONS[r.type] || '📋'}</span>
<div className="min-w-0">
<div className="text-sm font-medium truncate">{r.title}</div>
<div className="text-[10px] text-gray-400 truncate">{r.subtitle}</div>
</div>
</div>
</button>
))}
</div>
)}
</div>
);
}