mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-11-01 23:10:37 +01:00
feat: more login progress
This commit is contained in:
1 parent
1e64cb9e7d
commit
513105a624
18 files changed
+290
-295
No files matched your search
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "blitzkit",
|
"name": "blitzkit",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.22.6",
|
"version": "2.0.0",
|
||||||
"description": "🎉 Tools for everything World of Tanks Blitz",
|
"description": "🎉 Tools for everything World of Tanks Blitz",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"types": "./types.d.ts",
|
"types": "./types.d.ts",
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export * from './modelDefinitions';
|
|||||||
export * from './normalizeBoundingBox';
|
export * from './normalizeBoundingBox';
|
||||||
// export * from './parseBkni';
|
// export * from './parseBkni';
|
||||||
// export * from './patientFetch';
|
// export * from './patientFetch';
|
||||||
// export * from './patientFetchJSON';
|
export * from './patientFetchJSON';
|
||||||
export * from './progressiveStat';
|
export * from './progressiveStat';
|
||||||
export * from './provisionDefinitions';
|
export * from './provisionDefinitions';
|
||||||
export * from './resolveDpm';
|
export * from './resolveDpm';
|
||||||
|
|||||||
@@ -1,194 +0,0 @@
|
|||||||
import {
|
|
||||||
BkrlBaseEntry,
|
|
||||||
BkrlFormat,
|
|
||||||
BkrlSuperset1Entry,
|
|
||||||
BkrlWriteStream,
|
|
||||||
CdonValue,
|
|
||||||
CdonWriteStream,
|
|
||||||
getAccountInfo,
|
|
||||||
patientFetchJSON,
|
|
||||||
RatingInfo,
|
|
||||||
RatingNeighbors,
|
|
||||||
RatingPlayer,
|
|
||||||
RegionSubdomain,
|
|
||||||
regionSubdomainToRegion,
|
|
||||||
superCompress,
|
|
||||||
} from '@blitzkit/core';
|
|
||||||
import { chunk } from 'lodash-es';
|
|
||||||
import { argv } from 'process';
|
|
||||||
import { commitAssets } from './core/github/commitAssets';
|
|
||||||
import { FileChange } from './core/github/commitMultipleFiles';
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Central North American Time (UTC-5): use 0 5 * * *.
|
|
||||||
* Central European Time (UTC+1): use 0 23 * * *.
|
|
||||||
* Central Asia Standard Time (UTC+7): use 0 17 * * *.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const production = argv.includes('--production');
|
|
||||||
const target = argv
|
|
||||||
.find((arg) => arg.startsWith('--target='))
|
|
||||||
?.split('=')[1] as 'midnight' | 'latest' | undefined;
|
|
||||||
const regionSubdomains = argv
|
|
||||||
.find((arg) => arg.startsWith('--region='))
|
|
||||||
?.split('=')[1];
|
|
||||||
|
|
||||||
if (!target) throw new Error('Target parameter not specified');
|
|
||||||
if (!regionSubdomains) throw new Error('Region parameter not specified');
|
|
||||||
|
|
||||||
regionSubdomains.split(',').forEach(async (regionSubdomain) => {
|
|
||||||
const region = regionSubdomainToRegion(regionSubdomain as RegionSubdomain);
|
|
||||||
|
|
||||||
console.log(`Building ${target} rating leaderboard for ${region}`);
|
|
||||||
|
|
||||||
const NEIGHBORS = 2 ** 8;
|
|
||||||
const PLAYERS = await patientFetchJSON<RatingInfo>(
|
|
||||||
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/season/`,
|
|
||||||
).then((data) => (data.detail === undefined ? data.count : undefined));
|
|
||||||
|
|
||||||
if (PLAYERS === undefined) {
|
|
||||||
throw new Error('No current rating season running');
|
|
||||||
}
|
|
||||||
|
|
||||||
const players: Record<number, RatingPlayer> = {};
|
|
||||||
const leaderboard: BkrlBaseEntry[] = [];
|
|
||||||
let registered = 0;
|
|
||||||
|
|
||||||
async function branchFromPlayer(id: number) {
|
|
||||||
const { neighbors } = await patientFetchJSON<RatingNeighbors>(
|
|
||||||
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${NEIGHBORS}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const firstPlayer = neighbors[0];
|
|
||||||
const lastPlayer = neighbors[neighbors.length - 1];
|
|
||||||
|
|
||||||
const branches: Promise<void>[] = [];
|
|
||||||
if (!players[firstPlayer.number])
|
|
||||||
branches.push(branchFromPlayer(firstPlayer.spa_id));
|
|
||||||
if (!players[lastPlayer.number])
|
|
||||||
branches.push(branchFromPlayer(lastPlayer.spa_id));
|
|
||||||
|
|
||||||
neighbors.forEach((neighbor) => {
|
|
||||||
if (players[neighbor.number]) return;
|
|
||||||
|
|
||||||
players[neighbor.number] = neighbor;
|
|
||||||
registered++;
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`Registered ${registered} / ${PLAYERS} (${(
|
|
||||||
(registered / PLAYERS!) *
|
|
||||||
100
|
|
||||||
).toFixed(2)}%)`,
|
|
||||||
);
|
|
||||||
|
|
||||||
await Promise.all(branches);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function branchFromLeague(id: number) {
|
|
||||||
const { result } = await patientFetchJSON<{ result: RatingPlayer[] }>(
|
|
||||||
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/league/${id}/top/`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const firstPlayer = result[0];
|
|
||||||
const lastPlayer = result.at(-1)!;
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
branchFromPlayer(firstPlayer.spa_id),
|
|
||||||
branchFromPlayer(lastPlayer.spa_id),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Branching from leagues...');
|
|
||||||
await Promise.all([
|
|
||||||
branchFromLeague(0),
|
|
||||||
branchFromLeague(1),
|
|
||||||
branchFromLeague(2),
|
|
||||||
branchFromLeague(3),
|
|
||||||
branchFromLeague(4),
|
|
||||||
]);
|
|
||||||
|
|
||||||
console.log('Converting to an array...');
|
|
||||||
for (let index = 0; index < PLAYERS; index++) {
|
|
||||||
const listing = players[index + 1];
|
|
||||||
if (listing) {
|
|
||||||
leaderboard[index] = { id: listing.spa_id, score: listing.score };
|
|
||||||
} else {
|
|
||||||
console.warn(`Position ${index + 1} had no player`);
|
|
||||||
leaderboard[index] = { id: 0, score: 0 };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Requesting further stats...');
|
|
||||||
const comprehensiveLeaderboard: BkrlSuperset1Entry[] = [];
|
|
||||||
let chunkIndex = 0;
|
|
||||||
for (const leaderboardChunk of chunk(leaderboard, 100)) {
|
|
||||||
console.log(
|
|
||||||
`Chunk ${chunkIndex + 1} of ${Math.ceil(leaderboard.length / 100)}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const stats = await getAccountInfo(
|
|
||||||
region,
|
|
||||||
leaderboardChunk.map(({ id }) => id),
|
|
||||||
['statistics.rating'],
|
|
||||||
);
|
|
||||||
|
|
||||||
stats.forEach((stat, index) => {
|
|
||||||
const trueIndex = chunkIndex * 100 + index;
|
|
||||||
comprehensiveLeaderboard[trueIndex] =
|
|
||||||
leaderboardChunk[index].id === 0 ||
|
|
||||||
stat?.statistics.rating === undefined
|
|
||||||
? {
|
|
||||||
battles: 0,
|
|
||||||
damage: 0,
|
|
||||||
id: 0,
|
|
||||||
kills: 0,
|
|
||||||
score: 0,
|
|
||||||
survived: 0,
|
|
||||||
wins: 0,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
id: leaderboardChunk[index].id,
|
|
||||||
score: leaderboardChunk[index].score,
|
|
||||||
battles: stat.statistics.rating.battles,
|
|
||||||
damage: stat.statistics.rating.damage_dealt,
|
|
||||||
kills: stat.statistics.rating.frags,
|
|
||||||
survived: stat.statistics.rating.survived_battles,
|
|
||||||
wins: stat.statistics.rating.wins,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
chunkIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
const info = await patientFetchJSON<RatingInfo & { detail: undefined }>(
|
|
||||||
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/season/`,
|
|
||||||
);
|
|
||||||
const normalizedServer = regionSubdomain === 'na' ? 'com' : regionSubdomain;
|
|
||||||
const infoContent = new CdonWriteStream().cdon(
|
|
||||||
info as unknown as CdonValue,
|
|
||||||
).uint8Array;
|
|
||||||
const leaderboardWriteStream = new BkrlWriteStream();
|
|
||||||
|
|
||||||
leaderboardWriteStream.bkrl({
|
|
||||||
format: BkrlFormat.Superset1,
|
|
||||||
entries: comprehensiveLeaderboard,
|
|
||||||
});
|
|
||||||
|
|
||||||
const changes: FileChange[] = [
|
|
||||||
{
|
|
||||||
path: `regions/${normalizedServer}/rating/${info.current_season}/info.cdon.lz4`,
|
|
||||||
content: superCompress(infoContent),
|
|
||||||
encoding: 'base64',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: `regions/${normalizedServer}/rating/${info.current_season}/${target}.bkrl`,
|
|
||||||
content: Buffer.from(leaderboardWriteStream.uint8Array).toString(
|
|
||||||
'base64',
|
|
||||||
),
|
|
||||||
encoding: 'base64',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
commitAssets(`rating leaderboard ${regionSubdomain} ${target}`, changes);
|
|
||||||
});
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
asset,
|
asset,
|
||||||
decodeProtobuf,
|
encodePB64,
|
||||||
encodeProtobufToBase64,
|
|
||||||
Reviews,
|
Reviews,
|
||||||
toUniqueId,
|
toUniqueId,
|
||||||
Video,
|
Video,
|
||||||
@@ -27,13 +26,7 @@ console.log('Finding reviews...');
|
|||||||
|
|
||||||
const currentReviews = await fetch(asset('definitions/reviews.pb'))
|
const currentReviews = await fetch(asset('definitions/reviews.pb'))
|
||||||
.then((response) => response.arrayBuffer())
|
.then((response) => response.arrayBuffer())
|
||||||
.then((buffer) =>
|
.then((buffer) => Reviews.decode(new Uint8Array(buffer)));
|
||||||
decodeProtobuf<Reviews>(
|
|
||||||
'reviews',
|
|
||||||
'blitzkit.Reviews',
|
|
||||||
new Uint8Array(buffer),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
const auth = await google.auth.getClient({
|
const auth = await google.auth.getClient({
|
||||||
scopes: ['https://www.googleapis.com/auth/youtube.readonly'],
|
scopes: ['https://www.googleapis.com/auth/youtube.readonly'],
|
||||||
});
|
});
|
||||||
@@ -131,12 +124,10 @@ for (const tank of tanksSanitized) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = await encodeProtobufToBase64(
|
|
||||||
'reviews',
|
|
||||||
'blitzkit.Reviews',
|
|
||||||
reviews,
|
|
||||||
);
|
|
||||||
|
|
||||||
await commitAssets('reviews', [
|
await commitAssets('reviews', [
|
||||||
{ content, encoding: 'base64', path: 'definitions/reviews.pb' },
|
{
|
||||||
|
content: encodePB64(Reviews, reviews),
|
||||||
|
encoding: 'base64',
|
||||||
|
path: 'definitions/reviews.pb',
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { idToRegion } from '@blitzkit/core';
|
||||||
|
import { Button, Flex, Heading, Link, Text } from '@radix-ui/themes';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import type { PatreonAuthResponse } from '../pages/auth/[provider]/_index';
|
||||||
|
import { App } from '../stores/app';
|
||||||
|
import { CURRENT_POLICIES_AGREEMENT_INDEX } from '../stores/app/constants';
|
||||||
|
|
||||||
|
type Extension =
|
||||||
|
| {
|
||||||
|
status: 'ok';
|
||||||
|
data: {
|
||||||
|
access_token: string;
|
||||||
|
account_id: number;
|
||||||
|
expires_at: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
status: 'error';
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Checks() {
|
||||||
|
return (
|
||||||
|
<App.Provider>
|
||||||
|
<Content />
|
||||||
|
</App.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wargaming: 14 (refresh: 7)
|
||||||
|
* Patreon: 31 (refresh: 15)
|
||||||
|
*/
|
||||||
|
function Content() {
|
||||||
|
const logins = App.use((state) => state.logins);
|
||||||
|
const mutateApp = App.useMutation();
|
||||||
|
const appStore = App.useStore();
|
||||||
|
const policiesAgreementIndex = App.use(
|
||||||
|
(state) => state.policiesAgreementIndex,
|
||||||
|
);
|
||||||
|
const isLegal = window.location.pathname.startsWith('/legal');
|
||||||
|
const [showPoliciesAgreement, setShowPoliciesAgreement] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function unsubscribe() {
|
||||||
|
window.removeEventListener('scroll', show);
|
||||||
|
window.removeEventListener('pointermove', show);
|
||||||
|
window.removeEventListener('pointerdown', show);
|
||||||
|
}
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
setShowPoliciesAgreement(true);
|
||||||
|
unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
policiesAgreementIndex !== CURRENT_POLICIES_AGREEMENT_INDEX &&
|
||||||
|
!isLegal
|
||||||
|
) {
|
||||||
|
window.addEventListener('scroll', show);
|
||||||
|
window.addEventListener('pointermove', show);
|
||||||
|
window.addEventListener('pointerdown', show);
|
||||||
|
}
|
||||||
|
|
||||||
|
return unsubscribe;
|
||||||
|
}, [policiesAgreementIndex, isLegal]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (logins.wargaming) {
|
||||||
|
const expiresIn = logins.wargaming.expires - Date.now();
|
||||||
|
const expiresInDays = expiresIn / 1000 / 60 / 60 / 24;
|
||||||
|
|
||||||
|
if (expiresInDays < 0) {
|
||||||
|
mutateApp((draft) => {
|
||||||
|
draft.logins.wargaming = undefined;
|
||||||
|
});
|
||||||
|
} else if (expiresInDays < 7) {
|
||||||
|
const { wargaming } = appStore.getState().logins;
|
||||||
|
|
||||||
|
if (!wargaming) return;
|
||||||
|
|
||||||
|
fetch(
|
||||||
|
`https://api.worldoftanks.${idToRegion(
|
||||||
|
wargaming.id,
|
||||||
|
)}/wot/auth/prolongate/?application_id=${
|
||||||
|
import.meta.env.PUBLIC_WARGAMING_APPLICATION_ID
|
||||||
|
}&access_token=${wargaming.token}`,
|
||||||
|
)
|
||||||
|
.then((response) => response.json() as Promise<Extension>)
|
||||||
|
.then((json) => {
|
||||||
|
mutateApp((draft) => {
|
||||||
|
if (json.status === 'error') {
|
||||||
|
draft.logins.wargaming = undefined;
|
||||||
|
} else {
|
||||||
|
draft.logins.wargaming = {
|
||||||
|
id: wargaming.id,
|
||||||
|
token: json.data.access_token,
|
||||||
|
expires: json.data.expires_at * 1000,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logins.patreon) {
|
||||||
|
const expiresIn = logins.patreon.expires - Date.now();
|
||||||
|
const expiresInDays = expiresIn / 1000 / 60 / 60 / 24;
|
||||||
|
|
||||||
|
if (expiresInDays < 0) {
|
||||||
|
mutateApp((draft) => {
|
||||||
|
draft.logins.patreon = undefined;
|
||||||
|
});
|
||||||
|
} else if (expiresInDays < 15) {
|
||||||
|
const { patreon } = appStore.getState().logins;
|
||||||
|
|
||||||
|
if (!patreon) return;
|
||||||
|
|
||||||
|
fetch(`/api/patreon/refresh/${patreon.refreshToken}`)
|
||||||
|
.then((response) => response.json() as Promise<PatreonAuthResponse>)
|
||||||
|
.then((data) => {
|
||||||
|
mutateApp((draft) => {
|
||||||
|
draft.logins.patreon = {
|
||||||
|
token: data.access_token,
|
||||||
|
refreshToken: data.refresh_token,
|
||||||
|
expires: Date.now() + data.expires_in * 1000,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!showPoliciesAgreement) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
align="end"
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
background: 'var(--color-overlay)',
|
||||||
|
zIndex: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
p="8"
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
background: 'var(--color-panel-solid)',
|
||||||
|
}}
|
||||||
|
justify="center"
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
align="start"
|
||||||
|
gap="4"
|
||||||
|
style={{
|
||||||
|
maxWidth: 640 * 2,
|
||||||
|
}}
|
||||||
|
direction="column"
|
||||||
|
>
|
||||||
|
<Flex direction="column" gap="2">
|
||||||
|
<Heading>
|
||||||
|
{policiesAgreementIndex === -1
|
||||||
|
? "BlitzKit's policies"
|
||||||
|
: "BlitzKit's policies have changed"}
|
||||||
|
</Heading>
|
||||||
|
|
||||||
|
<Text>
|
||||||
|
This website utilizes cookies to perform analytics and personalize
|
||||||
|
your experience. You can learn more through{' '}
|
||||||
|
<Link href="/docs/legal/privacy-policy">our privacy policy</Link>.
|
||||||
|
You may opt out of personalized advertizement by visiting{' '}
|
||||||
|
<Link href="https://optout.aboutads.info/">WebChoices</Link>. By
|
||||||
|
using BlitzKit, you also agree to our{' '}
|
||||||
|
<Link href="/docs/legal/terms-of-service">terms of service</Link>.
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
mt="2"
|
||||||
|
onClick={() => {
|
||||||
|
appStore.setState({
|
||||||
|
policiesAgreementIndex: CURRENT_POLICIES_AGREEMENT_INDEX,
|
||||||
|
});
|
||||||
|
setShowPoliciesAgreement(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
I agree
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { patreonLoginUrl } from '@blitzkit/core';
|
import { patreonLoginUrl } from '@blitzkit/core';
|
||||||
import { useStore } from '@nanostores/react';
|
|
||||||
import { PersonIcon } from '@radix-ui/react-icons';
|
import { PersonIcon } from '@radix-ui/react-icons';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -14,8 +13,6 @@ import {
|
|||||||
} from '@radix-ui/themes';
|
} from '@radix-ui/themes';
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
import { App } from '../../stores/app';
|
import { App } from '../../stores/app';
|
||||||
import { $patreonLogin } from '../../stores/patreonLogin';
|
|
||||||
import { $wargamingLogin } from '../../stores/wargamingLogin';
|
|
||||||
import { BlitzKitTheme } from '../BlitzKitTheme';
|
import { BlitzKitTheme } from '../BlitzKitTheme';
|
||||||
import { PatreonIcon } from '../PatreonIcon';
|
import { PatreonIcon } from '../PatreonIcon';
|
||||||
import { WargamingAccountName } from '../WargamingAccountName';
|
import { WargamingAccountName } from '../WargamingAccountName';
|
||||||
@@ -23,8 +20,16 @@ import { WargamingIcon } from '../WargamingIcon';
|
|||||||
import { WargamingLoginButton } from '../WargamingLoginButton';
|
import { WargamingLoginButton } from '../WargamingLoginButton';
|
||||||
|
|
||||||
export function NavbarAccount() {
|
export function NavbarAccount() {
|
||||||
const patreon = useStore($patreonLogin);
|
return (
|
||||||
const wargaming = useStore($wargamingLogin);
|
<App.Provider>
|
||||||
|
<Content />
|
||||||
|
</App.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Content() {
|
||||||
|
const logins = App.use((state) => state.logins);
|
||||||
|
const mutateApp = App.useMutation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<App.Provider>
|
<App.Provider>
|
||||||
@@ -41,13 +46,13 @@ export function NavbarAccount() {
|
|||||||
width="320px"
|
width="320px"
|
||||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||||
>
|
>
|
||||||
{(patreon.token || wargaming.token) && (
|
{(logins.patreon || logins.wargaming) && (
|
||||||
<Flex direction="column" gap="2">
|
<Flex direction="column" gap="2">
|
||||||
<Text align="center" color="gray">
|
<Text align="center" color="gray">
|
||||||
Logged in accounts
|
Logged in accounts
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{patreon.token && (
|
{logins.patreon && (
|
||||||
<Flex align="center" gap="2" justify="center">
|
<Flex align="center" gap="2" justify="center">
|
||||||
<PatreonIcon width={15} height={15} />
|
<PatreonIcon width={15} height={15} />
|
||||||
<Text>Patreon</Text>
|
<Text>Patreon</Text>
|
||||||
@@ -55,7 +60,9 @@ export function NavbarAccount() {
|
|||||||
color="red"
|
color="red"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
$patreonLogin.setKey('token', undefined);
|
mutateApp((draft) => {
|
||||||
|
draft.logins.patreon = undefined;
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Logout
|
Logout
|
||||||
@@ -63,7 +70,7 @@ export function NavbarAccount() {
|
|||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{wargaming.token && (
|
{logins.wargaming && (
|
||||||
<Flex align="center" gap="2" justify="center">
|
<Flex align="center" gap="2" justify="center">
|
||||||
<WargamingIcon width={15} height={15} />
|
<WargamingIcon width={15} height={15} />
|
||||||
<Text>
|
<Text>
|
||||||
@@ -77,7 +84,9 @@ export function NavbarAccount() {
|
|||||||
color="red"
|
color="red"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
$wargamingLogin.setKey('token', undefined);
|
mutateApp((draft) => {
|
||||||
|
draft.logins.wargaming = undefined;
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Logout
|
Logout
|
||||||
@@ -87,19 +96,19 @@ export function NavbarAccount() {
|
|||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!patreon.token !== !wargaming.token && (
|
{!logins.patreon !== !logins.wargaming && (
|
||||||
<Inset side="x">
|
<Inset side="x">
|
||||||
<Separator my="4" size="4" />
|
<Separator my="4" size="4" />
|
||||||
</Inset>
|
</Inset>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(!patreon.token || !wargaming.token) && (
|
{(!logins.patreon || !logins.wargaming) && (
|
||||||
<Flex direction="column" gap="2">
|
<Flex direction="column" gap="2">
|
||||||
<Text align="center" color="gray">
|
<Text align="center" color="gray">
|
||||||
Log in with...
|
Log in with...
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{!patreon.token && (
|
{!logins.patreon && (
|
||||||
<Link style={{ width: '100%' }} href={patreonLoginUrl()}>
|
<Link style={{ width: '100%' }} href={patreonLoginUrl()}>
|
||||||
<Button style={{ width: '100%' }} color="tomato">
|
<Button style={{ width: '100%' }} color="tomato">
|
||||||
<PatreonIcon width={15} height={15} /> Patreon
|
<PatreonIcon width={15} height={15} /> Patreon
|
||||||
@@ -107,7 +116,7 @@ export function NavbarAccount() {
|
|||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!wargaming.token && <WargamingLoginButton />}
|
{!logins.wargaming && <WargamingLoginButton />}
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
</Popover.Content>
|
</Popover.Content>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { assertSecret, patreonLoginUrl } from '@blitzkit/core';
|
import { assertSecret, patreonLoginUrl } from '@blitzkit/core';
|
||||||
import { useStore } from '@nanostores/react';
|
|
||||||
import { ArrowRightIcon } from '@radix-ui/react-icons';
|
import { ArrowRightIcon } from '@radix-ui/react-icons';
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
@@ -11,12 +10,20 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
} from '@radix-ui/themes';
|
} from '@radix-ui/themes';
|
||||||
import { useAdExempt } from '../hooks/useAdExempt';
|
import { useAdExempt } from '../hooks/useAdExempt';
|
||||||
import { $patreonLogin } from '../stores/patreonLogin';
|
import { App } from '../stores/app';
|
||||||
import { PatreonIcon } from './PatreonIcon';
|
import { PatreonIcon } from './PatreonIcon';
|
||||||
|
|
||||||
export function Plugs() {
|
export function Plugs() {
|
||||||
|
return (
|
||||||
|
<App.Provider>
|
||||||
|
<Content />
|
||||||
|
</App.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Content() {
|
||||||
const exempt = useAdExempt();
|
const exempt = useAdExempt();
|
||||||
const hasPatreon = useStore($patreonLogin).token !== undefined;
|
const hasPatreon = App.use((state) => state.logins.patreon !== undefined);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
|
|||||||
+4
-3
@@ -9,11 +9,12 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
} from '@radix-ui/themes';
|
} from '@radix-ui/themes';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { App } from '../../../../../../stores/app';
|
||||||
import { Duel } from '../../../../../../stores/duel';
|
import { Duel } from '../../../../../../stores/duel';
|
||||||
import { $wargamingLogin } from '../../../../../../stores/wargamingLogin';
|
|
||||||
import { Stars } from '../../Stars';
|
import { Stars } from '../../Stars';
|
||||||
|
|
||||||
export function VoteCaster() {
|
export function VoteCaster() {
|
||||||
|
const appStore = App.useStore();
|
||||||
const tank = Duel.use((state) => state.protagonist.tank);
|
const tank = Duel.use((state) => state.protagonist.tank);
|
||||||
const [easiness, setEasiness] = useState<StarsInt | null>(null);
|
const [easiness, setEasiness] = useState<StarsInt | null>(null);
|
||||||
const [firepower, setFirepower] = useState<StarsInt | null>(null);
|
const [firepower, setFirepower] = useState<StarsInt | null>(null);
|
||||||
@@ -97,9 +98,9 @@ export function VoteCaster() {
|
|||||||
survivability === null)
|
survivability === null)
|
||||||
}
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const wargaming = $wargamingLogin.get();
|
const { wargaming } = appStore.getState().logins;
|
||||||
|
|
||||||
if (!wargaming.token) {
|
if (!wargaming) {
|
||||||
return setShowDialogue(false);
|
return setShowDialogue(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { formatCompact } from '@blitzkit/core';
|
import { formatCompact } from '@blitzkit/core';
|
||||||
import { useStore } from '@nanostores/react';
|
|
||||||
import { Flex, Heading, Skeleton, Text } from '@radix-ui/themes';
|
import { Flex, Heading, Skeleton, Text } from '@radix-ui/themes';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useTankVotes } from '../../../../../hooks/useTankVotes';
|
import { useTankVotes } from '../../../../../hooks/useTankVotes';
|
||||||
|
import { App } from '../../../../../stores/app';
|
||||||
import { Duel } from '../../../../../stores/duel';
|
import { Duel } from '../../../../../stores/duel';
|
||||||
import { $wargamingLogin } from '../../../../../stores/wargamingLogin';
|
|
||||||
import { WargamingLoginButton } from '../../../../WargamingLoginButton';
|
import { WargamingLoginButton } from '../../../../WargamingLoginButton';
|
||||||
import { StarRow } from './components/StarRow';
|
import { StarRow } from './components/StarRow';
|
||||||
import { VoteCaster } from './components/VoteCaster';
|
import { VoteCaster } from './components/VoteCaster';
|
||||||
@@ -13,7 +12,7 @@ export const Votes = memo(
|
|||||||
() => {
|
() => {
|
||||||
const id = Duel.use((state) => state.protagonist.tank.id);
|
const id = Duel.use((state) => state.protagonist.tank.id);
|
||||||
const votes = useTankVotes(id);
|
const votes = useTankVotes(id);
|
||||||
const wargaming = useStore($wargamingLogin);
|
const wargaming = App.use((state) => state.logins.wargaming);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" gap="3">
|
<Flex direction="column" gap="3">
|
||||||
|
|||||||
@@ -1,22 +1,19 @@
|
|||||||
import { getAccountInfo, idToRegion } from '@blitzkit/core';
|
import { getAccountInfo, idToRegion } from '@blitzkit/core';
|
||||||
import { useStore } from '@nanostores/react';
|
|
||||||
import { Text } from '@radix-ui/themes';
|
import { Text } from '@radix-ui/themes';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import usePromise from 'react-promise-suspense';
|
import usePromise from 'react-promise-suspense';
|
||||||
import { $wargamingLogin } from '../stores/wargamingLogin';
|
import { App } from '../stores/app';
|
||||||
|
|
||||||
export function WargamingAccountName() {
|
export function WargamingAccountName() {
|
||||||
const wargaming = useStore($wargamingLogin);
|
const wargaming = App.use((state) => state.logins.wargaming);
|
||||||
const promise = useMemo(async () => {
|
const promise = useMemo(async () => {
|
||||||
if (!wargaming.token) return null;
|
if (!wargaming) return null;
|
||||||
|
|
||||||
const data = await getAccountInfo(
|
const data = await getAccountInfo(
|
||||||
idToRegion(Number(wargaming.id)),
|
idToRegion(wargaming.id),
|
||||||
Number(wargaming.id),
|
wargaming.id,
|
||||||
[],
|
[],
|
||||||
{
|
{ access_token: wargaming.token },
|
||||||
access_token: wargaming.token,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return data?.nickname as string | undefined;
|
return data?.nickname as string | undefined;
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import { useStore } from '@nanostores/react';
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { $patreonLogin } from '../stores/patreonLogin';
|
import { App } from '../stores/app';
|
||||||
|
|
||||||
let cache: Record<string, boolean | Promise<boolean>> = {};
|
let cache: Record<string, boolean | Promise<boolean>> = {};
|
||||||
|
|
||||||
export function useAdExempt() {
|
export function useAdExempt() {
|
||||||
const patreon = useStore($patreonLogin);
|
const patreon = App.use((state) => state.logins.patreon);
|
||||||
const [exempt, setExempt] = useState(false);
|
const [exempt, setExempt] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
if (!patreon.token) return setExempt(false);
|
if (!patreon) return setExempt(false);
|
||||||
|
|
||||||
if (!(patreon.token in cache)) {
|
if (!(patreon.token in cache)) {
|
||||||
cache[patreon.token] = new Promise<boolean>(async (resolve) => {
|
cache[patreon.token] = new Promise<boolean>(async (resolve) => {
|
||||||
|
|||||||
@@ -3,18 +3,17 @@ import {
|
|||||||
type BlitzkitResponse,
|
type BlitzkitResponse,
|
||||||
type TankVotes,
|
type TankVotes,
|
||||||
} from '@blitzkit/core';
|
} from '@blitzkit/core';
|
||||||
import { useStore } from '@nanostores/react';
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { $wargamingLogin } from '../stores/wargamingLogin';
|
import { App } from '../stores/app';
|
||||||
|
|
||||||
const cache: Record<string, Promise<TankVotes> | TankVotes> = {};
|
const cache: Record<string, Promise<TankVotes> | TankVotes> = {};
|
||||||
|
|
||||||
export function useTankVotes(id: number) {
|
export function useTankVotes(id: number) {
|
||||||
const [votes, setVotes] = useState<TankVotes | null>(null);
|
const [votes, setVotes] = useState<TankVotes | null>(null);
|
||||||
const wargaming = useStore($wargamingLogin);
|
const wargaming = App.use((state) => state.logins.wargaming);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!wargaming.token) return;
|
if (!wargaming) return;
|
||||||
|
|
||||||
if (id in cache) {
|
if (id in cache) {
|
||||||
Promise.all([cache[id]]).then(([votes]) => setVotes(votes));
|
Promise.all([cache[id]]).then(([votes]) => setVotes(votes));
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
import { Flex } from '@radix-ui/themes';
|
import { Flex } from '@radix-ui/themes';
|
||||||
import { BlitzKitTheme } from '../components/BlitzKitTheme';
|
import { BlitzKitTheme } from '../components/BlitzKitTheme';
|
||||||
|
import { Checks } from '../components/Checks';
|
||||||
import Footer from '../components/Footer.astro';
|
import Footer from '../components/Footer.astro';
|
||||||
import Navbar from '../components/Navbar.astro';
|
import Navbar from '../components/Navbar.astro';
|
||||||
import Party3 from '../components/Party3.astro';
|
import Party3 from '../components/Party3.astro';
|
||||||
@@ -43,11 +44,9 @@ const isRoot = pathname === '/';
|
|||||||
<Flex direction="column" minHeight="100vh">
|
<Flex direction="column" minHeight="100vh">
|
||||||
<Navbar />
|
<Navbar />
|
||||||
|
|
||||||
<!-- <Checks /> -->
|
<Checks client:only />
|
||||||
|
|
||||||
<!-- <Suspense fallback={(<PageLoader />)}> -->
|
|
||||||
<slot />
|
<slot />
|
||||||
<!-- </Suspense> -->
|
|
||||||
|
|
||||||
<Footer />
|
<Footer />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { $patreonLogin } from '../../../stores/patreonLogin';
|
import { App } from '../../../stores/app';
|
||||||
import { $wargamingLogin } from '../../../stores/wargamingLogin';
|
import { $wargamingLogin } from '../../../stores/wargamingLogin';
|
||||||
|
|
||||||
export const AUTH_PROVIDERS = ['wargaming', 'patreon'] as const;
|
export const AUTH_PROVIDERS = ['wargaming', 'patreon'] as const;
|
||||||
@@ -25,6 +25,8 @@ interface AuthorizeProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Authorize({ provider }: AuthorizeProps) {
|
export function Authorize({ provider }: AuthorizeProps) {
|
||||||
|
const mutateApp = App.useMutation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const searchParams = new URLSearchParams(window.location.search);
|
const searchParams = new URLSearchParams(window.location.search);
|
||||||
@@ -56,10 +58,12 @@ export function Authorize({ provider }: AuthorizeProps) {
|
|||||||
);
|
);
|
||||||
const data = (await response.json()) as PatreonAuthResponse;
|
const data = (await response.json()) as PatreonAuthResponse;
|
||||||
|
|
||||||
$patreonLogin.set({
|
mutateApp((draft) => {
|
||||||
token: data.access_token,
|
draft.logins.patreon = {
|
||||||
refreshToken: data.refresh_token,
|
token: data.access_token,
|
||||||
expires: `${Date.now() + data.expires_in * 1000}`,
|
refreshToken: data.refresh_token,
|
||||||
|
expires: Date.now() + data.expires_in * 1000,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { PageWrapper } from '../../../components/PageWrapper';
|
import { PageWrapper } from '../../../components/PageWrapper';
|
||||||
import { TankSearch } from '../../../components/TankSearch';
|
import { TankSearch } from '../../../components/TankSearch';
|
||||||
|
import { App } from '../../../stores/app';
|
||||||
|
|
||||||
export function Page() {
|
export function Page() {
|
||||||
return (
|
return (
|
||||||
<PageWrapper color="purple" size={1028}>
|
<PageWrapper color="purple" size={1028}>
|
||||||
<TankSearch />
|
<App.Provider>
|
||||||
|
<TankSearch />
|
||||||
|
</App.Provider>
|
||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,26 @@
|
|||||||
|
import { merge } from 'lodash-es';
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
import { persist, subscribeWithSelector } from 'zustand/middleware';
|
||||||
import { createContextualStore } from '../../core/zustand/createContextualStore';
|
import { createContextualStore } from '../../core/zustand/createContextualStore';
|
||||||
|
|
||||||
interface AppStore {
|
interface AppStore {
|
||||||
developerMode: boolean;
|
developerMode: boolean;
|
||||||
policiesAgreementIndex: number;
|
policiesAgreementIndex: number;
|
||||||
|
logins: {
|
||||||
|
wargaming?: { token: string; id: number; expires: number };
|
||||||
|
patreon?: { token: string; refreshToken: string; expires: number };
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const App = createContextualStore(() =>
|
export const App = createContextualStore(() =>
|
||||||
create<AppStore>()(() => ({
|
create<AppStore>()(
|
||||||
developerMode: false,
|
persist(
|
||||||
policiesAgreementIndex: 0,
|
subscribeWithSelector<AppStore>(() => ({
|
||||||
})),
|
developerMode: false,
|
||||||
|
policiesAgreementIndex: -1,
|
||||||
|
logins: {},
|
||||||
|
})),
|
||||||
|
{ name: 'app', merge: (a, b) => merge(b, a) },
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { persistentMap } from '@nanostores/persistent';
|
|
||||||
|
|
||||||
type PatreonLogin =
|
|
||||||
| {
|
|
||||||
token: undefined;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
token: string;
|
|
||||||
refreshToken: string;
|
|
||||||
expires: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const $patreonLogin = persistentMap<PatreonLogin>('patreon:', {
|
|
||||||
token: undefined,
|
|
||||||
});
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { persistentMap } from '@nanostores/persistent';
|
|
||||||
|
|
||||||
type WargamingLogin =
|
|
||||||
| {
|
|
||||||
token: undefined;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
token: string;
|
|
||||||
id: string;
|
|
||||||
expires: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const $wargamingLogin = persistentMap<WargamingLogin>('wargaming:', {
|
|
||||||
token: undefined,
|
|
||||||
});
|
|
||||||
Reference in new issue
Block a user