11import { useState , useEffect , useRef , useCallback } from 'react' ;
22
33interface SearchResult {
4+ module : string ;
45 id : number ;
56 title : string ;
67 subtitle : string ;
78 url : string ;
8- type : string ;
99}
1010
11- const TYPE_COLORS : Record < string , string > = {
12- Product : 'bg-blue-100 text-blue-700' ,
13- Invoice : 'bg-green-100 text-green-700' ,
14- Contact : 'bg-purple-100 text-purple-700' ,
15- Lead : 'bg-orange-100 text-orange-700' ,
16- Ticket : 'bg-red-100 text-red-700' ,
17- Employee : 'bg-indigo-100 text-indigo-700' ,
18- Project : 'bg-teal-100 text-teal-700' ,
19- Order : 'bg-pink-100 text-pink-700' ,
11+ const MODULE_COLORS : Record < string , string > = {
12+ invoice : 'bg-green-100 text-green-700' ,
13+ contact : 'bg-blue-100 text-blue-700' ,
14+ product : 'bg-purple-100 text-purple-700' ,
15+ employee : 'bg-orange-100 text-orange-700' ,
16+ lead : 'bg-pink-100 text-pink-700' ,
17+ project : 'bg-indigo-100 text-indigo-700' ,
18+ purchase_order : 'bg-yellow-100 text-yellow-700' ,
2019} ;
2120
2221export default function GlobalSearch ( ) {
@@ -43,14 +42,26 @@ export default function GlobalSearch() {
4342 } , [ ] ) ;
4443
4544 const search = useCallback ( ( q : string ) => {
46- if ( q . length < 2 ) { setResults ( [ ] ) ; setOpen ( false ) ; return ; }
45+ if ( q . length < 2 ) {
46+ setResults ( [ ] ) ;
47+ setOpen ( false ) ;
48+ return ;
49+ }
4750 setLoading ( true ) ;
48- fetch ( `/search?q=${ encodeURIComponent ( q ) } ` , {
49- headers : { 'Accept' : 'application/json' , 'X-Requested-With' : 'XMLHttpRequest' }
51+ fetch ( `/api/v1/search?q=${ encodeURIComponent ( q ) } ` , {
52+ headers : {
53+ Accept : 'application/json' ,
54+ 'X-Requested-With' : 'XMLHttpRequest' ,
55+ } ,
5056 } )
51- . then ( r => r . json ( ) )
52- . then ( data => { setResults ( data . results ?? [ ] ) ; setOpen ( true ) ; setLoading ( false ) ; setActiveIndex ( - 1 ) ; } )
53- . catch ( ( ) => setLoading ( false ) ) ;
57+ . then ( r => r . json ( ) )
58+ . then ( data => {
59+ setResults ( data . data ?. results ?? [ ] ) ;
60+ setOpen ( true ) ;
61+ setLoading ( false ) ;
62+ setActiveIndex ( - 1 ) ;
63+ } )
64+ . catch ( ( ) => setLoading ( false ) ) ;
5465 } , [ ] ) ;
5566
5667 const handleChange = ( e : React . ChangeEvent < HTMLInputElement > ) => {
@@ -62,38 +73,80 @@ export default function GlobalSearch() {
6273
6374 const handleKeyDown = ( e : React . KeyboardEvent ) => {
6475 if ( ! open ) return ;
65- if ( e . key === 'ArrowDown' ) { e . preventDefault ( ) ; setActiveIndex ( i => Math . min ( i + 1 , results . length - 1 ) ) ; }
66- if ( e . key === 'ArrowUp' ) { e . preventDefault ( ) ; setActiveIndex ( i => Math . max ( i - 1 , 0 ) ) ; }
67- if ( e . key === 'Enter' && activeIndex >= 0 ) { window . location . href = results [ activeIndex ] . url ; }
68- if ( e . key === 'Escape' ) { setOpen ( false ) ; }
76+ if ( e . key === 'ArrowDown' ) {
77+ e . preventDefault ( ) ;
78+ setActiveIndex ( i => Math . min ( i + 1 , results . length - 1 ) ) ;
79+ }
80+ if ( e . key === 'ArrowUp' ) {
81+ e . preventDefault ( ) ;
82+ setActiveIndex ( i => Math . max ( i - 1 , 0 ) ) ;
83+ }
84+ if ( e . key === 'Enter' && activeIndex >= 0 ) {
85+ window . location . href = results [ activeIndex ] . url ;
86+ }
87+ if ( e . key === 'Escape' ) {
88+ setOpen ( false ) ;
89+ }
6990 } ;
7091
7192 return (
7293 < div className = "relative w-72" >
7394 < div className = "relative" >
74- < svg className = "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-400" fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" > < path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> </ svg >
95+ < svg
96+ className = "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-400"
97+ fill = "none"
98+ viewBox = "0 0 24 24"
99+ stroke = "currentColor"
100+ >
101+ < path
102+ strokeLinecap = "round"
103+ strokeLinejoin = "round"
104+ strokeWidth = { 2 }
105+ d = "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
106+ />
107+ </ svg >
75108 < input
76109 ref = { inputRef }
77110 type = "text"
78111 value = { query }
79112 onChange = { handleChange }
80113 onKeyDown = { handleKeyDown }
81114 onFocus = { ( ) => query . length >= 2 && setOpen ( true ) }
82- placeholder = "Search... (⌘K)"
115+ placeholder = "Search ERP ... (⌘K)"
83116 className = "w-full pl-9 pr-3 py-1.5 text-sm rounded-lg border border-slate-200 bg-slate-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
84117 />
85- { loading && < div className = "absolute right-3 top-1/2 -translate-y-1/2 h-3 w-3 border border-slate-400 border-t-transparent rounded-full animate-spin" /> }
118+ { loading && (
119+ < div className = "absolute right-3 top-1/2 -translate-y-1/2 h-3 w-3 border border-slate-400 border-t-transparent rounded-full animate-spin" />
120+ ) }
86121 </ div >
87122 { open && results . length > 0 && (
88123 < div className = "absolute top-full mt-1 left-0 right-0 bg-white border border-slate-200 rounded-lg shadow-lg z-50 max-h-80 overflow-y-auto" >
89124 { results . map ( ( r , i ) => (
90- < a key = { `${ r . type } -${ r . id } ` } href = { r . url }
91- className = { `flex items-start gap-3 px-3 py-2 hover:bg-slate-50 cursor-pointer ${ i === activeIndex ? 'bg-slate-50' : '' } ` }
92- onClick = { ( ) => setOpen ( false ) } >
93- < span className = { `mt-0.5 shrink-0 inline-flex items-center rounded-full px-1.5 py-0.5 text-xs font-medium ${ TYPE_COLORS [ r . type ] ?? 'bg-slate-100 text-slate-600' } ` } > { r . type } </ span >
125+ < a
126+ key = { `${ r . module } -${ r . id } ` }
127+ href = { r . url }
128+ className = { `flex items-start gap-3 px-3 py-2 hover:bg-slate-50 cursor-pointer ${
129+ i === activeIndex ? 'bg-slate-50' : ''
130+ } `}
131+ onClick = { ( ) => setOpen ( false ) }
132+ >
133+ < span
134+ className = { `mt-0.5 shrink-0 inline-flex items-center rounded-full px-1.5 py-0.5 text-xs font-medium capitalize ${
135+ MODULE_COLORS [ r . module ] ??
136+ 'bg-slate-100 text-slate-600'
137+ } `}
138+ >
139+ { r . module . replace ( '_' , ' ' ) }
140+ </ span >
94141 < div className = "min-w-0" >
95- < p className = "text-sm font-medium text-slate-900 truncate" > { r . title } </ p >
96- { r . subtitle && < p className = "text-xs text-slate-500 truncate" > { r . subtitle } </ p > }
142+ < p className = "text-sm font-medium text-slate-900 truncate" >
143+ { r . title }
144+ </ p >
145+ { r . subtitle && (
146+ < p className = "text-xs text-slate-500 truncate" >
147+ { r . subtitle }
148+ </ p >
149+ ) }
97150 </ div >
98151 </ a >
99152 ) ) }
0 commit comments