feat: add Raw Performer badge and real data to search results
This commit is contained in:
+26
-8
@@ -39,6 +39,9 @@ interface UnifiedResult {
|
|||||||
price?: { currency: string; initial: number; final: number } | null
|
price?: { currency: string; initial: number; final: number } | null
|
||||||
platforms?: { windows: boolean; mac: boolean; linux: boolean } | null
|
platforms?: { windows: boolean; mac: boolean; linux: boolean } | null
|
||||||
controllerSupport?: string | null
|
controllerSupport?: string | null
|
||||||
|
isRawPerformer?: boolean
|
||||||
|
bestFps?: number | null
|
||||||
|
latestVersion?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
function SearchContent() {
|
function SearchContent() {
|
||||||
@@ -86,8 +89,8 @@ function SearchContent() {
|
|||||||
|
|
||||||
function handleClick(result: UnifiedResult) {
|
function handleClick(result: UnifiedResult) {
|
||||||
const path = result.appId
|
const path = result.appId
|
||||||
? `/game/${result.appId}`
|
? `/game/${result.appId}?sync=1`
|
||||||
: `/game/${result.id}`
|
: `/game/${result.id}?sync=1`
|
||||||
router.push(path)
|
router.push(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,9 +247,16 @@ function SearchResultCard({
|
|||||||
{/* Row 1: Title + metascore/price row */}
|
{/* Row 1: Title + metascore/price row */}
|
||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<h3 className="text-sm sm:text-base font-semibold text-text group-hover:text-primary transition-colors duration-200 truncate">
|
<div className="min-w-0 flex items-center gap-2 flex-wrap">
|
||||||
{result.title}
|
<h3 className="text-sm sm:text-base font-semibold text-text group-hover:text-primary transition-colors duration-200 truncate">
|
||||||
</h3>
|
{result.title}
|
||||||
|
</h3>
|
||||||
|
{result.isRawPerformer && (
|
||||||
|
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-green-500/10 border border-green-500/20 text-green-400 text-[10px] font-semibold shrink-0">
|
||||||
|
⚡ RAW PERFORMER
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{(result.developer || result.publisher) && (
|
{(result.developer || result.publisher) && (
|
||||||
<p className="text-[11px] text-text/45 mt-0.5 truncate">
|
<p className="text-[11px] text-text/45 mt-0.5 truncate">
|
||||||
{result.developer}
|
{result.developer}
|
||||||
@@ -422,11 +432,19 @@ function SearchResultCard({
|
|||||||
color={result.platformSupport ? protonColor(result.platformSupport.protonStatus) : undefined}
|
color={result.platformSupport ? protonColor(result.platformSupport.protonStatus) : undefined}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Avg FPS */}
|
{/* Best FPS */}
|
||||||
<DataField label="Avg. FPS" value="—" bar />
|
<DataField
|
||||||
|
label="Best FPS"
|
||||||
|
value={result.bestFps != null ? String(Math.round(result.bestFps)) : "—"}
|
||||||
|
bar={result.bestFps != null}
|
||||||
|
color={result.bestFps != null && result.bestFps >= 60 ? "text-green-400" : undefined}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Version */}
|
{/* Version */}
|
||||||
<DataField label="Version" value="—" />
|
<DataField
|
||||||
|
label="Version"
|
||||||
|
value={result.latestVersion ?? "—"}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.article>
|
</motion.article>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
communityPresets,
|
communityPresets,
|
||||||
gameComments,
|
gameComments,
|
||||||
} from "@/lib/db/schema"
|
} from "@/lib/db/schema"
|
||||||
import { ilike, or, sql, eq, inArray } from "drizzle-orm"
|
import { ilike, or, sql, eq, inArray, and } from "drizzle-orm"
|
||||||
|
|
||||||
interface SteamSearchItem {
|
interface SteamSearchItem {
|
||||||
id: number
|
id: number
|
||||||
@@ -146,6 +146,64 @@ export const searchUnifiedRoutes = new Elysia({ prefix: "/search" }).get(
|
|||||||
countMap.get(c.gameId)!.comments = c.count
|
countMap.get(c.gameId)!.comments = c.count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 2b. Raw Performer + best FPS ────────────────────────────────
|
||||||
|
let rawPerformerMap = new Map<string, boolean>()
|
||||||
|
let bestFpsMap = new Map<string, number>()
|
||||||
|
|
||||||
|
if (localGameIds.length > 0) {
|
||||||
|
const perfStats = await db
|
||||||
|
.select({
|
||||||
|
gameId: gameVersions.gameId,
|
||||||
|
bestFps: sql<number>`MAX(${performanceEntries.fpsAvg})::real`,
|
||||||
|
isRawPerformer: sql<boolean>`BOOL_OR(
|
||||||
|
${performanceEntries.fpsAvg} >= 60
|
||||||
|
AND ${performanceEntries.fsrVersion} = 'none'
|
||||||
|
AND ${performanceEntries.frameGenMethod} = 'none'
|
||||||
|
)`,
|
||||||
|
})
|
||||||
|
.from(performanceEntries)
|
||||||
|
.innerJoin(
|
||||||
|
gameVersions,
|
||||||
|
eq(performanceEntries.versionId, gameVersions.id),
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
inArray(gameVersions.gameId, localGameIds),
|
||||||
|
eq(performanceEntries.isRemoved, false),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.groupBy(gameVersions.gameId)
|
||||||
|
|
||||||
|
for (const row of perfStats) {
|
||||||
|
bestFpsMap.set(row.gameId, row.bestFps)
|
||||||
|
rawPerformerMap.set(row.gameId, row.isRawPerformer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 2c. Latest version ──────────────────────────────────────────
|
||||||
|
let latestVersionMap = new Map<string, string>()
|
||||||
|
|
||||||
|
if (localGameIds.length > 0) {
|
||||||
|
const versionRows = await db
|
||||||
|
.select({
|
||||||
|
gameId: gameVersions.gameId,
|
||||||
|
versionString: gameVersions.versionString,
|
||||||
|
})
|
||||||
|
.from(gameVersions)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
inArray(gameVersions.gameId, localGameIds),
|
||||||
|
eq(gameVersions.isLatest, true),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const row of versionRows) {
|
||||||
|
if (row.versionString) {
|
||||||
|
latestVersionMap.set(row.gameId, row.versionString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── 3. Search Steam ─────────────────────────────────────────────
|
// ── 3. Search Steam ─────────────────────────────────────────────
|
||||||
let steamItems: SteamSearchItem[] = []
|
let steamItems: SteamSearchItem[] = []
|
||||||
try {
|
try {
|
||||||
@@ -214,6 +272,9 @@ export const searchUnifiedRoutes = new Elysia({ prefix: "/search" }).get(
|
|||||||
antiCheatStatus: platform.antiCheatStatus,
|
antiCheatStatus: platform.antiCheatStatus,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
|
isRawPerformer: rawPerformerMap.get(g.id) ?? false,
|
||||||
|
bestFps: bestFpsMap.get(g.id) ?? null,
|
||||||
|
latestVersion: latestVersionMap.get(g.id) ?? null,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,6 +303,9 @@ export const searchUnifiedRoutes = new Elysia({ prefix: "/search" }).get(
|
|||||||
: null,
|
: null,
|
||||||
platforms: item.platforms,
|
platforms: item.platforms,
|
||||||
controllerSupport: item.controller_support || null,
|
controllerSupport: item.controller_support || null,
|
||||||
|
isRawPerformer: false,
|
||||||
|
bestFps: null,
|
||||||
|
latestVersion: null,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user