From 32a948a34d6b4a3240053ff81417d189226c5363 Mon Sep 17 00:00:00 2001 From: Zaid Ahmad <109442753+zaidahmad16@users.noreply.github.com> Date: Wed, 17 Jun 2026 13:05:09 -0400 Subject: [PATCH 1/2] feat: added ExplorerSearch component with type-ahead filtering --- .gitignore | 1 + src/components/ExplorerSearch.tsx | 140 ++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 src/components/ExplorerSearch.tsx diff --git a/.gitignore b/.gitignore index fc6b291..b2bf9af 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ dist-ssr *.local # Editor directories and files +package-lock.json .vscode/* !.vscode/extensions.json .idea diff --git a/src/components/ExplorerSearch.tsx b/src/components/ExplorerSearch.tsx new file mode 100644 index 0000000..8efe068 --- /dev/null +++ b/src/components/ExplorerSearch.tsx @@ -0,0 +1,140 @@ +import {useState,useRef,useEffect,useCallback} from 'react'; +import { courseList } from '@/data/loadCourses'; +import { useExplorerStore } from '@/store/explorerStore'; + +const MAX_RESULTS = 8; + +function normalize (s: string): string { + return s.toLowerCase().replace(/\s+/g,''); +} + +interface Result { + code: string; + title: string; + +} + +function search(query: string): Result[]{ + if(!query.trim()) return[]; + const q = normalize(query); + const codePrefixMatches: Result[]=[]; + const otherMatches: Result[]=[]; + + for(const course of courseList){ + const normCode=normalize(course.code); + const normTitle = normalize(course.title); + if(normCode.startsWith(q)){ + codePrefixMatches.push({code: course.code,title: course.title}); + } + else if (normCode.includes(q)||normTitle.includes(q)){ + otherMatches.push({code: course.code,title:course.title}); + } + } + return [...codePrefixMatches,...otherMatches].slice(0,MAX_RESULTS) + + +} + +export default function ExplorerSearch() { + const { setSelectedCourse } = useExplorerStore(); + const [query, setQuery] = useState(''); + const [results, setResults] = useState([]); + const [activeIndex, setActiveIndex] = useState(-1); + const inputRef = useRef(null); + const listRef = useRef(null); + + useEffect(() => { + setResults(search(query)); + setActiveIndex(-1); + }, [query]); + + const selectResult = useCallback( + (code: string) => { + setSelectedCourse(code); + setQuery(''); + setResults([]); + setActiveIndex(-1); + }, + [setSelectedCourse], + ); + + function handleKeyDown(e: React.KeyboardEvent) { + if (e.key === 'ArrowDown') { + e.preventDefault(); + setActiveIndex((i) => Math.min(i + 1, results.length - 1)); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + setActiveIndex((i) => Math.max(i - 1, -1)); + } else if (e.key === 'Enter') { + if (activeIndex >= 0 && results[activeIndex]) { + selectResult(results[activeIndex].code); + } else if (results.length === 1) { + selectResult(results[0].code); + } + } else if (e.key === 'Escape') { + setQuery(''); + setResults([]); + inputRef.current?.blur(); + } + } + + useEffect(() => { + if (activeIndex >= 0 && listRef.current) { + const item = listRef.current.children[activeIndex] as HTMLElement; + item?.scrollIntoView({ block: 'nearest' }); + } + }, [activeIndex]); + + const open = results.length > 0; + + return ( +
+ setQuery(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Search courses…" + aria-label="Search courses" + aria-autocomplete="list" + aria-expanded={open} + aria-controls={open ? 'explorer-search-results' : undefined} + aria-activedescendant={ + activeIndex >= 0 ? `explorer-result-${activeIndex}` : undefined + } + className="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm shadow-md outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-200" + /> + {open && ( +
    + {results.map((r, i) => ( +
  • { + e.preventDefault(); + selectResult(r.code); + }} + onMouseEnter={() => setActiveIndex(i)} + className={`cursor-pointer px-3 py-2 text-sm ${ + i === activeIndex + ? 'bg-blue-50 text-blue-700' + : 'text-gray-800 hover:bg-gray-50' + }`} + > + {r.code} + — {r.title} +
  • + ))} +
+ )} +
+ ); +} From 16b710d204503cc3b0f83817b4ba4efaed68d08f Mon Sep 17 00:00:00 2001 From: Zaid Ahmad <109442753+zaidahmad16@users.noreply.github.com> Date: Wed, 17 Jun 2026 14:01:25 -0400 Subject: [PATCH 2/2] feat: implement explorer search with type-ahead filtering and keyboard navigation --- src/components/ExplorerSearch.tsx | 13 +++---------- src/pages/Explorer.tsx | 2 ++ 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/components/ExplorerSearch.tsx b/src/components/ExplorerSearch.tsx index 8efe068..b1cf47e 100644 --- a/src/components/ExplorerSearch.tsx +++ b/src/components/ExplorerSearch.tsx @@ -1,4 +1,4 @@ -import {useState,useRef,useEffect,useCallback} from 'react'; +import {useState,useRef,useEffect,useCallback,useMemo} from 'react'; import { courseList } from '@/data/loadCourses'; import { useExplorerStore } from '@/store/explorerStore'; @@ -38,21 +38,15 @@ function search(query: string): Result[]{ export default function ExplorerSearch() { const { setSelectedCourse } = useExplorerStore(); const [query, setQuery] = useState(''); - const [results, setResults] = useState([]); + const results = useMemo(() => search(query), [query]); const [activeIndex, setActiveIndex] = useState(-1); const inputRef = useRef(null); const listRef = useRef(null); - useEffect(() => { - setResults(search(query)); - setActiveIndex(-1); - }, [query]); - const selectResult = useCallback( (code: string) => { setSelectedCourse(code); setQuery(''); - setResults([]); setActiveIndex(-1); }, [setSelectedCourse], @@ -73,7 +67,6 @@ export default function ExplorerSearch() { } } else if (e.key === 'Escape') { setQuery(''); - setResults([]); inputRef.current?.blur(); } } @@ -93,7 +86,7 @@ export default function ExplorerSearch() { ref={inputRef} type="search" value={query} - onChange={(e) => setQuery(e.target.value)} + onChange={(e) => { setQuery(e.target.value); setActiveIndex(-1); }} onKeyDown={handleKeyDown} placeholder="Search courses…" aria-label="Search courses" diff --git a/src/pages/Explorer.tsx b/src/pages/Explorer.tsx index a198501..8ceb825 100644 --- a/src/pages/Explorer.tsx +++ b/src/pages/Explorer.tsx @@ -24,6 +24,7 @@ import { useExplorerStore } from '@/store/explorerStore'; import CourseNode from '@/components/CourseNode'; import type { CourseNodeData } from '@/components/CourseNode'; import CourseDetailPanel from '@/components/CourseDetailPanel'; +import ExplorerSearch from '@/components/ExplorerSearch'; const NODE_W = 180; const NODE_H = 60; @@ -106,6 +107,7 @@ export default function Explorer() { return (
+