Skip to content

Commit 7f8ce9e

Browse files
feat: create directory of resources to make claude code experience better
1 parent 661aa10 commit 7f8ce9e

File tree

3 files changed

+255
-0
lines changed

3 files changed

+255
-0
lines changed

app/_meta.global.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ const meta = {
2828
},
2929
}
3030
},
31+
tools: {
32+
type: 'page',
33+
title: 'Tools',
34+
href: '/tools/'
35+
},
3136
webapp: {
3237
href: 'https://app.happy.engineering',
3338
title: 'Web App',
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
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+
}

app/tools/[[...category]]/page.tsx

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { Metadata } from 'next'
2+
import ToolsClient from './ToolsClient'
3+
4+
const TOOL_CATEGORIES = [
5+
'mcp',
6+
'agents',
7+
'commands',
8+
'settings',
9+
'hooks',
10+
'templates'
11+
] as const
12+
13+
type ToolCategory = typeof TOOL_CATEGORIES[number]
14+
15+
// Static generation for SEO - these create the entry point routes
16+
export async function generateStaticParams() {
17+
return [
18+
{ category: [] },
19+
...TOOL_CATEGORIES.map(cat => ({ category: [cat] }))
20+
]
21+
}
22+
23+
export async function generateMetadata(props: {
24+
params: Promise<{ category?: string[] }>
25+
}): Promise<Metadata> {
26+
const params = await props.params
27+
const category = params.category?.[0]
28+
29+
if (!category) {
30+
return {
31+
title: 'Claude Code Tools & Resources',
32+
description: 'Discover tools, agents, MCP servers, and resources for Claude Code'
33+
}
34+
}
35+
36+
const categoryTitles: Record<ToolCategory, string> = {
37+
'mcp': 'MCP Servers',
38+
'agents': 'Agents',
39+
'commands': 'Commands',
40+
'settings': 'Settings',
41+
'hooks': 'Hooks',
42+
'templates': 'Templates'
43+
}
44+
45+
const title = categoryTitles[category as ToolCategory] || 'Tools'
46+
47+
return {
48+
title: `${title} | Claude Code Tools`,
49+
description: `Browse ${title.toLowerCase()} for Claude Code`
50+
}
51+
}
52+
53+
interface ToolsPageProps {
54+
params: Promise<{ category?: string[] }>
55+
}
56+
57+
export default async function ToolsPage({ params }: ToolsPageProps) {
58+
const resolvedParams = await params
59+
const initialCategory = resolvedParams.category?.[0] as ToolCategory | undefined
60+
61+
return <ToolsClient initialCategory={initialCategory || null} />
62+
}

0 commit comments

Comments
 (0)