From 0ff27b613d0163969f073c5ffe7abdb16a0a1f56 Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Sat, 16 May 2026 18:10:29 +0800 Subject: [PATCH] chore: remove public community dashboard page --- .env.example | 7 + app/dashboard/page.tsx | 183 --------- app/sitemap.ts | 1 - bun.lock | 19 +- components/dashboard/best-releases-chart.tsx | 106 ------ components/dashboard/dashboard-client.tsx | 117 ------ components/dashboard/most-tested-chart.tsx | 159 -------- components/dashboard/trending-games-chart.tsx | 107 ------ lib/steamdb/scrape.ts | 351 ++++++++++++++++-- public/.well-known/apple-app-site-association | 6 + public/.well-known/assetlinks.json | 15 + 11 files changed, 365 insertions(+), 706 deletions(-) delete mode 100644 app/dashboard/page.tsx delete mode 100644 components/dashboard/best-releases-chart.tsx delete mode 100644 components/dashboard/dashboard-client.tsx delete mode 100644 components/dashboard/most-tested-chart.tsx delete mode 100644 components/dashboard/trending-games-chart.tsx create mode 100644 public/.well-known/apple-app-site-association create mode 100644 public/.well-known/assetlinks.json diff --git a/.env.example b/.env.example index dd76ff6..bb05520 100644 --- a/.env.example +++ b/.env.example @@ -74,6 +74,13 @@ RP_ID="localhost" # RP Name: Human-readable name shown in passkey prompts RP_NAME="DeckyVault" +# Android APK key hash for native passkey support on Android +# Generate with: +# keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android | grep 'SHA256:' +# Then convert to base64url format (remove colons, lowercase, base64url encode) +# Leave empty for web-only passkey support +ANDROID_APK_KEY_HASH= + # ----------------------------------------------------------------------------- # EMAIL # ----------------------------------------------------------------------------- diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx deleted file mode 100644 index 32f4647..0000000 --- a/app/dashboard/page.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import type { Metadata } from "next" -import { db } from "@/lib/db/index" -import { sql } from "drizzle-orm" -import { DashboardClient } from "@/components/dashboard/dashboard-client" - -export const revalidate = 3600 - -export const metadata: Metadata = { - title: "Dashboard — DeckyVault", - description: - "Community dashboard with trending games, best new releases, and most tested games on DeckyVault.", - keywords: [ - "Steam Deck dashboard", - "trending games", - "best new releases", - "game benchmarks", - "community insights", - ], - alternates: { canonical: "https://deckyvault.xyz/dashboard" }, - openGraph: { - title: "Dashboard — DeckyVault", - description: - "Community dashboard with trending games, best new releases, and most tested games.", - url: "https://deckyvault.xyz/dashboard", - siteName: "DeckyVault", - type: "website", - }, -} - -const SEVEN_DAYS_AGO = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) - -export default async function DashboardPage() { - const trending = await db.execute(sql` - WITH recent_benchmarks AS ( - SELECT gv.game_id, COUNT(*) AS cnt - FROM performance_entries pe - JOIN game_versions gv ON pe.version_id = gv.id - WHERE pe.is_removed = false - AND pe.created_at >= ${SEVEN_DAYS_AGO} - GROUP BY gv.game_id - ), - recent_comments AS ( - SELECT gc.game_id, COUNT(*) AS cnt - FROM game_comments gc - WHERE gc.is_removed = false - AND gc.created_at >= ${SEVEN_DAYS_AGO} - GROUP BY gc.game_id - ), - recent_upvotes AS ( - SELECT gv.game_id, SUM(pe.upvotes) AS total_upvotes - FROM performance_entries pe - JOIN game_versions gv ON pe.version_id = gv.id - WHERE pe.is_removed = false - AND pe.updated_at >= ${SEVEN_DAYS_AGO} - GROUP BY gv.game_id - ) - SELECT - g.id, - g.title, - g.capsule_image, - g.header_image, - g.playability_status, - COALESCE(rb.cnt, 0) AS benchmark_count, - COALESCE(rc.cnt, 0) AS comment_count, - COALESCE(ru.total_upvotes, 0) AS upvote_count, - (COALESCE(rb.cnt, 0) * 3 + COALESCE(rc.cnt, 0) * 2 + COALESCE(ru.total_upvotes, 0) * 1) AS activity_score - FROM games g - LEFT JOIN recent_benchmarks rb ON rb.game_id = g.id - LEFT JOIN recent_comments rc ON rc.game_id = g.id - LEFT JOIN recent_upvotes ru ON ru.game_id = g.id - WHERE (rb.cnt IS NOT NULL OR rc.cnt IS NOT NULL OR ru.total_upvotes IS NOT NULL) - ORDER BY activity_score DESC - LIMIT 10 - `) - - const bestNew = await db.execute(sql` - SELECT - g.id, - g.title, - g.capsule_image, - g.header_image, - g.release_date, - g.created_at, - g.playability_status, - AVG(pe.fps_avg) AS avg_fps, - COUNT(pe.id) AS benchmark_count - FROM games g - JOIN game_versions gv ON gv.game_id = g.id - JOIN performance_entries pe ON pe.version_id = gv.id - WHERE pe.is_removed = false - AND (g.created_at >= ${new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)} - OR g.release_date IS NOT NULL) - GROUP BY g.id, g.title, g.capsule_image, g.header_image, g.release_date, g.created_at, g.playability_status - HAVING COUNT(pe.id) >= 3 - ORDER BY avg_fps DESC - LIMIT 10 - `) - - const mostTested = await db.execute(sql` - SELECT - g.id, - g.title, - g.capsule_image, - g.header_image, - g.playability_status, - COUNT(pe.id) AS benchmark_count - FROM games g - JOIN game_versions gv ON gv.game_id = g.id - JOIN performance_entries pe ON pe.version_id = gv.id - WHERE pe.is_removed = false - GROUP BY g.id, g.title, g.capsule_image, g.header_image, g.playability_status - ORDER BY benchmark_count DESC - LIMIT 10 - `) - - const mostReported = await db.execute(sql` - SELECT - g.id, - g.title, - g.capsule_image, - g.header_image, - COUNT(DISTINCT r.id) AS report_count - FROM games g - JOIN game_versions gv ON gv.game_id = g.id - JOIN performance_entries pe ON pe.version_id = gv.id - JOIN reports r ON r.entry_id = pe.id - WHERE r.status = 'open' - GROUP BY g.id, g.title, g.capsule_image, g.header_image - ORDER BY report_count DESC - LIMIT 10 - `) - - // Serialize rows for the client component - const serializedTrending = trending.rows.map((row) => ({ - id: String(row.id), - title: String(row.title), - capsule_image: row.capsule_image ? String(row.capsule_image) : null, - header_image: row.header_image ? String(row.header_image) : null, - playability_status: row.playability_status ? String(row.playability_status) : null, - benchmark_count: Number(row.benchmark_count ?? 0), - comment_count: Number(row.comment_count ?? 0), - upvote_count: Number(row.upvote_count ?? 0), - activity_score: Number(row.activity_score ?? 0), - })) - - const serializedBestNew = bestNew.rows.map((row) => ({ - id: String(row.id), - title: String(row.title), - capsule_image: row.capsule_image ? String(row.capsule_image) : null, - header_image: row.header_image ? String(row.header_image) : null, - release_date: row.release_date ? new Date(String(row.release_date)).toISOString() : null, - created_at: row.created_at ? new Date(String(row.created_at)).toISOString() : null, - playability_status: row.playability_status ? String(row.playability_status) : null, - avg_fps: row.avg_fps ? Number(row.avg_fps) : null, - benchmark_count: Number(row.benchmark_count ?? 0), - })) - - const serializedMostTested = mostTested.rows.map((row) => ({ - id: String(row.id), - title: String(row.title), - capsule_image: row.capsule_image ? String(row.capsule_image) : null, - header_image: row.header_image ? String(row.header_image) : null, - playability_status: row.playability_status ? String(row.playability_status) : null, - benchmark_count: Number(row.benchmark_count ?? 0), - })) - - const serializedMostReported = mostReported.rows.map((row) => ({ - id: String(row.id), - title: String(row.title), - capsule_image: row.capsule_image ? String(row.capsule_image) : null, - header_image: row.header_image ? String(row.header_image) : null, - report_count: Number(row.report_count ?? 0), - })) - - return ( - - ) -} diff --git a/app/sitemap.ts b/app/sitemap.ts index 00f164e..b7f48bf 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -25,7 +25,6 @@ const STATIC_ENTRIES: Array<{ }> = [ { urlPath: "", changeFrequency: "weekly", priority: 1 }, { urlPath: "/games", changeFrequency: "daily", priority: 0.8 }, - { urlPath: "/dashboard", changeFrequency: "daily", priority: 0.7 }, { urlPath: "/devices", changeFrequency: "monthly", priority: 0.6 }, { urlPath: "/updates", changeFrequency: "weekly", priority: 0.5 }, { urlPath: "/contact", changeFrequency: "yearly", priority: 0.3 }, diff --git a/bun.lock b/bun.lock index af5d8c8..dd13644 100644 --- a/bun.lock +++ b/bun.lock @@ -7,6 +7,7 @@ "dependencies": { "@aws-sdk/client-s3": "^3.1036.0", "@better-auth/drizzle-adapter": "^1.6.9", + "@better-auth/expo": "^1.6.11", "@better-auth/passkey": "^1.6.9", "@elysia/cron": "^1.4.2", "@elysia/eden": "^1.4.10", @@ -192,6 +193,8 @@ "@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.6.9", "", { "peerDependencies": { "@better-auth/core": "^1.6.9", "@better-auth/utils": "0.4.0", "drizzle-orm": "^0.45.2" }, "optionalPeers": ["drizzle-orm"] }, "sha512-Lcco5hOGrMgc4XKAkvB6x72eQm4wCcya8IevMg4wBHY9W9GVg8pu23rpRX6VsVQSO4Ux13S7lFwUWtF7/r9aKw=="], + "@better-auth/expo": ["@better-auth/expo@1.6.11", "", { "dependencies": { "@better-fetch/fetch": "1.1.21", "better-call": "1.3.5", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/core": "^1.6.11", "better-auth": "^1.6.11", "expo-constants": ">=17.0.0", "expo-linking": ">=7.0.0", "expo-network": ">=8.0.7", "expo-web-browser": ">=14.0.0" }, "optionalPeers": ["expo-constants", "expo-linking", "expo-network", "expo-web-browser"] }, "sha512-ahqtpj5DRF4Tu8+PZuLPkR10Q6b8AntQNsn4LPcOIp6za5IJDAsaD/go1I5qCYIRi+8YKiIXk9vh4qQM54u+hA=="], + "@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.6.9", "", { "peerDependencies": { "@better-auth/core": "^1.6.9", "@better-auth/utils": "0.4.0", "kysely": "^0.28.14" }, "optionalPeers": ["kysely"] }, "sha512-gyjuuxJtZ4o9G9z9q4kqn24X2kvMSp7F+KHogYxF03SnXY/2WleAcuj57iC4wP3e9mGDbjPOrnM5K6Kr3Ktdpw=="], "@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.6.9", "", { "peerDependencies": { "@better-auth/core": "^1.6.9", "@better-auth/utils": "0.4.0" } }, "sha512-XmIG4tUnOXZ+KEcWjHUjOI9Z5donD09dC2t/AQTXifAUIqx7cySg86w0KTM09ArzAxRx1fCqO36Wkt5nULnrkQ=="], @@ -1752,7 +1755,7 @@ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "zod": ["zod@4.4.1", "", {}, "sha512-a6ENMBBGZBsnlSebQ/eKCguSBeGKSf4O7BPnqVPmYGtpBYI7VSqoVqw+QcB7kPRjbqPwhYTpFbVj/RqNz/CT0Q=="], "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], @@ -1774,6 +1777,10 @@ "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@better-auth/core/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + + "@better-auth/passkey/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], @@ -1784,12 +1791,6 @@ "@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], - "@serwist/build/zod": ["zod@4.4.1", "", {}, "sha512-a6ENMBBGZBsnlSebQ/eKCguSBeGKSf4O7BPnqVPmYGtpBYI7VSqoVqw+QcB7kPRjbqPwhYTpFbVj/RqNz/CT0Q=="], - - "@serwist/next/zod": ["zod@4.4.1", "", {}, "sha512-a6ENMBBGZBsnlSebQ/eKCguSBeGKSf4O7BPnqVPmYGtpBYI7VSqoVqw+QcB7kPRjbqPwhYTpFbVj/RqNz/CT0Q=="], - - "@serwist/webpack-plugin/zod": ["zod@4.4.1", "", {}, "sha512-a6ENMBBGZBsnlSebQ/eKCguSBeGKSf4O7BPnqVPmYGtpBYI7VSqoVqw+QcB7kPRjbqPwhYTpFbVj/RqNz/CT0Q=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], @@ -1808,6 +1809,8 @@ "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + "better-auth/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "echarts/tslib": ["tslib@2.3.0", "", {}, "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="], "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], @@ -1820,6 +1823,8 @@ "eslint-plugin-react/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "eslint-plugin-react-hooks/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "glob/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], diff --git a/components/dashboard/best-releases-chart.tsx b/components/dashboard/best-releases-chart.tsx deleted file mode 100644 index bb8d440..0000000 --- a/components/dashboard/best-releases-chart.tsx +++ /dev/null @@ -1,106 +0,0 @@ -"use client" - -import { EChartWrapper, CHART_THEME } from "@/components/charts/EChartWrapper" -import type { EChartsOption } from "echarts" - -interface BestRelease { - id: string - title: string - avg_fps: number | null - benchmark_count: number -} - -function buildOption(games: BestRelease[]): EChartsOption { - const data = games.map((g) => ({ - name: g.title, - value: [g.benchmark_count, Math.round((g.avg_fps ?? 0) * 10) / 10, g.benchmark_count], - itemStyle: { - color: { - type: "radial" as const, - x: 0.5, - y: 0.5, - r: 0.5, - colorStops: [ - { offset: 0, color: CHART_THEME.accent }, - { offset: 1, color: CHART_THEME.primary }, - ], - }, - shadowBlur: 10, - shadowColor: CHART_THEME.primary + "40", - }, - })) - - return { - backgroundColor: CHART_THEME.bg, - tooltip: { - trigger: "item", - backgroundColor: "#1a1225", - borderColor: CHART_THEME.border, - textStyle: { color: CHART_THEME.text }, - formatter: (params: unknown) => { - const p = params as { name: string; value: number[] } - const v = p.value - return `
${p.name}
-
Avg FPS: ${v[1]}
-
Benchmarks: ${v[2]}
` - }, - }, - grid: { - left: "3%", - right: "6%", - bottom: "10%", - top: "10%", - containLabel: true, - }, - xAxis: { - name: "Benchmarks", - nameLocation: "middle", - nameGap: 24, - nameTextStyle: { color: CHART_THEME.textMuted, fontSize: 12 }, - type: "value", - splitLine: { lineStyle: { color: CHART_THEME.border, type: "dashed" } }, - axisLabel: { color: CHART_THEME.textMuted, fontSize: 11 }, - axisLine: { lineStyle: { color: CHART_THEME.border } }, - axisTick: { show: false }, - }, - yAxis: { - name: "Avg FPS", - nameTextStyle: { color: CHART_THEME.textMuted, fontSize: 12 }, - type: "value", - splitLine: { lineStyle: { color: CHART_THEME.border, type: "dashed" } }, - axisLabel: { color: CHART_THEME.textMuted, fontSize: 11 }, - axisLine: { lineStyle: { color: CHART_THEME.border } }, - axisTick: { show: false }, - }, - series: [ - { - type: "scatter", - data, - symbolSize: (val: number[]) => Math.max(12, Math.min(40, val[2] * 3)), - label: { - show: true, - formatter: (p: unknown) => (p as { name: string }).name, - position: "top", - color: CHART_THEME.text, - fontSize: 10, - overflow: "truncate", - width: 100, - }, - emphasis: { - scale: 1.5, - itemStyle: { - shadowBlur: 20, - shadowColor: CHART_THEME.accent + "60", - }, - }, - }, - ], - animationDuration: 800, - animationEasing: "cubicOut", - } -} - -export function BestReleasesChart({ games }: { games: BestRelease[] }) { - if (!games.length) return null - return -} diff --git a/components/dashboard/dashboard-client.tsx b/components/dashboard/dashboard-client.tsx deleted file mode 100644 index 6be1074..0000000 --- a/components/dashboard/dashboard-client.tsx +++ /dev/null @@ -1,117 +0,0 @@ -"use client" - -import { TrendingUpIcon, SparklesIcon, BarChart3Icon } from "lucide-react" -import { TrendingGamesChart } from "./trending-games-chart" -import { BestReleasesChart } from "./best-releases-chart" -import { MostTestedChart } from "./most-tested-chart" - -interface TrendingGame { - id: string - title: string - capsule_image: string | null - header_image: string | null - playability_status: string | null - benchmark_count: number - comment_count: number - upvote_count: number - activity_score: number -} - -interface BestRelease { - id: string - title: string - capsule_image: string | null - header_image: string | null - release_date: string | null - created_at: string | null - playability_status: string | null - avg_fps: number | null - benchmark_count: number -} - -interface MostTestedGame { - id: string - title: string - capsule_image: string | null - header_image: string | null - playability_status: string | null - benchmark_count: number -} - -interface MostReportedGame { - id: string - title: string - capsule_image: string | null - header_image: string | null - report_count: number -} - -interface DashboardClientProps { - trending: TrendingGame[] - bestNew: BestRelease[] - mostTested: MostTestedGame[] - mostReported: MostReportedGame[] -} - -export function DashboardClient({ - trending, - bestNew, - mostTested, - mostReported, -}: DashboardClientProps) { - return ( -
-
-

- Community Dashboard -

-

- Real-time insights from the DeckyVault community — trending games, top - performers, and activity highlights. -

-
- - {/* Trending Games */} -
-
- -

- Trending Games (7 days) -

-
-

- Top games by community activity: benchmarks, comments, and upvotes. -

- -
- - {/* Best New Releases */} -
-
- -

- Best Performing New Releases -

-
-

- Newly added games with the highest average FPS (minimum 3 benchmarks). -

- -
- - {/* Most Tested & Most Reported */} -
-
- -

- Most Tested & Most Reported -

-
-

- Games with the most benchmarks submitted and the most open reports. -

- -
-
- ) -} diff --git a/components/dashboard/most-tested-chart.tsx b/components/dashboard/most-tested-chart.tsx deleted file mode 100644 index db7d828..0000000 --- a/components/dashboard/most-tested-chart.tsx +++ /dev/null @@ -1,159 +0,0 @@ -"use client" - -import { EChartWrapper, CHART_THEME } from "@/components/charts/EChartWrapper" -import type { EChartsOption } from "echarts" - -interface MostTestedGame { - id: string - title: string - benchmark_count: number -} - -interface MostReportedGame { - id: string - title: string - report_count: number -} - -function buildOption( - tested: MostTestedGame[], - reported: MostReportedGame[], -): EChartsOption { - const testedTitles = tested.map((g) => g.title) - const testedCounts = tested.map((g) => g.benchmark_count) - const reportedTitles = reported.map((g) => g.title) - const reportedCounts = reported.map((g) => g.report_count) - - return { - backgroundColor: CHART_THEME.bg, - tooltip: { - trigger: "axis", - axisPointer: { type: "shadow" }, - backgroundColor: "#1a1225", - borderColor: CHART_THEME.border, - textStyle: { color: CHART_THEME.text }, - }, - legend: { - data: ["Most Tested", "Most Reported"], - textStyle: { color: CHART_THEME.textMuted }, - bottom: 0, - }, - grid: [ - { - left: "3%", - right: "52%", - bottom: "14%", - top: "8%", - containLabel: true, - }, - { - left: "52%", - right: "3%", - bottom: "14%", - top: "8%", - containLabel: true, - }, - ], - xAxis: [ - { - type: "value", - gridIndex: 0, - splitLine: { lineStyle: { color: CHART_THEME.border, type: "dashed" } }, - axisLabel: { color: CHART_THEME.textMuted, fontSize: 10 }, - axisLine: { show: false }, - axisTick: { show: false }, - }, - { - type: "value", - gridIndex: 1, - splitLine: { lineStyle: { color: CHART_THEME.border, type: "dashed" } }, - axisLabel: { color: CHART_THEME.textMuted, fontSize: 10 }, - axisLine: { show: false }, - axisTick: { show: false }, - }, - ], - yAxis: [ - { - type: "category", - gridIndex: 0, - data: testedTitles, - axisLabel: { color: CHART_THEME.text, fontSize: 11, width: 120, overflow: "truncate" }, - axisLine: { show: false }, - axisTick: { show: false }, - }, - { - type: "category", - gridIndex: 1, - data: reportedTitles, - axisLabel: { color: CHART_THEME.text, fontSize: 11, width: 120, overflow: "truncate" }, - axisLine: { show: false }, - axisTick: { show: false }, - }, - ], - series: [ - { - name: "Most Tested", - type: "bar", - xAxisIndex: 0, - yAxisIndex: 0, - data: testedCounts, - barWidth: "55%", - itemStyle: { - borderRadius: [0, 4, 4, 0], - color: { - type: "linear", - x: 0, - y: 0, - x2: 1, - y2: 0, - colorStops: [ - { offset: 0, color: CHART_THEME.info }, - { offset: 1, color: CHART_THEME.success }, - ], - }, - }, - }, - { - name: "Most Reported", - type: "bar", - xAxisIndex: 1, - yAxisIndex: 1, - data: reportedCounts, - barWidth: "55%", - itemStyle: { - borderRadius: [0, 4, 4, 0], - color: { - type: "linear", - x: 0, - y: 0, - x2: 1, - y2: 0, - colorStops: [ - { offset: 0, color: CHART_THEME.warning }, - { offset: 1, color: CHART_THEME.accent }, - ], - }, - }, - }, - ], - animationDuration: 800, - animationEasing: "cubicOut", - } -} - -export function MostTestedChart({ - mostTested, - mostReported, -}: { - mostTested: MostTestedGame[] - mostReported: MostReportedGame[] -}) { - if (!mostTested.length && !mostReported.length) return null - return ( - - ) -} diff --git a/components/dashboard/trending-games-chart.tsx b/components/dashboard/trending-games-chart.tsx deleted file mode 100644 index bd5d2a0..0000000 --- a/components/dashboard/trending-games-chart.tsx +++ /dev/null @@ -1,107 +0,0 @@ -"use client" - -import { EChartWrapper, CHART_THEME } from "@/components/charts/EChartWrapper" -import type { EChartsOption } from "echarts" - -interface TrendingGame { - id: string - title: string - activity_score: number - benchmark_count: number - comment_count: number - upvote_count: number -} - -function buildOption(games: TrendingGame[]): EChartsOption { - const sorted = [...games].reverse() - const titles = sorted.map((g) => g.title) - const scores = sorted.map((g) => g.activity_score) - const benchmarks = sorted.map((g) => g.benchmark_count) - - return { - backgroundColor: CHART_THEME.bg, - tooltip: { - trigger: "axis", - axisPointer: { type: "shadow" }, - backgroundColor: "#1a1225", - borderColor: CHART_THEME.border, - textStyle: { color: CHART_THEME.text }, - formatter: (params: unknown) => { - const p = params as { dataIndex: number }[] - const idx = p[0].dataIndex - const g = sorted[idx] - return `
${g.title}
-
Activity Score: ${g.activity_score}
-
-Benchmarks: ${g.benchmark_count} · Comments: ${g.comment_count} · Upvotes: ${g.upvote_count} -
` - }, - }, - grid: { - left: "3%", - right: "4%", - bottom: "3%", - top: "3%", - containLabel: true, - }, - xAxis: { - type: "value", - splitLine: { lineStyle: { color: CHART_THEME.border, type: "dashed" } }, - axisLabel: { color: CHART_THEME.textMuted, fontSize: 11 }, - axisLine: { show: false }, - axisTick: { show: false }, - }, - yAxis: { - type: "category", - data: titles, - axisLabel: { color: CHART_THEME.text, fontSize: 12, width: 160, overflow: "truncate" }, - axisLine: { show: false }, - axisTick: { show: false }, - }, - series: [ - { - name: "Activity Score", - type: "bar", - data: scores, - barWidth: "60%", - itemStyle: { - borderRadius: [0, 4, 4, 0], - color: { - type: "linear", - x: 0, - y: 0, - x2: 1, - y2: 0, - colorStops: [ - { offset: 0, color: CHART_THEME.secondary }, - { offset: 1, color: CHART_THEME.primary }, - ], - }, - }, - }, - { - name: "Benchmarks", - type: "bar", - data: benchmarks, - barWidth: "60%", - barGap: "-100%", - itemStyle: { - borderRadius: [0, 4, 4, 0], - color: "transparent", - borderWidth: 1, - borderColor: CHART_THEME.accent, - }, - emphasis: { disabled: true }, - tooltip: { show: false }, - silent: true, - }, - ], - animationDuration: 800, - animationEasing: "cubicOut", - } -} - -export function TrendingGamesChart({ games }: { games: TrendingGame[] }) { - if (!games.length) return null - return -} diff --git a/lib/steamdb/scrape.ts b/lib/steamdb/scrape.ts index 2ff6353..6cc3c22 100644 --- a/lib/steamdb/scrape.ts +++ b/lib/steamdb/scrape.ts @@ -7,6 +7,18 @@ interface ScrapeResult { buildId: string | null } +/** + * Scrape version/build info from SteamDB for a given Steam App ID. + * + * SteamDB is a client-side rendered app — the raw HTML from a fetch() is a + * skeleton that gets populated by JavaScript. To work around this we: + * 1. Search ALL