Skip to content

Commit 12fea4d

Browse files
authored
Search v2 (#73)
1 parent dadb297 commit 12fea4d

File tree

9 files changed

+766
-33
lines changed

9 files changed

+766
-33
lines changed

app/api/search/route.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { NextRequest } from 'next/server'
2+
import { searchQueries } from '@/lib/db/queries'
3+
import type { ApiResponse } from '@/lib/types'
4+
import { checkRateLimit, getRateLimitIdentifier, createRateLimitResponse } from '@/lib/rate-limit'
5+
6+
export async function GET(request: NextRequest) {
7+
try {
8+
// Rate limiting for search operations
9+
const identifier = await getRateLimitIdentifier(request)
10+
const rateLimitResult = await checkRateLimit('search', identifier)
11+
12+
if (!rateLimitResult.success) {
13+
return createRateLimitResponse(
14+
rateLimitResult.remaining || 0,
15+
rateLimitResult.reset || new Date(Date.now() + 3600000)
16+
)
17+
}
18+
19+
const { searchParams } = new URL(request.url)
20+
const q = searchParams.get('q')?.trim()
21+
const type = searchParams.get('type') // 'all', 'markets', 'events', 'tags'
22+
const limit = Math.min(parseInt(searchParams.get('limit') || '10'), 50)
23+
24+
// Market-specific filters
25+
const sort = searchParams.get('sort') as 'trending' | 'liquidity' | 'volume' | 'newest' | 'ending' | 'competitive' | null
26+
const status = searchParams.get('status') as 'active' | 'resolved' | 'all' | null
27+
const cursor = searchParams.get('cursor')
28+
29+
if (!q || q.length < 1) {
30+
return new Response(
31+
JSON.stringify({
32+
success: true,
33+
data: {
34+
markets: [],
35+
events: [],
36+
tags: [],
37+
totalResults: 0,
38+
suggestions: []
39+
}
40+
} as ApiResponse),
41+
{ headers: { 'Content-Type': 'application/json' } }
42+
)
43+
}
44+
45+
// Determine what to search based on type parameter
46+
const includeMarkets = type === 'all' || type === 'markets' || !type
47+
const includeEvents = type === 'all' || type === 'events' || !type
48+
const includeTags = type === 'all' || type === 'tags' || !type
49+
50+
// Search with unified query
51+
const results = await searchQueries.searchAll(q, {
52+
includeMarkets,
53+
includeEvents,
54+
includeTags,
55+
limit,
56+
marketOptions: {
57+
sort: sort || 'trending',
58+
status: status || 'active',
59+
cursorId: cursor,
60+
limit
61+
}
62+
})
63+
64+
return new Response(
65+
JSON.stringify({ success: true, data: results } as ApiResponse),
66+
{ headers: { 'Content-Type': 'application/json' } }
67+
)
68+
} catch (error) {
69+
console.error('Search API error:', error)
70+
return new Response(
71+
JSON.stringify({ success: false, error: 'Internal server error' } as ApiResponse),
72+
{ status: 500, headers: { 'Content-Type': 'application/json' } }
73+
)
74+
}
75+
}

app/search/page.tsx

Lines changed: 139 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ import Link from "next/link"
22
import { Suspense } from "react"
33
import { Button } from "@/components/ui/button"
44
import { Input } from "@/components/ui/input"
5-
import { Card, CardContent } from "@/components/ui/card"
5+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
66
import { Separator } from "@/components/ui/separator"
7-
import { Search } from "lucide-react"
8-
import { marketQueries } from "@/lib/db/queries"
7+
import { Badge } from "@/components/ui/badge"
8+
import { Search, TrendingUp, Calendar, Tag } from "lucide-react"
9+
import { searchQueries, marketQueries } from "@/lib/db/queries"
910
import MarketDetailsCard from "@/components/market-details-card"
1011
import { generateMarketURL } from "@/lib/utils"
11-
import { SearchBox } from "@/components/search-box"
12+
import { SearchInput } from "@/components/search-input"
1213

1314
interface SearchPageProps {
1415
searchParams: Promise<{ q?: string; cursor?: string; sort?: string; status?: string }>
@@ -17,54 +18,159 @@ interface SearchPageProps {
1718
export default async function SearchPage({ searchParams }: SearchPageProps) {
1819
const { q, cursor, sort = 'trending', status = 'active' } = await searchParams
1920
const query = (q || "").trim()
20-
const { items, nextCursor } = query
21-
? await marketQueries.searchMarkets(query, {
21+
22+
// Use unified search for all entity types
23+
const results = query
24+
? await searchQueries.searchAll(query, {
25+
includeMarkets: true,
26+
includeEvents: true,
27+
includeTags: true,
2228
limit: 20,
23-
sort: (sort as any) ?? 'trending',
24-
status: (status as any) ?? 'active',
25-
cursorId: cursor ?? null,
29+
marketOptions: {
30+
sort: (sort as any) ?? 'trending',
31+
status: (status as any) ?? 'active',
32+
cursorId: cursor ?? null,
33+
limit: 20
34+
}
2635
})
27-
: { items: [], nextCursor: null }
36+
: { markets: [], events: [], tags: [], totalResults: 0, suggestions: [] }
2837

2938
const marketsWithUrl = await Promise.all(
30-
items.map(async (m) => ({ market: m, url: await generateMarketURL(m.id) }))
39+
results.markets.map(async (m) => ({ market: m, url: await generateMarketURL(m.id) }))
3140
)
3241

3342
return (
3443
<div className="min-h-screen bg-background">
3544
<div className="container mx-auto px-4 py-6">
3645
<div className="mb-6">
3746
<h1 className="text-2xl font-bold text-foreground mb-2">
38-
{`Showing ${marketsWithUrl.length} result${marketsWithUrl.length === 1 ? "" : "s"}${query ? ` for ${query}` : ""}`}
47+
{query ? (
48+
`${results.totalResults} result${results.totalResults === 1 ? "" : "s"} for "${query}"`
49+
) : (
50+
"Search BetterAI"
51+
)}
3952
</h1>
4053
</div>
4154

4255
<div className="flex gap-6">
43-
<div className="flex-1 space-y-4">
44-
{marketsWithUrl.map(({ market, url }) => (
45-
<MarketDetailsCard
46-
key={market.id}
47-
market={market}
48-
event={market.event}
49-
externalMarketUrl={url}
50-
latestPrediction={market.predictions?.[0] ?? null}
51-
className="hover:bg-muted/30 transition-colors"
52-
href={`/market/${market.id}`}
53-
/>
54-
))}
55-
{nextCursor && (
56-
<div className="pt-2">
57-
<Button variant="outline" asChild className="w-full">
58-
<Link href={`/search?${new URLSearchParams({ q: query, sort: String(sort), status: String(status), cursor: nextCursor }).toString()}`}>
59-
Load more
60-
</Link>
61-
</Button>
56+
<div className="flex-1 space-y-6">
57+
{/* Markets Section */}
58+
{marketsWithUrl.length > 0 && (
59+
<div>
60+
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
61+
<TrendingUp className="h-5 w-5 text-blue-500" />
62+
Markets ({marketsWithUrl.length})
63+
</h2>
64+
<div className="space-y-4">
65+
{marketsWithUrl.map(({ market, url }) => (
66+
<MarketDetailsCard
67+
key={market.id}
68+
market={market}
69+
event={market.event}
70+
externalMarketUrl={url}
71+
latestPrediction={market.predictions?.[0] ?? null}
72+
className="hover:bg-muted/30 transition-colors"
73+
href={`/market/${market.id}`}
74+
/>
75+
))}
76+
</div>
6277
</div>
6378
)}
79+
80+
{/* Events Section */}
81+
{results.events.length > 0 && (
82+
<div>
83+
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
84+
<Calendar className="h-5 w-5 text-green-500" />
85+
Events ({results.events.length})
86+
</h2>
87+
<div className="grid gap-4">
88+
{results.events.map((event) => (
89+
<Card key={event.id} className="hover:bg-muted/30 transition-colors">
90+
<CardContent className="p-4">
91+
<Link
92+
href={`/event/${event.slug}`}
93+
className="block hover:text-primary transition-colors"
94+
>
95+
<h3 className="font-medium">{event.title}</h3>
96+
{event.description && (
97+
<p className="text-sm text-muted-foreground mt-1 line-clamp-2">
98+
{event.description}
99+
</p>
100+
)}
101+
</Link>
102+
</CardContent>
103+
</Card>
104+
))}
105+
</div>
106+
</div>
107+
)}
108+
109+
{/* Tags Section */}
110+
{results.tags.length > 0 && (
111+
<div>
112+
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
113+
<Tag className="h-5 w-5 text-purple-500" />
114+
Tags ({results.tags.length})
115+
</h2>
116+
<div className="flex flex-wrap gap-2">
117+
{results.tags.map((tag) => (
118+
<Link
119+
key={tag.id}
120+
href={`/search?q=${encodeURIComponent(tag.label)}`}
121+
>
122+
<Badge
123+
variant="outline"
124+
className="hover:bg-muted transition-colors cursor-pointer"
125+
>
126+
{tag.label}
127+
{tag.eventCount && tag.eventCount > 0 && (
128+
<span className="ml-1 text-xs text-muted-foreground">
129+
({tag.eventCount})
130+
</span>
131+
)}
132+
</Badge>
133+
</Link>
134+
))}
135+
</div>
136+
</div>
137+
)}
138+
139+
{/* No Results */}
140+
{query && results.totalResults === 0 && (
141+
<Card>
142+
<CardContent className="p-6 text-center">
143+
<p className="text-muted-foreground mb-4">
144+
No results found for "{query}"
145+
</p>
146+
{results.suggestions && results.suggestions.length > 0 && (
147+
<div>
148+
<p className="text-sm text-muted-foreground mb-2">Try searching for:</p>
149+
<div className="flex flex-wrap gap-2 justify-center">
150+
{results.suggestions.map((suggestion, index) => (
151+
<Link key={index} href={`/search?q=${encodeURIComponent(suggestion)}`}>
152+
<Badge variant="secondary" className="hover:bg-muted cursor-pointer">
153+
{suggestion}
154+
</Badge>
155+
</Link>
156+
))}
157+
</div>
158+
</div>
159+
)}
160+
</CardContent>
161+
</Card>
162+
)}
163+
164+
{/* Empty State */}
64165
{!query && (
65166
<Card>
66167
<CardContent className="p-6 text-muted-foreground">
67-
Enter a search term to find markets by question, description, event title/description, or tag label.
168+
Enter a search term to find markets, events, and tags. You can search by:
169+
<ul className="list-disc list-inside mt-2 space-y-1">
170+
<li>Market questions and descriptions</li>
171+
<li>Event titles and descriptions</li>
172+
<li>Tag labels</li>
173+
</ul>
68174
</CardContent>
69175
</Card>
70176
)}
@@ -73,7 +179,7 @@ export default async function SearchPage({ searchParams }: SearchPageProps) {
73179
<div className="w-80 flex-shrink-0">
74180
<Card>
75181
<CardContent className="p-4 space-y-4">
76-
<SearchBox defaultQuery={query} sort={String(sort)} status={String(status)} />
182+
<SearchInput defaultQuery={query} sort={String(sort)} status={String(status)} />
77183
<Separator />
78184
{/* Sort options */}
79185
<div className="space-y-2">

0 commit comments

Comments
 (0)