mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 02:37:05 +02:00
feat: cc account tracking
This commit is contained in:
1 parent
6ae86a7a70
commit
d2b18360ed
5 files changed
+260
-282
No files matched your search
@@ -0,0 +1,62 @@
|
||||
'use client';
|
||||
|
||||
import { Flex, Heading, Text } from '@radix-ui/themes';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
import PageWrapper from '../../../components/PageWrapper';
|
||||
import { mutateApp } from '../../../stores/app';
|
||||
|
||||
export default function Page({
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
params: { provider: string };
|
||||
searchParams: { [key: string]: string };
|
||||
}) {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
switch (params.provider) {
|
||||
case 'wargaming': {
|
||||
if (
|
||||
!searchParams.expires_at ||
|
||||
!searchParams.access_token ||
|
||||
!searchParams.account_id
|
||||
) {
|
||||
break;
|
||||
}
|
||||
|
||||
mutateApp((draft) => {
|
||||
draft.login = {
|
||||
expiresAt: Number(searchParams.expires_at),
|
||||
id: Number(searchParams.account_id),
|
||||
token: searchParams.access_token,
|
||||
};
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
console.error(`Unknown provider: ${params.provider}`);
|
||||
}
|
||||
}
|
||||
|
||||
router.push(searchParams.return ? searchParams.return : '/');
|
||||
});
|
||||
|
||||
return (
|
||||
<PageWrapper>
|
||||
<Flex
|
||||
style={{ flex: 1 }}
|
||||
align="center"
|
||||
justify="center"
|
||||
direction="column"
|
||||
gap="2"
|
||||
>
|
||||
<Heading>Verifying you...</Heading>
|
||||
<Text color="gray">You'll be on your way shortly.</Text>
|
||||
</Flex>
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
+161
-240
@@ -1,269 +1,190 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
CaretDownIcon,
|
||||
CopyIcon,
|
||||
PersonIcon,
|
||||
ReloadIcon,
|
||||
} from '@radix-ui/react-icons';
|
||||
import { MagnifyingGlassIcon } from '@radix-ui/react-icons';
|
||||
import {
|
||||
AlertDialog,
|
||||
Button,
|
||||
Checkbox,
|
||||
DropdownMenu,
|
||||
Card,
|
||||
Flex,
|
||||
Heading,
|
||||
Link,
|
||||
Text,
|
||||
TextField,
|
||||
} from '@radix-ui/themes';
|
||||
import { debounce } from 'lodash';
|
||||
import { ChangeEvent, useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import PageWrapper from '../../../components/PageWrapper';
|
||||
import { UNLOCALIZED_REGION_NAMES } from '../../../constants/regions';
|
||||
import { UNLOCALIZED_REGION_NAMES_SHORT } from '../../../constants/regions';
|
||||
import { WARGAMING_APPLICATION_ID } from '../../../constants/wargamingApplicationID';
|
||||
import { getAccountInfo } from '../../../core/blitz/getAccountInfo';
|
||||
import { idToRegion } from '../../../core/blitz/idToRegion';
|
||||
import searchPlayersAcrossRegions, {
|
||||
AccountListWithServer,
|
||||
} from '../../../core/blitz/searchPlayersAcrossRegions';
|
||||
import { resetSession } from '../../../core/blitzkit/resetSession';
|
||||
import { setSession } from '../../../core/blitzkit/setSession';
|
||||
import { useSession } from '../../../stores/session';
|
||||
import SessionPage from '../../embeds/session/page';
|
||||
import { Menu } from './components/Menu';
|
||||
import * as styles from './page.css';
|
||||
import { useApp } from '../../../stores/app';
|
||||
import mutateSession, {
|
||||
SessionTracking,
|
||||
useSession,
|
||||
} from '../../../stores/session';
|
||||
|
||||
export default function Page() {
|
||||
const [showSearch, setShowSearch] = useState(false);
|
||||
const [searching, setSearching] = useState(false);
|
||||
const [searchResults, setSearchResults] = useState<AccountListWithServer>([]);
|
||||
const [showFurtherVerification, setShowFurtherVerification] = useState(false);
|
||||
const input = useRef<HTMLInputElement>(null);
|
||||
const [searchResults, setSearchResults] = useState<
|
||||
AccountListWithServer | undefined
|
||||
>(undefined);
|
||||
const [showSearchResults, setShowSearchResults] = useState(false);
|
||||
const handleChange = debounce(
|
||||
async (event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (event.target.value) {
|
||||
setSearchResults(await searchPlayersAcrossRegions(event.target.value));
|
||||
} else {
|
||||
setSearchResults(undefined);
|
||||
setShowSearchResults(false);
|
||||
}
|
||||
},
|
||||
500,
|
||||
);
|
||||
const session = useSession();
|
||||
const login = useApp((state) => state.login);
|
||||
|
||||
const search = debounce(async () => {
|
||||
if (!input.current) return;
|
||||
|
||||
setSearchResults(await searchPlayersAcrossRegions(input.current.value, 9));
|
||||
setSearching(false);
|
||||
}, 500);
|
||||
|
||||
useEffect(() => {
|
||||
if (session.isTracking) input.current!.value = session.nickname;
|
||||
}, [session]);
|
||||
(async () => {
|
||||
if (!session.tracking) return;
|
||||
|
||||
const accountInfo = await getAccountInfo(
|
||||
idToRegion(session.player.id),
|
||||
session.player.id,
|
||||
);
|
||||
|
||||
if (accountInfo === null) {
|
||||
if (login?.id === session.player.id) {
|
||||
} else {
|
||||
setShowFurtherVerification(true);
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, [session.tracking && session.player.id]);
|
||||
|
||||
return (
|
||||
<PageWrapper color="blue">
|
||||
<div className={styles.toolBar}>
|
||||
<div style={{ flex: 1, boxSizing: 'border-box', position: 'relative' }}>
|
||||
<TextField.Root
|
||||
defaultValue={session.isTracking ? session.nickname : undefined}
|
||||
ref={input}
|
||||
onChange={(event) => {
|
||||
event.stopPropagation();
|
||||
<PageWrapper>
|
||||
{/* omit totally to avoid accessing undefined player */}
|
||||
{showFurtherVerification && (
|
||||
<AlertDialog.Root open>
|
||||
<AlertDialog.Content>
|
||||
<AlertDialog.Title>We need further verification</AlertDialog.Title>
|
||||
<AlertDialog.Description>
|
||||
You're trying to track a CC account with private statistics.
|
||||
Please sign in to verify the ownership of this account.
|
||||
</AlertDialog.Description>
|
||||
|
||||
if (event.target.value) {
|
||||
setShowSearchResults(true);
|
||||
setSearchResults(undefined);
|
||||
} else {
|
||||
setShowSearchResults(false);
|
||||
}
|
||||
|
||||
handleChange(event);
|
||||
}}
|
||||
onBlur={(event) => {
|
||||
// TODO: remove this hack when https://github.com/radix-ui/primitives/issues/2193 (https://github.com/radix-ui/primitives/issues/1969) is fixed
|
||||
if (showSearchResults) event.target.focus();
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
setShowSearchResults(false);
|
||||
input.current?.blur();
|
||||
}
|
||||
}}
|
||||
placeholder="Search for a player..."
|
||||
>
|
||||
<TextField.Slot>
|
||||
<PersonIcon height="16" width="16" />
|
||||
</TextField.Slot>
|
||||
</TextField.Root>
|
||||
|
||||
<DropdownMenu.Root open={showSearchResults} modal={false}>
|
||||
<DropdownMenu.Trigger>
|
||||
<div />
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Content>
|
||||
{searchResults === undefined ? (
|
||||
<DropdownMenu.Item disabled>Searching...</DropdownMenu.Item>
|
||||
) : searchResults.length === 0 ? (
|
||||
<DropdownMenu.Item disabled>No results</DropdownMenu.Item>
|
||||
) : (
|
||||
searchResults?.map(
|
||||
({ account_id: id, nickname, region }, index) => (
|
||||
<DropdownMenu.Item
|
||||
key={id}
|
||||
onClick={() => {
|
||||
setShowSearchResults(false);
|
||||
setSession(region, id, nickname);
|
||||
}}
|
||||
shortcut={UNLOCALIZED_REGION_NAMES[region]}
|
||||
>
|
||||
{nickname}
|
||||
</DropdownMenu.Item>
|
||||
),
|
||||
)
|
||||
)}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<AlertDialog.Root>
|
||||
<AlertDialog.Trigger>
|
||||
<Button
|
||||
variant="soft"
|
||||
onClick={(event) => {
|
||||
if (!session.resetPrompt) {
|
||||
event.preventDefault();
|
||||
resetSession();
|
||||
}
|
||||
}}
|
||||
className={styles.toolbarButton}
|
||||
color="red"
|
||||
>
|
||||
<ReloadIcon width="16" height="16" /> Reset
|
||||
</Button>
|
||||
</AlertDialog.Trigger>
|
||||
|
||||
<AlertDialog.Content>
|
||||
<Flex direction="column" gap="4">
|
||||
<Flex direction="column" gap="0">
|
||||
<AlertDialog.Title>
|
||||
Are you sure you want to reset?
|
||||
</AlertDialog.Title>
|
||||
<AlertDialog.Description>
|
||||
This'll discard your current session and start tracking from
|
||||
scratch again.
|
||||
</AlertDialog.Description>
|
||||
</Flex>
|
||||
|
||||
<Text size="2">
|
||||
<Checkbox
|
||||
mr="1"
|
||||
onCheckedChange={(checked) =>
|
||||
useSession.setState({
|
||||
resetPrompt: !checked,
|
||||
})
|
||||
}
|
||||
/>
|
||||
Don't ask me again
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Flex gap="3" mt="4">
|
||||
<AlertDialog.Cancel>
|
||||
<Button variant="soft" color="gray">
|
||||
Cancel
|
||||
</Button>
|
||||
</AlertDialog.Cancel>
|
||||
|
||||
<AlertDialog.Action>
|
||||
<Button variant="solid" color="red" onClick={resetSession}>
|
||||
Reset
|
||||
</Button>
|
||||
</AlertDialog.Action>
|
||||
</Flex>
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Root>
|
||||
|
||||
<AlertDialog.Root>
|
||||
<AlertDialog.Trigger>
|
||||
<Button
|
||||
variant="soft"
|
||||
className={styles.toolbarButton}
|
||||
onClick={(event) => {
|
||||
if (!session.embedPrompt) {
|
||||
event.preventDefault();
|
||||
resetSession();
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(
|
||||
`${location.origin}/embeds/session`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<CopyIcon width="16" height="16" /> Embed
|
||||
</Button>
|
||||
</AlertDialog.Trigger>
|
||||
|
||||
<AlertDialog.Content>
|
||||
<AlertDialog.Title>
|
||||
Copied link to your clipboard
|
||||
</AlertDialog.Title>
|
||||
<AlertDialog.Description>
|
||||
Instructions for streamers:
|
||||
<iframe
|
||||
style={{
|
||||
width: '100%',
|
||||
aspectRatio: '16 / 9',
|
||||
border: 'none',
|
||||
}}
|
||||
src="https://www.youtube-nocookie.com/embed/e--GwjyrJAE?si=ftEiCmpbMI2FUY5N"
|
||||
title="YouTube video player"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
></iframe>
|
||||
{login && (
|
||||
<AlertDialog.Description mt="2" color="red">
|
||||
You are currently signed with a different account.
|
||||
</AlertDialog.Description>
|
||||
)}
|
||||
|
||||
<Text size="2">
|
||||
<Checkbox
|
||||
mr="1"
|
||||
onCheckedChange={(checked) =>
|
||||
useSession.setState({
|
||||
embedPrompt: !checked,
|
||||
})
|
||||
}
|
||||
/>
|
||||
Don't show this again
|
||||
</Text>
|
||||
|
||||
<Flex gap="3" mt="4">
|
||||
<AlertDialog.Cancel>
|
||||
<Button variant="soft">Got it!</Button>
|
||||
</AlertDialog.Cancel>
|
||||
</Flex>
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Root>
|
||||
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<Button variant="soft" className={styles.toolbarButton}>
|
||||
Options
|
||||
<CaretDownIcon />
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<Menu Builder={DropdownMenu} reset={resetSession} />
|
||||
</DropdownMenu.Root>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SessionPage naked={false} />
|
||||
|
||||
{!session.isTracking && (
|
||||
<Flex
|
||||
align="center"
|
||||
justify="center"
|
||||
direction="column"
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<Heading color="gray">No player selected</Heading>
|
||||
<Text color="gray">
|
||||
Search for a <PersonIcon /> Player to get started
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex gap="3" mt="4" justify="end">
|
||||
<AlertDialog.Cancel>
|
||||
<Button
|
||||
variant="soft"
|
||||
color="gray"
|
||||
onClick={() => {
|
||||
mutateSession((draft) => {
|
||||
draft.tracking = false;
|
||||
});
|
||||
setShowFurtherVerification(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</AlertDialog.Cancel>
|
||||
<AlertDialog.Action>
|
||||
<Link
|
||||
href={`https://api.worldoftanks.${
|
||||
(session as SessionTracking).player.region
|
||||
}/wot/auth/login/?application_id=${WARGAMING_APPLICATION_ID}&redirect_uri=${encodeURIComponent(
|
||||
`${location.origin}/callback/wargaming/?return=${location.href}`,
|
||||
)}`}
|
||||
>
|
||||
<Button variant="solid">Sign in</Button>
|
||||
</Link>
|
||||
</AlertDialog.Action>
|
||||
</Flex>
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Root>
|
||||
)}
|
||||
|
||||
<TextField.Root
|
||||
style={{
|
||||
position: 'relative',
|
||||
}}
|
||||
ref={input}
|
||||
placeholder="Search players..."
|
||||
onChange={() => {
|
||||
if (!input.current) return;
|
||||
|
||||
const sanitized = input.current.value.trim();
|
||||
|
||||
if (sanitized.length > 0) {
|
||||
setShowSearch(true);
|
||||
setSearching(true);
|
||||
search();
|
||||
} else {
|
||||
setShowSearch(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TextField.Slot>
|
||||
<MagnifyingGlassIcon />
|
||||
</TextField.Slot>
|
||||
|
||||
{showSearch && (
|
||||
<Card
|
||||
mt="2"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '100%',
|
||||
left: 0,
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{searching && (
|
||||
<Flex justify="center">
|
||||
<Text color="gray">Searching...</Text>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{!searching && searchResults.length === 0 && (
|
||||
<Flex justify="center">
|
||||
<Text color="gray">No players found</Text>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{!searching && searchResults.length > 0 && (
|
||||
<Flex direction="column" gap="2">
|
||||
{searchResults.map((player) => (
|
||||
<Button
|
||||
key={player.account_id}
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setShowSearch(false);
|
||||
mutateSession((draft) => {
|
||||
if (!input.current) return;
|
||||
|
||||
draft.tracking = true;
|
||||
(draft as SessionTracking).player = {
|
||||
id: player.account_id,
|
||||
region: player.region,
|
||||
};
|
||||
|
||||
input.current.value = player.nickname;
|
||||
});
|
||||
}}
|
||||
>
|
||||
{player.nickname} (
|
||||
{UNLOCALIZED_REGION_NAMES_SHORT[player.region]})
|
||||
</Button>
|
||||
))}
|
||||
</Flex>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
</TextField.Root>
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
@@ -11,3 +11,12 @@ export const UNLOCALIZED_REGION_NAMES: Record<Region, string> = {
|
||||
asia: 'Asia',
|
||||
eu: 'Europe',
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export const UNLOCALIZED_REGION_NAMES_SHORT: Record<Region, string> = {
|
||||
com: 'NA',
|
||||
asia: 'APAC',
|
||||
eu: 'EU',
|
||||
};
|
||||
@@ -1,11 +1,19 @@
|
||||
import { produce } from 'immer';
|
||||
import { merge } from 'lodash';
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
interface Login {
|
||||
id: number;
|
||||
token: string;
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
export interface App {
|
||||
devBuildAgreementTime: number;
|
||||
developerMode: boolean;
|
||||
darkMode: boolean;
|
||||
login?: Login;
|
||||
}
|
||||
|
||||
export const useApp = create<App>()(
|
||||
@@ -18,3 +26,7 @@ export const useApp = create<App>()(
|
||||
{ name: 'app', merge: (a, b) => merge(b, a) },
|
||||
),
|
||||
);
|
||||
|
||||
export function mutateApp(recipe: (draft: App) => void) {
|
||||
useApp.setState(produce(recipe));
|
||||
}
|
||||
+16
-42
@@ -2,56 +2,30 @@ import { produce } from 'immer';
|
||||
import { merge } from 'lodash';
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { CustomColumnDisplay } from '../app/tools/session/components/CustomColumn';
|
||||
import { AccentColor, GrayColor } from '../constants/radixColors';
|
||||
import { Region } from '../constants/regions';
|
||||
import { NormalizedTankStats } from '../types/tanksStats';
|
||||
|
||||
type Session = (
|
||||
| {
|
||||
isTracking: false;
|
||||
}
|
||||
| {
|
||||
isTracking: true;
|
||||
interface SessionBase {}
|
||||
|
||||
region: Region;
|
||||
id: number;
|
||||
nickname: string;
|
||||
tankStats: NormalizedTankStats;
|
||||
time: number;
|
||||
}
|
||||
) & {
|
||||
title: string;
|
||||
resetPrompt: boolean;
|
||||
embedPrompt: boolean;
|
||||
customColumns: {
|
||||
display: CustomColumnDisplay;
|
||||
showDelta: boolean;
|
||||
}[] & { length: 4 };
|
||||
showTotal: boolean;
|
||||
showCareer: boolean;
|
||||
color: AccentColor | GrayColor;
|
||||
};
|
||||
export interface SessionTracking extends SessionBase {
|
||||
tracking: true;
|
||||
player: {
|
||||
id: number;
|
||||
region: Region;
|
||||
};
|
||||
}
|
||||
|
||||
interface SessionNotTracking extends SessionBase {
|
||||
tracking: false;
|
||||
}
|
||||
|
||||
type Session = SessionTracking | SessionNotTracking;
|
||||
|
||||
export const useSession = create<Session>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
title: 'Untitled session',
|
||||
isTracking: false,
|
||||
resetPrompt: true,
|
||||
showTotal: true,
|
||||
showCareer: true,
|
||||
embedPrompt: true,
|
||||
|
||||
customColumns: [
|
||||
{ display: 'battles', showDelta: false },
|
||||
{ display: 'winrate', showDelta: true },
|
||||
{ display: 'wn8', showDelta: false },
|
||||
{ display: 'damage', showDelta: true },
|
||||
],
|
||||
color: 'slate',
|
||||
tracking: false,
|
||||
}),
|
||||
{ name: 'session', merge: (a, b) => merge(b, a) },
|
||||
{ name: 'session-2', merge: (a, b) => merge(b, a) },
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
Reference in new issue
Block a user