feat(search): add api-backed filtering and sorting
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
import type { Player, PlayerService, Shop } from "@/lib/types"
|
||||
import type { SearchResponse, SearchResultItem, SearchSort } from "@/lib/search/types"
|
||||
|
||||
export interface SearchCatalogParams {
|
||||
q?: string
|
||||
selectedGames?: string[]
|
||||
min?: string
|
||||
max?: string
|
||||
onlyOnline?: boolean
|
||||
minRating?: string
|
||||
sort?: SearchSort
|
||||
limit?: number
|
||||
offset?: number
|
||||
}
|
||||
|
||||
export interface SearchCatalogData {
|
||||
players: Player[]
|
||||
shops: Shop[]
|
||||
services: PlayerService[]
|
||||
}
|
||||
|
||||
type InternalResultItem = SearchResultItem & { __index: number }
|
||||
|
||||
function minPriceFromServices(services: PlayerService[]) {
|
||||
return services.length === 0 ? 0 : Math.min(...services.map((s) => s.price))
|
||||
}
|
||||
|
||||
function unitFromCheapestService(services: PlayerService[]): PlayerService["unit"] {
|
||||
if (services.length === 0) return "局"
|
||||
return services.reduce((prev, curr) => (prev.price < curr.price ? prev : curr)).unit
|
||||
}
|
||||
|
||||
export function searchCatalog(
|
||||
params: SearchCatalogParams,
|
||||
data: SearchCatalogData,
|
||||
): SearchResponse {
|
||||
const q = params.q ?? ""
|
||||
const query = q ? q.toLowerCase() : ""
|
||||
const selectedGames = params.selectedGames ?? []
|
||||
|
||||
const min = params.min ?? ""
|
||||
const max = params.max ?? ""
|
||||
const minP = min ? Number(min) : 0
|
||||
const maxP = max ? Number(max) : Infinity
|
||||
|
||||
const onlyOnline = params.onlyOnline ?? false
|
||||
const minRating = Number(params.minRating ?? "0")
|
||||
const sort = params.sort ?? "composite"
|
||||
|
||||
const offset = Math.max(0, params.offset ?? 0)
|
||||
const limit = Math.max(0, params.limit ?? 12)
|
||||
|
||||
const shopDerivedById = new Map<
|
||||
string,
|
||||
{
|
||||
minPrice: number
|
||||
unit: PlayerService["unit"]
|
||||
games: string[]
|
||||
hasAvailable: boolean
|
||||
}
|
||||
>()
|
||||
|
||||
for (const shop of data.shops) {
|
||||
const shopPlayers = data.players.filter((player) => player.shopId === shop.id)
|
||||
const playerIds = new Set(shopPlayers.map((player) => player.id))
|
||||
const shopServices = data.services.filter((service) => playerIds.has(service.playerId))
|
||||
const shopMinPrice = shopServices.length > 0 ? Math.min(...shopServices.map((s) => s.price)) : 0
|
||||
const shopUnit = shopServices.length > 0 ? unitFromCheapestService(shopServices) : "局"
|
||||
const shopGames = [...new Set(shopServices.map((service) => service.gameName))]
|
||||
const hasAvailable = shopPlayers.some((player) => player.status === "available")
|
||||
|
||||
shopDerivedById.set(shop.id, {
|
||||
minPrice: shopMinPrice,
|
||||
unit: shopUnit,
|
||||
games: shopGames,
|
||||
hasAvailable,
|
||||
})
|
||||
}
|
||||
|
||||
let nextIndex = 0
|
||||
const results: InternalResultItem[] = []
|
||||
|
||||
for (const player of data.players) {
|
||||
results.push({
|
||||
__index: nextIndex++,
|
||||
type: "player",
|
||||
player,
|
||||
rating: player.rating,
|
||||
orders: player.totalOrders,
|
||||
minPrice: minPriceFromServices(player.services),
|
||||
unit: unitFromCheapestService(player.services),
|
||||
})
|
||||
}
|
||||
|
||||
for (const shop of data.shops) {
|
||||
const derived = shopDerivedById.get(shop.id)
|
||||
if (!derived) continue
|
||||
|
||||
results.push({
|
||||
__index: nextIndex++,
|
||||
type: "shop",
|
||||
shop,
|
||||
rating: shop.rating,
|
||||
orders: shop.totalOrders,
|
||||
minPrice: derived.minPrice,
|
||||
unit: derived.unit,
|
||||
games: derived.games,
|
||||
hasAvailable: derived.hasAvailable,
|
||||
})
|
||||
}
|
||||
|
||||
const filtered = results.filter((item) => {
|
||||
if (query) {
|
||||
if (item.type === "player") {
|
||||
const matchName = item.player.user.nickname.toLowerCase().includes(query)
|
||||
const matchTags = item.player.tags.some((tag) => tag.toLowerCase().includes(query))
|
||||
const matchGames = item.player.games.some((game) => game.toLowerCase().includes(query))
|
||||
if (!matchName && !matchTags && !matchGames) return false
|
||||
} else {
|
||||
const matchName = item.shop.name.toLowerCase().includes(query)
|
||||
const matchDescription = item.shop.description.toLowerCase().includes(query)
|
||||
const matchGames = item.games.some((game) => game.toLowerCase().includes(query))
|
||||
if (!matchName && !matchDescription && !matchGames) return false
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedGames.length > 0) {
|
||||
if (item.type === "player") {
|
||||
const hasGame = item.player.games.some((game) => selectedGames.includes(game))
|
||||
if (!hasGame) return false
|
||||
} else {
|
||||
const hasGame = item.games.some((game) => selectedGames.includes(game))
|
||||
if (!hasGame) return false
|
||||
}
|
||||
}
|
||||
|
||||
if (item.minPrice < minP) return false
|
||||
if (max && item.minPrice > maxP) return false
|
||||
|
||||
if (onlyOnline) {
|
||||
if (item.type === "player") {
|
||||
if (item.player.status !== "available") return false
|
||||
} else {
|
||||
if (!item.hasAvailable) return false
|
||||
}
|
||||
}
|
||||
|
||||
if (item.type === "player") {
|
||||
if (item.player.rating < minRating) return false
|
||||
} else {
|
||||
if (item.shop.rating < minRating) return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
filtered.sort((a, b) => {
|
||||
let diff = 0
|
||||
switch (sort) {
|
||||
case "rating":
|
||||
diff = b.rating - a.rating
|
||||
break
|
||||
case "price_asc":
|
||||
diff = a.minPrice - b.minPrice
|
||||
break
|
||||
case "price_desc":
|
||||
diff = b.minPrice - a.minPrice
|
||||
break
|
||||
case "orders":
|
||||
diff = b.orders - a.orders
|
||||
break
|
||||
case "composite": {
|
||||
const scoreA = a.rating * Math.log10(a.orders + 1)
|
||||
const scoreB = b.rating * Math.log10(b.orders + 1)
|
||||
diff = scoreB - scoreA
|
||||
break
|
||||
}
|
||||
default:
|
||||
diff = 0
|
||||
}
|
||||
|
||||
if (diff !== 0) return diff
|
||||
return a.__index - b.__index
|
||||
})
|
||||
|
||||
const total = filtered.length
|
||||
const items = filtered.slice(offset, offset + limit).map(({ __index, ...item }) => item)
|
||||
return { items, meta: { total, offset, limit } }
|
||||
}
|
||||
Reference in New Issue
Block a user