1- import { createFileRoute , Link } from '@tanstack/react-router'
2- import { useQuery } from 'convex/react'
1+ import { createFileRoute , Link , useNavigate } from '@tanstack/react-router'
2+ import { useAction , useQuery } from 'convex/react'
3+ import { useEffect , useMemo , useRef , useState } from 'react'
34import { api } from '../../convex/_generated/api'
45import type { Doc } from '../../convex/_generated/dataModel'
56import { InstallSwitcher } from '../components/InstallSwitcher'
67import { SkillCard } from '../components/SkillCard'
78
89export const Route = createFileRoute ( '/' ) ( {
10+ validateSearch : ( search ) => ( {
11+ q : typeof search . q === 'string' ? search . q : '' ,
12+ highlighted : search . highlighted === '1' || search . highlighted === 'true' ,
13+ } ) ,
914 component : Home ,
1015} )
1116
1217function Home ( ) {
18+ const navigate = useNavigate ( )
19+ const search = Route . useSearch ( )
20+ const searchSkills = useAction ( api . search . searchSkills )
1321 const highlighted =
1422 ( useQuery ( api . skills . list , { batch : 'highlighted' , limit : 6 } ) as Doc < 'skills' > [ ] ) ?? [ ]
1523 const latest = ( useQuery ( api . skills . list , { limit : 12 } ) as Doc < 'skills' > [ ] ) ?? [ ]
24+ const [ query , setQuery ] = useState ( search . q ?? '' )
25+ const [ highlightedOnly , setHighlightedOnly ] = useState ( search . highlighted ?? false )
26+ const [ results , setResults ] = useState <
27+ Array < { skill : Doc < 'skills' > ; version : Doc < 'skillVersions' > | null ; score : number } >
28+ > ( [ ] )
29+ const [ isSearching , setIsSearching ] = useState ( false )
30+ const [ searchMode , setSearchMode ] = useState ( Boolean ( search . q || search . highlighted ) )
31+ const searchRequest = useRef ( 0 )
32+ const inputRef = useRef < HTMLInputElement | null > ( null )
33+ const trimmedQuery = useMemo ( ( ) => query . trim ( ) , [ query ] )
34+ const hasQuery = trimmedQuery . length > 0
35+
36+ useEffect ( ( ) => {
37+ setQuery ( search . q ?? '' )
38+ setHighlightedOnly ( search . highlighted ?? false )
39+ if ( search . q || search . highlighted ) {
40+ setSearchMode ( true )
41+ }
42+ } , [ search . highlighted , search . q ] )
43+
44+ useEffect ( ( ) => {
45+ void navigate ( {
46+ search : ( prev ) => ( {
47+ ...prev ,
48+ q : trimmedQuery || undefined ,
49+ highlighted : highlightedOnly ? '1' : undefined ,
50+ } ) ,
51+ replace : true ,
52+ } )
53+ } , [ highlightedOnly , navigate , trimmedQuery ] )
54+
55+ useEffect ( ( ) => {
56+ if ( ! trimmedQuery ) {
57+ setResults ( [ ] )
58+ setIsSearching ( false )
59+ return
60+ }
61+ const requestId = ( searchRequest . current += 1 )
62+ setIsSearching ( true )
63+ const handle = window . setTimeout ( ( ) => {
64+ void ( async ( ) => {
65+ try {
66+ const data = ( await searchSkills ( { query : trimmedQuery , highlightedOnly } ) ) as Array < {
67+ skill : Doc < 'skills' >
68+ version : Doc < 'skillVersions' > | null
69+ score : number
70+ } >
71+ if ( requestId === searchRequest . current ) {
72+ setResults ( data )
73+ }
74+ } finally {
75+ if ( requestId === searchRequest . current ) {
76+ setIsSearching ( false )
77+ }
78+ }
79+ } ) ( )
80+ } , 220 )
81+ return ( ) => window . clearTimeout ( handle )
82+ } , [ highlightedOnly , searchSkills , trimmedQuery ] )
1683
1784 return (
1885 < main className = "app-shell" >
19- < section className = " hero" >
86+ < section className = { ` hero${ searchMode ? ' search-mode' : '' } ` } >
2087 < div className = "hero-inner" >
21- < div className = "fade-up" data-delay = "1" >
88+ < div className = "hero-copy fade-up" data-delay = "1" >
2289 < span className = "hero-badge" > Lobster-light. Agent-right.</ span >
2390 < h1 className = "hero-title" > ClawdHub, the skill dock for sharp agents.</ h1 >
2491 < p className = "hero-subtitle" >
@@ -34,72 +101,136 @@ function Home() {
34101 </ Link >
35102 </ div >
36103 </ div >
37- < div className = "hero-card fade-up" data-delay = "2" >
38- < div className = "search-bar" >
104+ < div className = "hero-card hero-search-card fade-up" data-delay = "2" >
105+ < form
106+ className = "search-bar"
107+ onSubmit = { ( event ) => {
108+ event . preventDefault ( )
109+ if ( ! searchMode ) setSearchMode ( true )
110+ inputRef . current ?. focus ( )
111+ } }
112+ >
39113 < span className = "mono" > /</ span >
40114 < input
115+ ref = { inputRef }
41116 className = "search-input"
42117 placeholder = "Search skills, tags, or capabilities"
43- disabled
118+ value = { query }
119+ onChange = { ( event ) => setQuery ( event . target . value ) }
120+ onFocus = { ( ) => setSearchMode ( true ) }
121+ onKeyDown = { ( event ) => {
122+ if ( event . key === 'Escape' && ! trimmedQuery ) {
123+ setSearchMode ( false )
124+ inputRef . current ?. blur ( )
125+ }
126+ } }
44127 />
45- < Link to = "/search" className = "btn" >
46- Search
47- </ Link >
48- </ div >
49- < div className = "hero-install" style = { { marginTop : 18 } } >
50- < div className = "stat" > Search skills. Versioned, rollback-ready.</ div >
51- < InstallSwitcher exampleSlug = "sonoscli" />
52- </ div >
128+ < button
129+ className = "search-filter-button"
130+ type = "button"
131+ aria-pressed = { highlightedOnly }
132+ onClick = { ( ) => {
133+ setHighlightedOnly ( ( value ) => ! value )
134+ setSearchMode ( true )
135+ } }
136+ >
137+ Highlighted
138+ </ button >
139+ </ form >
140+ { ! searchMode ? (
141+ < div className = "hero-install" style = { { marginTop : 18 } } >
142+ < div className = "stat" > Search skills. Versioned, rollback-ready.</ div >
143+ < InstallSwitcher exampleSlug = "sonoscli" />
144+ </ div >
145+ ) : null }
53146 </ div >
54147 </ div >
55148 </ section >
56149
57- < section className = "section" >
58- < h2 className = "section-title" > Highlighted batch</ h2 >
59- < p className = "section-subtitle" > Curated signal — highlighted for quick trust.</ p >
60- < div className = "grid" >
61- { highlighted . length === 0 ? (
62- < div className = "card" > No highlighted skills yet.</ div >
63- ) : (
64- highlighted . map ( ( skill ) => (
65- < SkillCard
66- key = { skill . _id }
67- skill = { skill }
68- badge = "Highlighted"
69- summaryFallback = "A fresh skill bundle."
70- meta = {
71- < div className = "stat" >
72- ⭐ { skill . stats . stars } · ⤓ { skill . stats . downloads }
73- </ div >
74- }
75- />
76- ) )
77- ) }
78- </ div >
79- </ section >
150+ { searchMode ? (
151+ < section className = "section" >
152+ < h2 className = "section-title" > Search results</ h2 >
153+ < p className = "section-subtitle" >
154+ { isSearching ? 'Searching now.' : 'Instant results as you type.' }
155+ </ p >
156+ < div className = "grid" >
157+ { ! hasQuery ? (
158+ < div className = "card" > Start typing to search.</ div >
159+ ) : results . length === 0 ? (
160+ < div className = "card" > No results yet. Try a different prompt.</ div >
161+ ) : (
162+ results . map ( ( result ) => (
163+ < Link
164+ key = { result . skill . _id }
165+ to = "/skills/$slug"
166+ params = { { slug : result . skill . slug } }
167+ className = "card"
168+ >
169+ < div className = "tag" > Score { ( result . score ?? 0 ) . toFixed ( 2 ) } </ div >
170+ < h3 className = "section-title" style = { { fontSize : '1.2rem' , margin : 0 } } >
171+ { result . skill . displayName }
172+ </ h3 >
173+ < p className = "section-subtitle" style = { { margin : 0 } } >
174+ { result . skill . summary ?? 'Skill pack' }
175+ </ p >
176+ { result . skill . batch === 'highlighted' ? (
177+ < div className = "tag" > Highlighted</ div >
178+ ) : null }
179+ </ Link >
180+ ) )
181+ ) }
182+ </ div >
183+ </ section >
184+ ) : (
185+ < >
186+ < section className = "section" >
187+ < h2 className = "section-title" > Highlighted batch</ h2 >
188+ < p className = "section-subtitle" > Curated signal — highlighted for quick trust.</ p >
189+ < div className = "grid" >
190+ { highlighted . length === 0 ? (
191+ < div className = "card" > No highlighted skills yet.</ div >
192+ ) : (
193+ highlighted . map ( ( skill ) => (
194+ < SkillCard
195+ key = { skill . _id }
196+ skill = { skill }
197+ badge = "Highlighted"
198+ summaryFallback = "A fresh skill bundle."
199+ meta = {
200+ < div className = "stat" >
201+ ⭐ { skill . stats . stars } · ⤓ { skill . stats . downloads }
202+ </ div >
203+ }
204+ />
205+ ) )
206+ ) }
207+ </ div >
208+ </ section >
80209
81- < section className = "section" >
82- < h2 className = "section-title" > Latest drops</ h2 >
83- < p className = "section-subtitle" > Newest uploads across the registry.</ p >
84- < div className = "grid" >
85- { latest . length === 0 ? (
86- < div className = "card" > No skills yet. Be the first.</ div >
87- ) : (
88- latest . map ( ( skill ) => (
89- < SkillCard
90- key = { skill . _id }
91- skill = { skill }
92- summaryFallback = "Agent-ready skill pack."
93- meta = {
94- < div className = "stat" >
95- { skill . stats . versions } versions · { skill . stats . downloads } downloads
96- </ div >
97- }
98- />
99- ) )
100- ) }
101- </ div >
102- </ section >
210+ < section className = "section" >
211+ < h2 className = "section-title" > Latest drops</ h2 >
212+ < p className = "section-subtitle" > Newest uploads across the registry.</ p >
213+ < div className = "grid" >
214+ { latest . length === 0 ? (
215+ < div className = "card" > No skills yet. Be the first.</ div >
216+ ) : (
217+ latest . map ( ( skill ) => (
218+ < SkillCard
219+ key = { skill . _id }
220+ skill = { skill }
221+ summaryFallback = "Agent-ready skill pack."
222+ meta = {
223+ < div className = "stat" >
224+ { skill . stats . versions } versions · { skill . stats . downloads } downloads
225+ </ div >
226+ }
227+ />
228+ ) )
229+ ) }
230+ </ div >
231+ </ section >
232+ </ >
233+ ) }
103234 </ main >
104235 )
105236}
0 commit comments