Add PC post-match player pages with opt-in public OSS sync.

Generate /players/{account_id}[/{match_id}] locally after POST_GAME via OpenDota; publish to OSS only when public_share is enabled.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
voson
2026-07-31 11:01:32 +08:00
co-authored by Cursor
parent d6f7c3f0f5
commit 4a61aeeb26
16 changed files with 1684 additions and 23 deletions
+30 -2
View File
@@ -4,7 +4,7 @@
* Path-based router for the Climperor web site (web/frontend).
*
* Synchronizes the browser URL with app state across these dimensions:
* - page: heroes | rankings | matches | streamers | trends | mechanics | items | patches
* - page: heroes | rankings | matches | streamers | trends | mechanics | items | patches | players
* - hero: selected hero key + detail sub-tab
* (skills|core|fears|trends|matchups|matches|streamers|patches; legacy stats → trends)
* - rankings: Immortal leaderboard region
@@ -12,6 +12,8 @@
* - matches: star-player recent matches (pro_matches)
* /matches[/account_id][?origin=pro|china][&page=N]
* (default origin all omitted; page=1 omitted)
* - players: PC post-match player home + match detail (local/OSS JSON)
* /players/{account_id}[/{match_id}]
* - streamers: curated streamer directory
* /streamers
* - trends: medal bracket for the 8-week win/pick board
@@ -38,7 +40,17 @@
*/
const ROUTE_DEFAULT = "/heroes";
const VALID_PAGES = ["heroes", "rankings", "matches", "streamers", "trends", "mechanics", "items", "patches"];
const VALID_PAGES = [
"heroes",
"rankings",
"matches",
"streamers",
"trends",
"mechanics",
"items",
"patches",
"players",
];
const VALID_DETAIL_TABS = [
"skills",
"core",
@@ -114,6 +126,8 @@ function parseHash(hashOrPath) {
matchesPlayerId: null,
matchesOrigin: null,
matchesPage: null,
playerAccountId: null,
playerMatchId: null,
trendsBracket: null,
trendsSort: null,
mechanicEffect: null,
@@ -147,6 +161,13 @@ function parseHash(hashOrPath) {
if (segs[1] && /^\d+$/.test(segs[1])) {
out.matchesPlayerId = segs[1];
}
} else if (page === "players") {
if (segs[1] && /^\d+$/.test(segs[1])) {
out.playerAccountId = segs[1];
}
if (segs[2] && /^\d+$/.test(segs[2])) {
out.playerMatchId = segs[2];
}
} else if (page === "trends") {
if (segs[1]) out.trendsBracket = safeDecode(segs[1]);
} else if (page === "mechanics") {
@@ -219,6 +240,13 @@ function serializeHash(state) {
if (state.matchesPlayerId) {
path += "/" + encodeURIComponent(String(state.matchesPlayerId));
}
} else if (state.page === "players") {
if (state.playerAccountId) {
path += "/" + encodeURIComponent(String(state.playerAccountId));
if (state.playerMatchId) {
path += "/" + encodeURIComponent(String(state.playerMatchId));
}
}
} else if (state.page === "trends") {
// Omit bracket when it is the default (legend) — bare /trends means legend.
const bracket = state.trendsBracket || DEFAULT_TRENDS_BRACKET;