feat: popular tanks

This commit is contained in:
TrèsAbhi committed 2026-02-03 09:04:36 -06:00
1 parent 0ebf9f6c3d
commit ea782af079
9 files changed
+125 -16

No files matched your search

+3
View File
@@ -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": ["[email protected]", "", { "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": ["[email protected]", "", { "dependencies": { "google-auth-library": "^10.2.0", "googleapis-common": "^8.0.0" } }, "sha512-2f3O75VjbKRnvwN5Pwi6ZZEcnOiO10ZfPSX19oE2ehxC8689NxWZLMPKsap7qgT48adu2NEA8tlUQZK2nt0EDA=="],
"@blitzkit/website-ue/vite": ["[email protected]", "", { "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/[email protected]", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
-1
View File
@@ -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";
+2 -1
View File
@@ -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",
+14 -3
View File
@@ -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<string, Record<string, string>> = {};
protected abstract _gameStrings(
locale: string
locale: string,
): Promise<Record<string, string>>;
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<Record<string, string>>;
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<PopularTanks>;
async popularTanks() {
if (this._popularTanksCache === undefined) {
this._popularTanksCache = await this._popularTanks();
}
return this._popularTanksCache;
}
}
+89 -6
View File
@@ -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<string, number> = {};
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;
}
}
@@ -11,7 +11,7 @@ const { locale } = Astro.props;
const tankList = await api.tankList();
---
<BaseLayout selectedTool={tools.tanks} {locale} accent="purple">
<BaseLayout {locale} accent="purple">
<ToolBanner {locale} tool={tools.tanks} slot="pre-app" />
<h1>tanks</h1>
@@ -20,6 +20,7 @@ const tankList = await api.tankList();
tankList.list.map(({ id, slug }) => (
<p>
<a href={`/tanks/${slug}`}>
<img height="128" width="128" src={`/api/tanks/${id}.webp`} />
{slug} ({id})
</a>
</p>
@@ -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);
}
+4 -4
View File
@@ -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;
}
@@ -0,0 +1,4 @@
export type PopularTanks = {
id: string;
views: number;
}[];