1- import { ReactNode , useState } from 'react'
1+ import { ReactNode , useState , useCallback , useRef } from 'react'
22import { Link , usePage , router , Form } from '@inertiajs/react'
33import { Menu , MenuButton , MenuItems , MenuItem } from '@headlessui/react'
44import {
55 Bars3Icon ,
66 MagnifyingGlassIcon ,
77 ChevronDownIcon
88} from '@heroicons/react/24/outline'
9- import { User } from '@/types'
9+
10+ import { Topic } from '../types'
1011import Sidebar from "@/components/Sidebar" ;
12+ import { debounce } from '../utils/debounce'
1113
1214interface AppLayoutProps {
1315 children : ReactNode
1416}
1517
1618export default function AppLayout ( { children } : AppLayoutProps ) {
1719 const [ sidebarOpen , setSidebarOpen ] = useState ( true )
18- const { props : { current_user : currentUser } } = usePage ( )
20+ const {
21+ props : {
22+ current_user : currentUser ,
23+ search_results : searchResults
24+ } ,
25+ } = usePage ( )
26+
27+ const { topics, q } = searchResults
28+ const { username } = currentUser || { }
29+
30+ const debouncedSearch = useCallback (
31+ debounce ( ( q : string ) => {
32+ router . reload ( { only : [ 'search_results' ] , data : { q } , preserveUrl : true } )
33+ } , 500 ) ,
34+ [ ]
35+ )
1936
20- const { username} = currentUser || { } as User
37+ const handleSearchChange = ( e : React . ChangeEvent < HTMLInputElement > ) => {
38+ const value = e . target . value
39+ debouncedSearch ( value )
40+ }
41+
42+ const inputRef = useRef < HTMLInputElement > ( null )
43+
44+ const handleEscapeKey = ( e : React . KeyboardEvent < HTMLInputElement > ) => {
45+ if ( e . key === 'Escape' ) {
46+ inputRef . current ?. blur ( )
47+ }
48+ }
2149
2250 return (
2351 < div className = "flex h-screen bg-gray-50" >
@@ -32,8 +60,12 @@ export default function AppLayout({ children }: AppLayoutProps) {
3260 >
3361 < Bars3Icon className = "h-6 w-6" />
3462 </ button >
35- < div className = "flex-1 max-w-lg" >
36- < Form action = "/search" method = "get" >
63+ < div className = "group flex-1 max-w-lg relative" >
64+ < Form
65+ action = "/search"
66+ method = "get"
67+ resetOnSuccess
68+ >
3769 < div className = "relative" >
3870 < div className = "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none" >
3971 < MagnifyingGlassIcon className = "h-5 w-5 text-gray-400" />
@@ -42,10 +74,38 @@ export default function AppLayout({ children }: AppLayoutProps) {
4274 type = "text"
4375 name = "query"
4476 placeholder = "Search"
77+ onChange = { handleSearchChange }
78+ onKeyDown = { handleEscapeKey }
79+ ref = { inputRef }
80+ defaultValue = { q }
4581 className = "block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-sky-500 focus:border-sky-500 sm:text-sm"
4682 />
4783 </ div >
4884 </ Form >
85+
86+ < div className = "absolute z-50 mt-1 w-full rounded-md bg-white shadow-lg ring-1 ring-black/5 max-h-96 overflow-y-auto hidden group-focus-within:block" >
87+ < div className = "py-1" >
88+ { topics && topics . map ( ( topic : Topic ) => (
89+ < Link
90+ key = { topic . id }
91+ href = { `/topics/${ topic . id } ` }
92+ className = "block px-4 py-3 hover:bg-gray-50 transition-colors"
93+ >
94+ < div className = "text-sm font-medium text-sky-700" >
95+ { topic . title }
96+ </ div >
97+ < div className = "text-xs text-gray-500 mt-1" >
98+ { topic . category . name }
99+ </ div >
100+ </ Link >
101+ ) ) }
102+ { topics . length === 0 && (
103+ < div className = "block px-4 py-3 text-sm text-gray-500" >
104+ No results found
105+ </ div >
106+ ) }
107+ </ div >
108+ </ div >
49109 </ div >
50110 </ div >
51111
0 commit comments