feat: update search page and improve details
This commit is contained in:
@@ -4,12 +4,12 @@ import { games } from "@/lib/db/schema"
|
||||
import { eq } from "drizzle-orm"
|
||||
|
||||
interface SteamAppDetails {
|
||||
type?: string
|
||||
steam_appid: number
|
||||
name: string
|
||||
developers?: string[]
|
||||
publishers?: string[]
|
||||
header_image?: string
|
||||
capsule_imagev5?: string
|
||||
genres?: { id: string; description: string }[]
|
||||
website?: string
|
||||
}
|
||||
@@ -50,6 +50,13 @@ export const gameStubRoutes = new Elysia({ prefix: "/games" }).post(
|
||||
details = entry.data
|
||||
}
|
||||
}
|
||||
|
||||
// Reject non-games (DLCs, soundtracks, demos, etc.)
|
||||
if (details?.type && details.type !== "game") {
|
||||
set.status = 400
|
||||
return { error: `Not a game (type: ${details.type})` }
|
||||
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch Steam appdetails:", err)
|
||||
}
|
||||
@@ -60,7 +67,7 @@ export const gameStubRoutes = new Elysia({ prefix: "/games" }).post(
|
||||
const genres = details?.genres?.map((g) => g.description) || []
|
||||
const headerImage = details?.header_image || null
|
||||
const capsuleImage =
|
||||
details?.capsule_imagev5 || details?.header_image || null
|
||||
`https://cdn.akamai.steamstatic.com/steam/apps/${body.steamAppId}/library_600x900.jpg`
|
||||
|
||||
const [game] = await db
|
||||
.insert(games)
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { Elysia, t } from "elysia"
|
||||
import { db } from "@/lib/db/index"
|
||||
import { games, gameVersions, performanceEntries, communityPresets, gameComments } from "@/lib/db/schema"
|
||||
import {
|
||||
games,
|
||||
gameVersions,
|
||||
performanceEntries,
|
||||
communityPresets,
|
||||
gameComments,
|
||||
} from "@/lib/db/schema"
|
||||
import { ilike, or, sql, eq, inArray } from "drizzle-orm"
|
||||
|
||||
interface SteamSearchItem {
|
||||
@@ -8,7 +14,9 @@ interface SteamSearchItem {
|
||||
name: string
|
||||
tiny_image: string
|
||||
metascore: string
|
||||
price?: { currency: string; initial: number; final: number }
|
||||
platforms: { windows: boolean; mac: boolean; linux: boolean }
|
||||
controller_support?: string
|
||||
}
|
||||
|
||||
interface SteamSearchResponse {
|
||||
@@ -44,6 +52,42 @@ export const searchUnifiedRoutes = new Elysia({ prefix: "/search" }).get(
|
||||
localGames.map((g) => g.steamAppId).filter(Boolean),
|
||||
)
|
||||
|
||||
// Fetch platform support + anti-cheat for local games
|
||||
let platformSupportMap = new Map<
|
||||
string,
|
||||
{
|
||||
isSupported: boolean
|
||||
protonStatus: string
|
||||
antiCheatRelevant: boolean
|
||||
antiCheatName: string | null
|
||||
antiCheatStatus: string
|
||||
}
|
||||
>()
|
||||
if (localGameIds.length > 0) {
|
||||
const { gamePlatformSupport } = await import("@/lib/db/schema")
|
||||
const supportRows = await db
|
||||
.select({
|
||||
gameId: gamePlatformSupport.gameId,
|
||||
isSupported: gamePlatformSupport.isSupported,
|
||||
protonStatus: gamePlatformSupport.protonStatus,
|
||||
antiCheatRelevant: gamePlatformSupport.antiCheatRelevant,
|
||||
antiCheatName: gamePlatformSupport.antiCheatName,
|
||||
antiCheatStatus: gamePlatformSupport.antiCheatStatus,
|
||||
})
|
||||
.from(gamePlatformSupport)
|
||||
.where(inArray(gamePlatformSupport.gameId, localGameIds))
|
||||
|
||||
for (const row of supportRows) {
|
||||
platformSupportMap.set(row.gameId, {
|
||||
isSupported: row.isSupported,
|
||||
protonStatus: row.protonStatus,
|
||||
antiCheatRelevant: row.antiCheatRelevant,
|
||||
antiCheatName: row.antiCheatName,
|
||||
antiCheatStatus: row.antiCheatStatus,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ── 2. Count related data for local games ───────────────────────
|
||||
let benchmarkCounts: { gameId: string; count: number }[] = []
|
||||
let presetCounts: { gameId: string; count: number }[] = []
|
||||
@@ -115,19 +159,40 @@ export const searchUnifiedRoutes = new Elysia({ prefix: "/search" }).get(
|
||||
})
|
||||
if (res.ok) {
|
||||
const data = (await res.json()) as SteamSearchResponse
|
||||
steamItems = data.items || []
|
||||
steamItems = (data.items || []).filter((item) => {
|
||||
const name = item.name.toLowerCase()
|
||||
const exclude = [
|
||||
"soundtrack",
|
||||
" original soundtrack",
|
||||
" ost",
|
||||
" - ost",
|
||||
"dlc",
|
||||
"expansion",
|
||||
"season pass",
|
||||
" deluxe edition",
|
||||
" ultimate edition",
|
||||
" premium edition",
|
||||
" demo",
|
||||
" trial",
|
||||
" playtest",
|
||||
" beta",
|
||||
" artbook",
|
||||
" soundtrack bundle",
|
||||
]
|
||||
return !exclude.some((kw) => name.includes(kw))
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
// Steam search failure is non-fatal
|
||||
}
|
||||
|
||||
// ── 4. Build unified results ────────────────────────────────────
|
||||
// Local games first (they have data), then Steam-only results
|
||||
const results = []
|
||||
|
||||
// Add local games
|
||||
for (const g of localGames) {
|
||||
const counts = countMap.get(g.id)!
|
||||
const platform = platformSupportMap.get(g.id)
|
||||
results.push({
|
||||
kind: "local" as const,
|
||||
id: g.id,
|
||||
@@ -136,23 +201,47 @@ export const searchUnifiedRoutes = new Elysia({ prefix: "/search" }).get(
|
||||
image: g.capsuleImage || g.headerImage,
|
||||
developer: g.developer,
|
||||
publisher: g.publisher,
|
||||
description: g.description,
|
||||
genres: g.genres,
|
||||
source: g.source,
|
||||
counts,
|
||||
platformSupport: platform
|
||||
? {
|
||||
isSupported: platform.isSupported,
|
||||
protonStatus: platform.protonStatus,
|
||||
antiCheatRelevant: platform.antiCheatRelevant,
|
||||
antiCheatName: platform.antiCheatName,
|
||||
antiCheatStatus: platform.antiCheatStatus,
|
||||
}
|
||||
: null,
|
||||
})
|
||||
}
|
||||
|
||||
// Add Steam-only games (deduplicated against local steamAppIds)
|
||||
// Add Steam-only games
|
||||
for (const item of steamItems) {
|
||||
if (localSteamAppIds.has(item.id)) continue
|
||||
results.push({
|
||||
kind: "steam" as const,
|
||||
appId: item.id,
|
||||
title: item.name,
|
||||
image: item.tiny_image,
|
||||
image: `https://cdn.akamai.steamstatic.com/steam/apps/${item.id}/library_600x900.jpg`,
|
||||
developer: null,
|
||||
publisher: null,
|
||||
description: null,
|
||||
genres: null,
|
||||
source: "steam" as const,
|
||||
counts: null,
|
||||
platformSupport: null,
|
||||
metascore: item.metascore || null,
|
||||
price: item.price
|
||||
? {
|
||||
currency: item.price.currency,
|
||||
initial: item.price.initial,
|
||||
final: item.price.final,
|
||||
}
|
||||
: null,
|
||||
platforms: item.platforms,
|
||||
controllerSupport: item.controller_support || null,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+27
-2
@@ -38,11 +38,36 @@ export const steamSearchRoutes = new Elysia({ prefix: "/search" })
|
||||
|
||||
const data = await res.json()
|
||||
|
||||
const items: SteamSearchItem[] = (data.items || []).filter(
|
||||
(item: SteamSearchItem) => {
|
||||
const name = item.name.toLowerCase()
|
||||
const exclude = [
|
||||
"soundtrack",
|
||||
" original soundtrack",
|
||||
" ost",
|
||||
" - ost",
|
||||
"dlc",
|
||||
"expansion",
|
||||
"season pass",
|
||||
" deluxe edition",
|
||||
" ultimate edition",
|
||||
" premium edition",
|
||||
" demo",
|
||||
" trial",
|
||||
" playtest",
|
||||
" beta",
|
||||
" artbook",
|
||||
" soundtrack bundle",
|
||||
]
|
||||
return !exclude.some((kw) => name.includes(kw))
|
||||
},
|
||||
)
|
||||
|
||||
return {
|
||||
items: (data.items || []).map((item: SteamSearchItem) => ({
|
||||
items: items.map((item) => ({
|
||||
appId: item.id,
|
||||
title: item.name,
|
||||
image: item.tiny_image,
|
||||
image: `https://cdn.akamai.steamstatic.com/steam/apps/${item.id}/library_600x900.jpg`,
|
||||
platforms: item.platforms,
|
||||
metascore: item.metascore,
|
||||
})),
|
||||
|
||||
+1
-2
@@ -8,7 +8,6 @@ interface SteamAppDetails {
|
||||
developers?: string[]
|
||||
publishers?: string[]
|
||||
header_image?: string
|
||||
capsule_imagev5?: string
|
||||
genres?: { id: string; description: string }[]
|
||||
website?: string
|
||||
short_description?: string
|
||||
@@ -59,7 +58,7 @@ export async function syncSteamGame(steamAppId: number): Promise<void> {
|
||||
description: d.short_description || null,
|
||||
genres: d.genres?.map((g) => g.description) || [],
|
||||
headerImage: d.header_image || null,
|
||||
capsuleImage: d.capsule_imagev5 || d.header_image || null,
|
||||
capsuleImage: `https://cdn.akamai.steamstatic.com/steam/apps/${steamAppId}/library_600x900.jpg`,
|
||||
storeUrl: `https://store.steampowered.com/app/${steamAppId}`,
|
||||
lastSync: new Date(),
|
||||
syncStatus: "synced",
|
||||
|
||||
Reference in New Issue
Block a user