From ea782af079829d568b44ebe2897ebfad164f977f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Tue, 3 Feb 2026 09:04:36 -0600 Subject: [PATCH] feat: popular tanks --- bun.lock | 3 + packages/core/src/index.ts | 1 - packages/website-ue/package.json | 3 +- packages/website-ue/src/core/api/abstract.ts | 17 +++- packages/website-ue/src/core/api/server.ts | 95 +++++++++++++++++-- .../src/pages/[...locale]/tanks/index.astro | 3 +- .../src/pages/api/tanks/popular.json.ts | 7 ++ packages/website-ue/src/protos/tank.proto | 8 +- packages/website-ue/src/types/popularTanks.ts | 4 + 9 files changed, 125 insertions(+), 16 deletions(-) create mode 100644 packages/website-ue/src/pages/api/tanks/popular.json.ts create mode 100644 packages/website-ue/src/types/popularTanks.ts diff --git a/bun.lock b/bun.lock index b320bb3a6..f1749fec1 100644 --- a/bun.lock +++ b/bun.lock @@ -211,6 +211,7 @@ "autoprefixer": "^10.4.23", "cssnano": "^7.1.2", "fuzzysort": "^3.1.0", + "googleapis": "^171.1.0", "immer": "^10.1.3", "lodash-es": "^4.17.21", "postcss": "^8.5.6", @@ -2964,6 +2965,8 @@ "@blitzkit/website/vite": ["vite@7.3.0", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg=="], + "@blitzkit/website-ue/googleapis": ["googleapis@171.1.0", "", { "dependencies": { "google-auth-library": "^10.2.0", "googleapis-common": "^8.0.0" } }, "sha512-2f3O75VjbKRnvwN5Pwi6ZZEcnOiO10ZfPSX19oE2ehxC8689NxWZLMPKsap7qgT48adu2NEA8tlUQZK2nt0EDA=="], + "@blitzkit/website-ue/vite": ["vite@7.3.0", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg=="], "@discordjs/rest/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 5550ecc1a..d070608a3 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -4,7 +4,6 @@ export * from "./imgur"; export * from "./math"; export * from "./patreon"; export * from "./protobuf"; -export * from "./protos"; export * from "./statistics"; export * from "./streams"; export * from "./types"; diff --git a/packages/website-ue/package.json b/packages/website-ue/package.json index f102c82ed..dd5795cc2 100644 --- a/packages/website-ue/package.json +++ b/packages/website-ue/package.json @@ -15,8 +15,8 @@ "@astrolib/seo": "^1.0.0-beta.8", "@blitzkit/closed": "workspace:*", "@blitzkit/core": "workspace:*", - "@blitzkit/i18n": "workspace:*", "@blitzkit/game": "workspace:*", + "@blitzkit/i18n": "workspace:*", "@bufbuild/protobuf": "^2.10.2", "@fontsource-variable/roboto-flex": "^5.2.8", "@fontsource-variable/roboto-mono": "^5.2.8", @@ -34,6 +34,7 @@ "autoprefixer": "^10.4.23", "cssnano": "^7.1.2", "fuzzysort": "^3.1.0", + "googleapis": "^171.1.0", "immer": "^10.1.3", "lodash-es": "^4.17.21", "postcss": "^8.5.6", diff --git a/packages/website-ue/src/core/api/abstract.ts b/packages/website-ue/src/core/api/abstract.ts index 9c183294a..54396ca35 100644 --- a/packages/website-ue/src/core/api/abstract.ts +++ b/packages/website-ue/src/core/api/abstract.ts @@ -7,6 +7,7 @@ import type { Backgrounds } from "../../protos/backgrounds"; import type { Tank } from "../../protos/tank"; import type { TankList } from "../../protos/tank_list"; import type { Tanks } from "../../protos/tanks"; +import type { PopularTanks } from "../../types/popularTanks"; export abstract class AbstractAPI { private _tankListCache: TankList | undefined; @@ -41,7 +42,7 @@ export abstract class AbstractAPI { private assertLocale(locale: string) { const supported = locales.supported.find( - (supported) => supported.locale === locale + (supported) => supported.locale === locale, ); if (supported === undefined) { @@ -65,7 +66,7 @@ export abstract class AbstractAPI { private _gameStringsCache: Record> = {}; protected abstract _gameStrings( - locale: string + locale: string, ): Promise>; async gameStrings(locale: string) { this.assertLocale(locale); @@ -83,7 +84,7 @@ export abstract class AbstractAPI { > = {}; protected abstract _groupedGameStrings( locale: string, - group: string + group: string, ): Promise>; async groupedGameStrings(locale: string, group: string, prefix: boolean) { this.assertLocale(locale); @@ -158,4 +159,14 @@ export abstract class AbstractAPI { return this._gameStringGroupsCache; } + + private _popularTanksCache: PopularTanks | undefined; + protected abstract _popularTanks(): Promise; + async popularTanks() { + if (this._popularTanksCache === undefined) { + this._popularTanksCache = await this._popularTanks(); + } + + return this._popularTanksCache; + } } diff --git a/packages/website-ue/src/core/api/server.ts b/packages/website-ue/src/core/api/server.ts index f1da53d10..a15342a95 100644 --- a/packages/website-ue/src/core/api/server.ts +++ b/packages/website-ue/src/core/api/server.ts @@ -1,8 +1,12 @@ import type { MetadataAccessor } from "@blitzkit/closed"; -import { sluggify, type DeepPartial } from "@blitzkit/core"; +import { assertSecret, sluggify } from "@blitzkit/core"; import type { Strings } from "@blitzkit/i18n"; import locales from "@blitzkit/i18n/locales.json"; import type { RemoteStorageComponent } from "@protos/blitz_static_remote_storage_component"; +import type { DeepPartial } from "@protos/blitz_static_reward_currency"; +import { existsSync } from "fs"; +import { mkdir, readFile, writeFile } from "fs/promises"; +import { google } from "googleapis"; import { merge } from "lodash-es"; import { parse } from "yaml"; import type { Avatar } from "../../protos/avatar"; @@ -10,6 +14,7 @@ import type { Background } from "../../protos/background"; import type { Tank } from "../../protos/tank"; import type { TankListEntry } from "../../protos/tank_list"; import type { Tanks } from "../../protos/tanks"; +import type { PopularTanks } from "../../types/popularTanks"; import { AbstractAPI } from "./abstract"; if (typeof window !== "undefined") { @@ -19,6 +24,7 @@ if (typeof window !== "undefined") { const globbedStrings = import.meta.glob("../../../../i18n/strings/*.json", { import: "default", }); +const tankSlugPattern = /^(\/\w+)?\/tanks\/([a-z0-9-]+)\/$/; interface LocalizationConfig { namespaces: string[]; @@ -34,7 +40,7 @@ export class ServerAPI extends AbstractAPI { if (group.length !== 1) { throw new RangeError( - `Don't know how to handle ${group.length} ClientConfigsEntities` + `Don't know how to handle ${group.length} ClientConfigsEntities`, ); } @@ -76,7 +82,7 @@ export class ServerAPI extends AbstractAPI { Object.assign( strings, await fetch( - `${remoteStorage.url}${remoteStorage.relative_path}/${namespace}/${locale}.yaml` + `${remoteStorage.url}${remoteStorage.relative_path}/${namespace}/${locale}.yaml`, ) .then((response) => response.text()) .then((text) => { @@ -88,13 +94,13 @@ export class ServerAPI extends AbstractAPI { } return strings; - }) + }), ); } } catch (error) { console.warn( `Failed to fetch config from ${configPath}. Returning empty strings. This will throw an error in production. Error:`, - error + error, ); } @@ -174,7 +180,7 @@ export class ServerAPI extends AbstractAPI { const tank = item.TankCatalog(); const compensation = item.Compensation(); - return { tank, compensation, slug } satisfies Tank; + return { id, slug, tank, compensation } satisfies Tank; } protected async _avatars() { @@ -232,4 +238,81 @@ export class ServerAPI extends AbstractAPI { return Array.from(groups.values()); } + + protected async _popularTanks() { + if (!existsSync("../../temp/popular.json")) { + const { tanks } = await this.tanks(); + const tanksArray = Object.values(tanks); + + const auth = await google.auth.getClient({ + keyFile: assertSecret(import.meta.env.GOOGLE_APPLICATION_CREDENTIALS), + scopes: ["https://www.googleapis.com/auth/analytics.readonly"], + }); + const analytics = google.analyticsdata({ version: "v1beta", auth }); + const report = await analytics.properties.runReport({ + property: `properties/${assertSecret( + import.meta.env.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID, + )}`, + requestBody: { + dateRanges: [{ startDate: "7daysAgo", endDate: "today" }], + dimensions: [{ name: "pagePath" }], + metrics: [{ name: "screenPageViews" }], + orderBys: [{ metric: { metricName: "screenPageViews" }, desc: true }], + dimensionFilter: { + filter: { + fieldName: "pagePath", + stringFilter: { + matchType: "BEGINS_WITH", + value: "/tanks/", + }, + }, + }, + }, + }); + + const views: Record = {}; + + if (!report.data.rows) { + throw new Error("No rows in report"); + } + + for (const row of report.data.rows) { + if ( + !row.dimensionValues || + !row.dimensionValues[0].value || + !row.metricValues || + !row.metricValues[0].value + ) { + continue; + } + + const matches = row.dimensionValues[0].value.match(tankSlugPattern); + + if (!matches) continue; + + const slug = matches[2]; + const tank = tanksArray.find((tank) => tank.slug === slug); + + if (!tank) continue; + + if (tank.id in views) { + views[tank.id] += Number(row.metricValues[0].value); + } else { + views[tank.id] = Number(row.metricValues[0].value); + } + } + + const popularTanks: PopularTanks = Object.entries(views) + .sort(([, a], [, b]) => b - a) + .map(([id, views]) => ({ id, views })); + + await mkdir("../../temp", { recursive: true }); + await writeFile("../../temp/popular.json", JSON.stringify(popularTanks)); + + return popularTanks; + } + + const data = await readFile("../../temp/popular.json", "utf-8"); + return JSON.parse(data) as PopularTanks; + } } diff --git a/packages/website-ue/src/pages/[...locale]/tanks/index.astro b/packages/website-ue/src/pages/[...locale]/tanks/index.astro index c9b725df4..117fe0908 100644 --- a/packages/website-ue/src/pages/[...locale]/tanks/index.astro +++ b/packages/website-ue/src/pages/[...locale]/tanks/index.astro @@ -11,7 +11,7 @@ const { locale } = Astro.props; const tankList = await api.tankList(); --- - +

tanks

@@ -20,6 +20,7 @@ const tankList = await api.tankList(); tankList.list.map(({ id, slug }) => (

+ {slug} ({id})

diff --git a/packages/website-ue/src/pages/api/tanks/popular.json.ts b/packages/website-ue/src/pages/api/tanks/popular.json.ts new file mode 100644 index 000000000..b3429c1c2 --- /dev/null +++ b/packages/website-ue/src/pages/api/tanks/popular.json.ts @@ -0,0 +1,7 @@ +import { api } from "../../../core/api/dynamic"; + +export async function GET() { + throw new Error("MAKE THIS A PROTO AND ADD LAST UPDATED TIME"); + const popular = await api.popularTanks(); + return Response.json(popular); +} diff --git a/packages/website-ue/src/protos/tank.proto b/packages/website-ue/src/protos/tank.proto index c1cdeae6b..141611d01 100644 --- a/packages/website-ue/src/protos/tank.proto +++ b/packages/website-ue/src/protos/tank.proto @@ -6,9 +6,9 @@ import "blitz_static_compensation_component.proto"; import "blitz_static_tank_component.proto"; message Tank { - blitz_static_tank_component.TankCatalogComponent tank = 1; - blitz_static_compensation_component.CompensationComponent compensation = 2; + string id = 1; + string slug = 2; - string id = 3; - string slug = 4; + blitz_static_tank_component.TankCatalogComponent tank = 3; + blitz_static_compensation_component.CompensationComponent compensation = 4; } \ No newline at end of file diff --git a/packages/website-ue/src/types/popularTanks.ts b/packages/website-ue/src/types/popularTanks.ts new file mode 100644 index 000000000..e2b49b35d --- /dev/null +++ b/packages/website-ue/src/types/popularTanks.ts @@ -0,0 +1,4 @@ +export type PopularTanks = { + id: string; + views: number; +}[];