mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 10:47:04 +02:00
feat: popular tanks
This commit is contained in:
1 parent
0ebf9f6c3d
commit
ea782af079
9 files changed
+125
-16
No files matched your search
@@ -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=="],
|
||||
|
||||
@@ -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";
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}[];
|
||||
Reference in new issue
Block a user