feat(search): migrate to backend endpoint

This commit is contained in:
zetaloop
2026-02-28 12:17:52 +08:00
parent db02313801
commit 8463e9ea1c
3 changed files with 18 additions and 124 deletions
-62
View File
@@ -1,62 +0,0 @@
import { mockPlayers, mockServices, mockShops } from "@/lib/mock"
import { searchCatalog } from "@/lib/search/search-catalog"
import type { SearchSort } from "@/lib/search/types"
import { NextResponse } from "next/server"
export const dynamic = "force-dynamic"
const SEARCH_SORTS: ReadonlySet<SearchSort> = new Set([
"composite",
"rating",
"orders",
"price_asc",
"price_desc",
])
function numberParam(value: string | null, fallback: number) {
if (value === null) return fallback
const parsed = Number(value)
return Number.isFinite(parsed) ? parsed : fallback
}
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const q = searchParams.get("q") ?? undefined
const selectedGames = searchParams.getAll("game")
const min = searchParams.get("min") ?? ""
const max = searchParams.get("max") ?? ""
const onlyOnline = (searchParams.get("online") ?? "0") === "1"
const minRating = searchParams.get("minRating") ?? "0"
const sortParam = (searchParams.get("sort") ?? "composite") as SearchSort
const sort: SearchSort = SEARCH_SORTS.has(sortParam) ? sortParam : "composite"
const limit = numberParam(searchParams.get("limit"), 12)
const offset = numberParam(searchParams.get("offset"), 0)
const response = searchCatalog(
{
q,
selectedGames,
min,
max,
onlyOnline,
minRating,
sort,
limit,
offset,
},
{
players: mockPlayers,
shops: mockShops,
services: mockServices,
},
)
return NextResponse.json(response, {
headers: {
"Cache-Control": "no-store",
},
})
}
+18 -11
View File
@@ -1,5 +1,8 @@
import { isApiError } from "@/lib/errors"
import type { SearchResponse, SearchSort } from "@/lib/search/types"
import { httpJson } from "./http"
export interface SearchCatalogParams {
q?: string
selectedGames?: string[]
@@ -19,26 +22,30 @@ export async function searchCatalog(params: SearchCatalogParams): Promise<Search
if (params.q) searchParams.set("q", params.q)
for (const game of params.selectedGames ?? []) {
searchParams.append("game", game)
searchParams.append("selectedGames", game)
}
if (params.min) searchParams.set("min", params.min)
if (params.max) searchParams.set("max", params.max)
if (params.onlyOnline) searchParams.set("online", "1")
if (params.onlyOnline !== undefined) searchParams.set("onlyOnline", String(params.onlyOnline))
if (params.minRating && params.minRating !== "0") searchParams.set("minRating", params.minRating)
if (params.sort && params.sort !== "composite") searchParams.set("sort", params.sort)
if (params.limit !== undefined) searchParams.set("limit", String(params.limit))
if (params.offset !== undefined) searchParams.set("offset", String(params.offset))
const res = await fetch(`/api/search?${searchParams.toString()}`, {
cache: "no-store",
signal: params.signal,
})
if (!res.ok) {
throw new Error(`Search API request failed: ${res.status} ${res.statusText}`)
try {
return await httpJson<SearchResponse>(`/api/v1/search?${searchParams.toString()}`, {
cache: "no-store",
signal: params.signal,
})
} catch (error) {
if (error instanceof Error && error.message === "UNAUTHORIZED") {
throw error
}
if (isApiError(error)) {
throw new Error(`Search API request failed: ${error.code} ${error.msg}`)
}
throw error
}
return (await res.json()) as SearchResponse
}
-51
View File
@@ -1,51 +0,0 @@
import { GET } from "@/app/api/search/route"
import { describe, expect, it } from "vitest"
describe("GET /api/search", () => {
it("returns 200 with items and meta", async () => {
const request = new Request("http://x/api/search")
const response = await GET(request)
expect(response.status).toBe(200)
const json = await response.json()
expect(Array.isArray(json.items)).toBe(true)
expect(json.meta).toHaveProperty("total")
expect(json.meta).toHaveProperty("offset")
expect(json.meta).toHaveProperty("limit")
})
it("sets Cache-Control to no-store", async () => {
const request = new Request("http://x/api/search")
const response = await GET(request)
expect(response.headers.get("Cache-Control")).toBe("no-store")
})
it("filters by q param", async () => {
const request = new Request("http://x/api/search?q=winter")
const response = await GET(request)
const json = await response.json()
expect(response.status).toBe(200)
expect(json.items.length).toBeGreaterThan(0)
})
it("respects limit and offset", async () => {
const request = new Request("http://x/api/search?limit=2&offset=1")
const response = await GET(request)
const json = await response.json()
expect(json.items.length).toBeLessThanOrEqual(2)
expect(json.meta.limit).toBe(2)
expect(json.meta.offset).toBe(1)
})
it("handles game filter", async () => {
const request = new Request("http://x/api/search?game=CS2")
const response = await GET(request)
const json = await response.json()
expect(response.status).toBe(200)
expect(json.items.length).toBeGreaterThan(0)
})
})