1+ 'use client'
2+
3+ import { useRouter , useSearchParams } from 'next/navigation'
4+ import { useEffect , useState } from 'react'
5+
6+ /*
7+ * Faceted Search URL Strategy:
8+ * - SEO Entry Points: Clean URLs like /tools/agents for crawlers and direct links
9+ * - Interactive State: Once user interacts with ANY facet, switch to /tools?category=X&q=Y
10+ *
11+ * This gives us:
12+ * - Clean URLs for SEO (/tools/mcp-servers, /tools/agents)
13+ * - Faceted search state in query params after interaction (/tools?category=hooks&q=search)
14+ * - Category treated as just another search facet alongside query
15+ * - Extensible for future facets (tags, license, etc.)
16+ */
17+
18+ const TOOL_CATEGORIES = [
19+ 'mcp' ,
20+ 'agents' ,
21+ 'commands' ,
22+ 'settings' ,
23+ 'hooks' ,
24+ 'templates'
25+ ] as const
26+
27+ type ToolCategory = typeof TOOL_CATEGORIES [ number ]
28+
29+ interface ToolsClientProps {
30+ initialCategory ?: ToolCategory | null
31+ }
32+
33+ export default function ToolsClient ( { initialCategory } : ToolsClientProps ) {
34+ const router = useRouter ( )
35+ const searchParams = useSearchParams ( )
36+ const [ currentCategory , setCurrentCategory ] = useState < ToolCategory | null > ( initialCategory || null )
37+ const [ searchQuery , setSearchQuery ] = useState ( '' )
38+ const [ mounted , setMounted ] = useState ( false )
39+ const [ hasInteracted , setHasInteracted ] = useState ( false )
40+
41+ const categoryDisplayNames : Record < ToolCategory , string > = {
42+ 'mcp' : 'MCP Servers' ,
43+ 'agents' : 'Agents' ,
44+ 'commands' : 'Commands' ,
45+ 'settings' : 'Settings' ,
46+ 'hooks' : 'Hooks' ,
47+ 'templates' : 'Templates'
48+ }
49+
50+ // Initialize from URL params after mount
51+ useEffect ( ( ) => {
52+ const queryCategory = searchParams . get ( 'category' ) as ToolCategory | null
53+ const querySearch = searchParams . get ( 'q' ) || ''
54+
55+ // If we have query params, we're in interactive mode
56+ if ( queryCategory || querySearch ) {
57+ setHasInteracted ( true )
58+ setCurrentCategory ( queryCategory || null )
59+ setSearchQuery ( querySearch )
60+ } else {
61+ // Use initial category from path, not yet interactive
62+ setCurrentCategory ( initialCategory || null )
63+ setSearchQuery ( '' )
64+ }
65+
66+ setMounted ( true )
67+ } , [ initialCategory , searchParams ] )
68+
69+ // Handle category changes - switches to faceted search mode
70+ const handleCategoryChange = ( category : ToolCategory | null ) => {
71+ setCurrentCategory ( category )
72+ setHasInteracted ( true ) // Mark as interactive - switch to query param URLs
73+
74+ const newParams = new URLSearchParams ( )
75+ if ( category ) newParams . set ( 'category' , category )
76+ if ( searchQuery ) newParams . set ( 'q' , searchQuery )
77+
78+ const queryString = newParams . toString ( )
79+ const newUrl = queryString ? `/tools?${ queryString } ` : '/tools'
80+
81+ router . push ( newUrl , { scroll : false } )
82+ }
83+
84+ // Handle search changes - switches to faceted search mode
85+ const handleSearchChange = ( query : string ) => {
86+ setSearchQuery ( query )
87+ setHasInteracted ( true ) // Mark as interactive - switch to query param URLs
88+
89+ const newParams = new URLSearchParams ( )
90+ if ( currentCategory ) newParams . set ( 'category' , currentCategory )
91+ if ( query ) newParams . set ( 'q' , query )
92+
93+ const queryString = newParams . toString ( )
94+ const newUrl = queryString ? `/tools?${ queryString } ` : '/tools'
95+
96+ router . push ( newUrl , { scroll : false } )
97+ }
98+
99+ // Don't render until mounted to avoid hydration mismatch
100+ if ( ! mounted ) {
101+ return < div > Loading...</ div >
102+ }
103+
104+ return (
105+ < div className = "max-w-6xl mx-auto px-4 py-8" >
106+ < div className = "mb-8" >
107+ < h1 className = "text-3xl font-bold mb-4" >
108+ Claude Code Tools & Resources
109+ </ h1 >
110+ < p className = "text-lg text-gray-600 dark:text-gray-400" >
111+ Discover tools, agents, MCP servers, and resources to enhance your Claude Code experience
112+ </ p >
113+ </ div >
114+
115+ { /* Category Filter Tabs - Client-side navigation */ }
116+ < div className = "mb-8" >
117+ < div className = "border-b border-gray-200 dark:border-gray-700" >
118+ < nav className = "-mb-px flex space-x-8" >
119+ < button
120+ onClick = { ( ) => handleCategoryChange ( null ) }
121+ className = { `py-2 px-1 border-b-2 font-medium text-sm ${
122+ ! currentCategory
123+ ? 'border-blue-500 text-blue-600 dark:text-blue-400'
124+ : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
125+ } `}
126+ >
127+ All
128+ </ button >
129+ { TOOL_CATEGORIES . map ( ( category ) => (
130+ < button
131+ key = { category }
132+ onClick = { ( ) => handleCategoryChange ( category ) }
133+ className = { `py-2 px-1 border-b-2 font-medium text-sm ${
134+ currentCategory === category
135+ ? 'border-blue-500 text-blue-600 dark:text-blue-400'
136+ : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
137+ } `}
138+ >
139+ { categoryDisplayNames [ category ] }
140+ </ button >
141+ ) ) }
142+ </ nav >
143+ </ div >
144+ </ div >
145+
146+ { /* Search Bar */ }
147+ < div className = "mb-8" >
148+ < div className = "relative" >
149+ < div className = "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none" >
150+ < svg className = "h-5 w-5 text-gray-400" fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" >
151+ < path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
152+ </ svg >
153+ </ div >
154+ < input
155+ type = "text"
156+ value = { searchQuery }
157+ onChange = { ( e ) => handleSearchChange ( e . target . value ) }
158+ className = "block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md leading-5 bg-white dark:bg-gray-800 dark:border-gray-600 placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
159+ placeholder = { `Search ${ currentCategory ? categoryDisplayNames [ currentCategory ] . toLowerCase ( ) : 'all tools' } ...` }
160+ />
161+ </ div >
162+ </ div >
163+
164+ { /* Content Area */ }
165+ < div className = "bg-gray-50 dark:bg-gray-800 rounded-lg p-8 text-center" >
166+ < h2 className = "text-xl font-semibold mb-4" >
167+ { currentCategory ? `${ categoryDisplayNames [ currentCategory ] } Coming Soon` : 'Tools Coming Soon' }
168+ </ h2 >
169+ < p className = "text-gray-600 dark:text-gray-400 mb-4" >
170+ { currentCategory
171+ ? `We're building a comprehensive directory of ${ categoryDisplayNames [ currentCategory ] . toLowerCase ( ) } for Claude Code.`
172+ : "We're building a comprehensive directory of tools and resources for Claude Code."
173+ }
174+ </ p >
175+ { currentCategory === 'mcp' && (
176+ < p className = "text-sm text-gray-500 dark:text-gray-500" >
177+ This will showcase MCP servers similar to the design you provided, with search, filtering, and easy installation.
178+ </ p >
179+ ) }
180+ { searchQuery && (
181+ < p className = "text-sm text-gray-500 dark:text-gray-500 mt-2" >
182+ Searching for: "{ searchQuery } "
183+ </ p >
184+ ) }
185+ </ div >
186+ </ div >
187+ )
188+ }
0 commit comments