feat: add GET /api/mobile/game/:gameId consolidated endpoint
This commit is contained in:
@@ -43,6 +43,7 @@ import { savedFilterRoutes } from "@/lib/api/saved-filters"
|
||||
import { cronRoutes } from "@/lib/api/cron"
|
||||
import { profilePhotoRoutes } from "@/lib/api/profile-photo"
|
||||
import { screenshotRoutes } from "@/lib/api/screenshots"
|
||||
import { mobileRoutes } from "@/lib/api/mobile"
|
||||
|
||||
const betterAuth = new Elysia({ name: "better-auth" })
|
||||
.mount(auth.handler)
|
||||
@@ -87,6 +88,7 @@ export const app = new Elysia({ prefix: "/api" })
|
||||
{ name: "Admin", description: "Admin-only endpoints" },
|
||||
{ name: "Steam", description: "Steam proxy endpoints" },
|
||||
{ name: "Search", description: "Search endpoints" },
|
||||
{ name: "Mobile", description: "Mobile-optimized consolidated endpoints" },
|
||||
{ name: "Contact", description: "Contact form" },
|
||||
{ name: "Dashboard", description: "Dashboard data" },
|
||||
{ name: "Cron", description: "Scheduled job triggers" },
|
||||
@@ -181,6 +183,7 @@ export const app = new Elysia({ prefix: "/api" })
|
||||
.use(steamdbVersionRoutes)
|
||||
.use(gamesManualRoutes)
|
||||
.use(screenshotRoutes)
|
||||
.use(mobileRoutes)
|
||||
)
|
||||
// ── Write routes (write rate limit + betterAuth) ─────────────
|
||||
.group("", (app) =>
|
||||
|
||||
@@ -28,3 +28,4 @@ export { dashboardRoutes } from "./dashboard"
|
||||
export { dashboardPublicRoutes } from "./dashboard-public"
|
||||
export { cronRoutes } from "./cron"
|
||||
export { profilePhotoRoutes } from "./profile-photo"
|
||||
export { mobileRoutes } from "./mobile"
|
||||
|
||||
@@ -0,0 +1,536 @@
|
||||
import { Elysia, t } from "elysia"
|
||||
import { db } from "@/lib/db/index"
|
||||
import {
|
||||
games,
|
||||
gameVersions,
|
||||
performanceEntries,
|
||||
hardware,
|
||||
user,
|
||||
gamePlatformSupport,
|
||||
entryScreenshots,
|
||||
gameComments,
|
||||
} from "@/lib/db/schema"
|
||||
import { eq, and, desc, sql, isNull, inArray } from "drizzle-orm"
|
||||
import { getR2PublicUrl } from "@/lib/storage"
|
||||
|
||||
/**
|
||||
* GET /api/mobile/game/:gameId
|
||||
*
|
||||
* Consolidated mobile-optimized endpoint that bundles game details,
|
||||
* stats, presets, platform support, and first page of comments
|
||||
* into a single response — replacing 4+ separate API calls.
|
||||
*/
|
||||
export const mobileRoutes = new Elysia({
|
||||
prefix: "/mobile",
|
||||
detail: { tags: ["Mobile"] },
|
||||
}).get(
|
||||
"/game/:gameId",
|
||||
async ({ params, set }) => {
|
||||
const { gameId } = params
|
||||
|
||||
// ── 1. Fetch game ───────────────────────────────────────────
|
||||
const [game] = await db
|
||||
.select()
|
||||
.from(games)
|
||||
.where(eq(games.id, gameId))
|
||||
.limit(1)
|
||||
|
||||
if (!game) {
|
||||
set.status = 404
|
||||
return { error: "Game not found" }
|
||||
}
|
||||
|
||||
// ── 2. Fetch all non-removed performance entries with joins ──
|
||||
// Used for both stats and presets
|
||||
const entries = await db
|
||||
.select({
|
||||
id: performanceEntries.id,
|
||||
hardwareSlug: performanceEntries.hardwareSlug,
|
||||
hardwareName: hardware.name,
|
||||
fpsAvg: performanceEntries.fpsAvg,
|
||||
fpsLow: performanceEntries.fpsLow,
|
||||
fpsHigh: performanceEntries.fpsHigh,
|
||||
fpsOnePercentLow: performanceEntries.fpsOnePercentLow,
|
||||
upscalerType: performanceEntries.upscalerType,
|
||||
upscalerVersion: performanceEntries.upscalerVersion,
|
||||
frameGenMethod: performanceEntries.frameGenMethod,
|
||||
protonVersion: performanceEntries.protonVersion,
|
||||
osVersion: performanceEntries.osVersion,
|
||||
upvotes: performanceEntries.upvotes,
|
||||
downvotes: performanceEntries.downvotes,
|
||||
verifiedAt: performanceEntries.verifiedAt,
|
||||
userNotes: performanceEntries.userNotes,
|
||||
createdAt: performanceEntries.createdAt,
|
||||
versionId: performanceEntries.versionId,
|
||||
tdpWatts: performanceEntries.tdpWatts,
|
||||
settingsJson: performanceEntries.settingsJson,
|
||||
userId: performanceEntries.userId,
|
||||
userName: user.name,
|
||||
userImage: user.image,
|
||||
launchOptions: performanceEntries.launchOptions,
|
||||
loadTimeSsd: performanceEntries.loadTimeSsd,
|
||||
loadTimeSd: performanceEntries.loadTimeSd,
|
||||
youtubeVideoId: performanceEntries.youtubeVideoId,
|
||||
customSystem: performanceEntries.customSystem,
|
||||
isPinned: performanceEntries.isPinned,
|
||||
pinnedAt: performanceEntries.pinnedAt,
|
||||
versionString: gameVersions.versionString,
|
||||
buildId: gameVersions.buildId,
|
||||
gameAntiCheatName: gamePlatformSupport.antiCheatName,
|
||||
gameAntiCheatStatus: gamePlatformSupport.antiCheatStatus,
|
||||
})
|
||||
.from(performanceEntries)
|
||||
.innerJoin(gameVersions, eq(performanceEntries.versionId, gameVersions.id))
|
||||
.innerJoin(hardware, eq(performanceEntries.hardwareSlug, hardware.slug))
|
||||
.innerJoin(user, eq(performanceEntries.userId, user.id))
|
||||
.innerJoin(
|
||||
gamePlatformSupport,
|
||||
and(
|
||||
eq(gamePlatformSupport.gameId, gameVersions.gameId),
|
||||
eq(gamePlatformSupport.hardwareSlug, performanceEntries.hardwareSlug),
|
||||
),
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(gameVersions.gameId, gameId),
|
||||
eq(performanceEntries.isRemoved, false),
|
||||
),
|
||||
)
|
||||
.orderBy(desc(performanceEntries.isPinned), desc(performanceEntries.upvotes))
|
||||
|
||||
// ── 3. Fetch hardware details for all devices involved ───────
|
||||
const deviceSlugs = [...new Set(entries.map((e) => e.hardwareSlug))]
|
||||
const deviceData = deviceSlugs.length > 0
|
||||
? await db
|
||||
.select({
|
||||
slug: hardware.slug,
|
||||
name: hardware.name,
|
||||
wattHours: hardware.wattHours,
|
||||
tdpMax: hardware.tdpMax,
|
||||
deviceType: hardware.deviceType,
|
||||
})
|
||||
.from(hardware)
|
||||
.where(inArray(hardware.slug, deviceSlugs))
|
||||
: []
|
||||
|
||||
const deviceWattHoursMap = new Map(
|
||||
deviceData.map((d) => [d.slug, d]),
|
||||
)
|
||||
|
||||
// ── 4. Compute stats ────────────────────────────────────────
|
||||
const stats = (() => {
|
||||
if (entries.length === 0) {
|
||||
return {
|
||||
summary: {
|
||||
totalEntries: 0,
|
||||
avgFps: null as number | null,
|
||||
bestDevice: null as string | null,
|
||||
verifiedCount: 0,
|
||||
versionCount: 0,
|
||||
},
|
||||
isRawPerformer: false,
|
||||
isPoorPerformance: false,
|
||||
boxplot: [] as Array<{
|
||||
hardwareSlug: string
|
||||
hardwareName: string
|
||||
min: number
|
||||
q1: number
|
||||
median: number
|
||||
onePercentLow: number
|
||||
q3: number
|
||||
max: number
|
||||
}>,
|
||||
fpsRange: [] as Array<{
|
||||
id: string
|
||||
hardwareSlug: string
|
||||
fpsLow: number
|
||||
fpsAvg: number
|
||||
fpsHigh: number
|
||||
fpsOnePercentLow: number | null
|
||||
isRawPerformer: boolean
|
||||
isPoorPerformer: boolean
|
||||
}>,
|
||||
deviceBreakdown: [] as Array<{
|
||||
hardwareSlug: string
|
||||
hardwareName: string
|
||||
count: number
|
||||
wattHours: number | null
|
||||
tdpMax: number | null
|
||||
deviceType: string | null
|
||||
}>,
|
||||
batteryLife: [] as Array<{
|
||||
id: string
|
||||
hardwareSlug: string
|
||||
tdpWatts: number
|
||||
estimatedBatteryMin: number
|
||||
estimatedBatteryHours: number
|
||||
wattHours: number | null
|
||||
tdpMax: number | null
|
||||
estimatedAtMaxTdpMin: number | null
|
||||
}>,
|
||||
filterOptions: { protonVersions: [] as string[], osVersions: [] as string[] },
|
||||
}
|
||||
}
|
||||
|
||||
const totalEntries = entries.length
|
||||
const avgFps =
|
||||
Math.round(
|
||||
(entries.reduce((sum, e) => sum + (e.fpsAvg ?? 0), 0) / totalEntries) * 10,
|
||||
) / 10
|
||||
const verifiedCount = entries.filter((e) => e.verifiedAt !== null).length
|
||||
|
||||
// Best device by mean fpsAvg
|
||||
const deviceFpsMap = new Map<string, number[]>()
|
||||
for (const e of entries) {
|
||||
const arr = deviceFpsMap.get(e.hardwareSlug) || []
|
||||
arr.push(e.fpsAvg ?? 0)
|
||||
deviceFpsMap.set(e.hardwareSlug, arr)
|
||||
}
|
||||
let bestDevice: string | null = null
|
||||
let bestDeviceAvg = 0
|
||||
for (const [slug, fpsArr] of deviceFpsMap) {
|
||||
const mean = fpsArr.reduce((a, b) => a + b, 0) / fpsArr.length
|
||||
if (mean > bestDeviceAvg) {
|
||||
bestDeviceAvg = mean
|
||||
bestDevice = slug
|
||||
}
|
||||
}
|
||||
|
||||
// Version count
|
||||
const versionCount = new Set(entries.map((e) => e.versionId)).size
|
||||
|
||||
// Flags
|
||||
const isRawPerformer = entries.some(
|
||||
(e) =>
|
||||
(e.fpsAvg ?? 0) >= 60 &&
|
||||
e.upscalerType === "none" &&
|
||||
e.frameGenMethod === "none",
|
||||
)
|
||||
const isPoorPerformance = entries.some((e) => (e.fpsAvg ?? 0) < 30)
|
||||
|
||||
// Boxplot per device
|
||||
const boxplotMap = new Map<
|
||||
string,
|
||||
{ hardwareName: string; values: number[] }
|
||||
>()
|
||||
for (const e of entries) {
|
||||
const existing = boxplotMap.get(e.hardwareSlug) || {
|
||||
hardwareName: e.hardwareName,
|
||||
values: [],
|
||||
}
|
||||
existing.values.push(e.fpsAvg ?? 0)
|
||||
boxplotMap.set(e.hardwareSlug, existing)
|
||||
}
|
||||
|
||||
const boxplot = Array.from(boxplotMap.entries()).map(
|
||||
([slug, { hardwareName, values }]) => {
|
||||
const sorted = [...values].sort((a, b) => a - b)
|
||||
const n = sorted.length
|
||||
const q1Idx = Math.floor(n * 0.25)
|
||||
const medIdx = Math.floor(n * 0.5)
|
||||
const q3Idx = Math.floor(n * 0.75)
|
||||
const onePercentLow =
|
||||
sorted.length > 0
|
||||
? sorted[Math.max(0, Math.floor(sorted.length * 0.01))]
|
||||
: sorted[0]
|
||||
return {
|
||||
hardwareSlug: slug,
|
||||
hardwareName,
|
||||
min: sorted[0],
|
||||
q1: sorted[q1Idx],
|
||||
median: sorted[medIdx],
|
||||
onePercentLow,
|
||||
q3: sorted[q3Idx],
|
||||
max: sorted[n - 1],
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// FPS range per entry
|
||||
const fpsRange = entries
|
||||
.filter((e) => e.fpsLow !== null && e.fpsHigh !== null)
|
||||
.map((e) => ({
|
||||
id: e.id,
|
||||
hardwareSlug: e.hardwareSlug,
|
||||
fpsLow: e.fpsLow!,
|
||||
fpsAvg: e.fpsAvg ?? 0,
|
||||
fpsHigh: e.fpsHigh!,
|
||||
fpsOnePercentLow: e.fpsOnePercentLow ?? null,
|
||||
isRawPerformer:
|
||||
(e.fpsAvg ?? 0) >= 60 &&
|
||||
e.upscalerType === "none" &&
|
||||
e.frameGenMethod === "none",
|
||||
isPoorPerformer: (e.fpsAvg ?? 0) < 30,
|
||||
}))
|
||||
|
||||
// Device breakdown with hardware info
|
||||
const deviceBreakdown = Array.from(boxplotMap.entries()).map(
|
||||
([slug, { hardwareName, values }]) => {
|
||||
const dev = deviceWattHoursMap.get(slug)
|
||||
return {
|
||||
hardwareSlug: slug,
|
||||
hardwareName,
|
||||
count: values.length,
|
||||
wattHours: dev?.wattHours ? Number(dev.wattHours) : null,
|
||||
tdpMax: dev?.tdpMax ? Number(dev.tdpMax) : null,
|
||||
deviceType: dev?.deviceType ?? null,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// Battery life estimates
|
||||
const batteryLife = entries
|
||||
.filter((e) => e.tdpWatts != null && e.tdpWatts > 0)
|
||||
.map((e) => {
|
||||
const device = deviceWattHoursMap.get(e.hardwareSlug)
|
||||
const wh = device?.wattHours ? Number(device.wattHours) : null
|
||||
const tdpMax = device?.tdpMax ? Number(device.tdpMax) : null
|
||||
if (!wh) return null
|
||||
const estimatedBatteryHours = wh / e.tdpWatts!
|
||||
const estimatedBatteryMin = estimatedBatteryHours * 60
|
||||
const estimatedAtMaxTdpMin = tdpMax ? (wh / tdpMax) * 60 : null
|
||||
return {
|
||||
id: e.id,
|
||||
hardwareSlug: e.hardwareSlug,
|
||||
tdpWatts: Number(e.tdpWatts),
|
||||
estimatedBatteryMin: Math.round(estimatedBatteryMin),
|
||||
estimatedBatteryHours: Math.round(estimatedBatteryHours * 10) / 10,
|
||||
wattHours: wh,
|
||||
tdpMax,
|
||||
estimatedAtMaxTdpMin: estimatedAtMaxTdpMin
|
||||
? Math.round(estimatedAtMaxTdpMin)
|
||||
: null,
|
||||
}
|
||||
})
|
||||
.filter(Boolean) as Array<{
|
||||
id: string
|
||||
hardwareSlug: string
|
||||
tdpWatts: number
|
||||
estimatedBatteryMin: number
|
||||
estimatedBatteryHours: number
|
||||
wattHours: number | null
|
||||
tdpMax: number | null
|
||||
estimatedAtMaxTdpMin: number | null
|
||||
}>
|
||||
|
||||
// Filter options
|
||||
const protonVersions = [
|
||||
...new Set(entries.map((e) => e.protonVersion).filter(Boolean)),
|
||||
] as string[]
|
||||
const osVersions = [
|
||||
...new Set(entries.map((e) => e.osVersion).filter(Boolean)),
|
||||
] as string[]
|
||||
|
||||
return {
|
||||
summary: {
|
||||
totalEntries,
|
||||
avgFps,
|
||||
bestDevice,
|
||||
verifiedCount,
|
||||
versionCount,
|
||||
},
|
||||
isRawPerformer,
|
||||
isPoorPerformance,
|
||||
boxplot,
|
||||
fpsRange,
|
||||
deviceBreakdown,
|
||||
batteryLife,
|
||||
filterOptions: { protonVersions, osVersions },
|
||||
}
|
||||
})()
|
||||
|
||||
// ── 5. Build presets (entries with settingsJson) ──────────────
|
||||
const presetEntries = entries.filter((e) => e.settingsJson !== null)
|
||||
const publicUrl = getR2PublicUrl()
|
||||
|
||||
const presetEntryIds = presetEntries.map((e) => e.id)
|
||||
|
||||
// Batch-fetch screenshots for all presets
|
||||
const screenshotsMap = new Map<
|
||||
string,
|
||||
Array<{
|
||||
id: string
|
||||
url: string
|
||||
width: number
|
||||
height: number
|
||||
orderIndex: number
|
||||
}>
|
||||
>()
|
||||
|
||||
if (presetEntryIds.length > 0) {
|
||||
const allScreenshots = await db
|
||||
.select({
|
||||
id: entryScreenshots.id,
|
||||
storageKey: entryScreenshots.storageKey,
|
||||
orderIndex: entryScreenshots.orderIndex,
|
||||
width: entryScreenshots.width,
|
||||
height: entryScreenshots.height,
|
||||
entryId: entryScreenshots.entryId,
|
||||
})
|
||||
.from(entryScreenshots)
|
||||
.where(inArray(entryScreenshots.entryId, presetEntryIds))
|
||||
.orderBy(entryScreenshots.orderIndex)
|
||||
|
||||
for (const ss of allScreenshots) {
|
||||
const arr = screenshotsMap.get(ss.entryId) || []
|
||||
arr.push({
|
||||
id: ss.id,
|
||||
url: `${publicUrl}/${ss.storageKey}`,
|
||||
width: ss.width,
|
||||
height: ss.height,
|
||||
orderIndex: ss.orderIndex,
|
||||
})
|
||||
screenshotsMap.set(ss.entryId, arr)
|
||||
}
|
||||
}
|
||||
|
||||
const presets = presetEntries.map((p) => {
|
||||
const settingsCount = Array.isArray(p.settingsJson)
|
||||
? (p.settingsJson as Array<{ settings: unknown[] }>).reduce(
|
||||
(sum, cat) => sum + cat.settings.length,
|
||||
0,
|
||||
)
|
||||
: 0
|
||||
|
||||
const hw = deviceWattHoursMap.get(p.hardwareSlug)
|
||||
|
||||
return {
|
||||
id: p.id,
|
||||
gameId,
|
||||
hardwareSlug: p.hardwareSlug,
|
||||
hardwareName: p.hardwareName,
|
||||
upvotes: p.upvotes,
|
||||
downvotes: p.downvotes,
|
||||
settingsCount,
|
||||
fpsAvg: p.fpsAvg,
|
||||
fpsLow: p.fpsLow,
|
||||
fpsHigh: p.fpsHigh,
|
||||
fpsOnePercentLow: p.fpsOnePercentLow ?? null,
|
||||
upscalerType: p.upscalerType,
|
||||
upscalerVersion: p.upscalerVersion,
|
||||
frameGenMethod: p.frameGenMethod,
|
||||
protonVersion: p.protonVersion,
|
||||
osVersion: p.osVersion,
|
||||
createdAt: p.createdAt.toISOString(),
|
||||
settingsJson: p.settingsJson,
|
||||
launchOptions: p.launchOptions,
|
||||
loadTimeSsd: p.loadTimeSsd ?? null,
|
||||
loadTimeSd: p.loadTimeSd ?? null,
|
||||
tdpWatts: p.tdpWatts ?? null,
|
||||
youtubeVideoId: p.youtubeVideoId ?? null,
|
||||
screenshots: screenshotsMap.get(p.id) || [],
|
||||
hardwareWattHours: hw?.wattHours ? Number(hw.wattHours) : null,
|
||||
hardwareDeviceType: hw?.deviceType ?? null,
|
||||
customSystem: p.customSystem ?? false,
|
||||
userNotes: p.userNotes,
|
||||
versionString: p.versionString ?? null,
|
||||
buildId: p.buildId ?? null,
|
||||
gameAntiCheatName: p.gameAntiCheatName ?? null,
|
||||
gameAntiCheatStatus: p.gameAntiCheatStatus ?? null,
|
||||
userId: p.userId,
|
||||
userName: p.userName,
|
||||
userImage: p.userImage,
|
||||
verifiedAt: p.verifiedAt ? p.verifiedAt.toISOString() : null,
|
||||
isPinned: p.isPinned,
|
||||
pinnedAt: p.pinnedAt ? p.pinnedAt.toISOString() : null,
|
||||
}
|
||||
})
|
||||
|
||||
// ── 6. Fetch platform support ────────────────────────────────
|
||||
const platformSupport = await db
|
||||
.select({
|
||||
id: gamePlatformSupport.id,
|
||||
hardwareSlug: gamePlatformSupport.hardwareSlug,
|
||||
isSupported: gamePlatformSupport.isSupported,
|
||||
protonStatus: gamePlatformSupport.protonStatus,
|
||||
antiCheatRelevant: gamePlatformSupport.antiCheatRelevant,
|
||||
antiCheatName: gamePlatformSupport.antiCheatName,
|
||||
antiCheatVersion: gamePlatformSupport.antiCheatVersion,
|
||||
antiCheatStatus: gamePlatformSupport.antiCheatStatus,
|
||||
playabilityStatus: gamePlatformSupport.playabilityStatus,
|
||||
playabilityOverride: gamePlatformSupport.playabilityOverride,
|
||||
playabilityCalculatedAt: gamePlatformSupport.playabilityCalculatedAt,
|
||||
createdAt: gamePlatformSupport.createdAt,
|
||||
updatedAt: gamePlatformSupport.updatedAt,
|
||||
})
|
||||
.from(gamePlatformSupport)
|
||||
.where(eq(gamePlatformSupport.gameId, gameId))
|
||||
|
||||
// ── 7. Fetch first page of comments ──────────────────────────
|
||||
const commentLimit = 20
|
||||
const commentConditions = [
|
||||
eq(gameComments.gameId, gameId),
|
||||
eq(gameComments.isRemoved, false),
|
||||
isNull(gameComments.parentId),
|
||||
]
|
||||
|
||||
const [commentData, [{ count: commentTotal }]] = await Promise.all([
|
||||
db
|
||||
.select({
|
||||
id: gameComments.id,
|
||||
gameId: gameComments.gameId,
|
||||
userId: gameComments.userId,
|
||||
parentId: gameComments.parentId,
|
||||
content: gameComments.content,
|
||||
upvotes: gameComments.upvotes,
|
||||
createdAt: gameComments.createdAt,
|
||||
updatedAt: gameComments.updatedAt,
|
||||
userName: user.name,
|
||||
userImage: user.image,
|
||||
})
|
||||
.from(gameComments)
|
||||
.innerJoin(user, eq(gameComments.userId, user.id))
|
||||
.where(and(...commentConditions))
|
||||
.orderBy(desc(gameComments.createdAt))
|
||||
.limit(commentLimit),
|
||||
db
|
||||
.select({ count: sql<number>`count(*)::int` })
|
||||
.from(gameComments)
|
||||
.where(and(...commentConditions)),
|
||||
])
|
||||
|
||||
// ── 8. Assemble response ─────────────────────────────────────
|
||||
return {
|
||||
game: {
|
||||
id: game.id,
|
||||
steamAppId: game.steamAppId,
|
||||
title: game.title,
|
||||
description: game.description,
|
||||
developer: game.developer,
|
||||
publisher: game.publisher,
|
||||
genres: game.genres,
|
||||
headerImage: game.headerImage,
|
||||
capsuleImage: game.capsuleImage,
|
||||
storeUrl: game.storeUrl,
|
||||
source: game.source,
|
||||
lastSync: game.lastSync?.toISOString() ?? null,
|
||||
syncStatus: game.syncStatus,
|
||||
createdAt: game.createdAt.toISOString(),
|
||||
systemRequirements: game.systemRequirements,
|
||||
metacriticScore: game.metacriticScore,
|
||||
metacriticUrl: game.metacriticUrl,
|
||||
recommendationsTotal: game.recommendationsTotal,
|
||||
priceCurrent: game.priceCurrent,
|
||||
priceInitial: game.priceInitial,
|
||||
priceCurrency: game.priceCurrency,
|
||||
isFree: game.isFree,
|
||||
releaseDate: game.releaseDate,
|
||||
categories: game.categories,
|
||||
platforms: game.platforms,
|
||||
steamReviewScore: game.steamReviewScore,
|
||||
steamReviewSentiment: game.steamReviewSentiment,
|
||||
steamReviewCount: game.steamReviewCount,
|
||||
},
|
||||
stats,
|
||||
presets,
|
||||
platformSupport,
|
||||
comments: {
|
||||
data: commentData,
|
||||
total: commentTotal,
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
params: t.Object({ gameId: t.String() }),
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user