feat: cc account tracking

This commit is contained in:
TrèsAbhi committed 2024-04-28 18:43:12 -05:00
1 parent 6ae86a7a70
commit d2b18360ed
5 files changed
+260 -282

No files matched your search

+62
View File
@@ -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
View File
@@ -1,269 +1,190 @@
'use client'; 'use client';
import { import { MagnifyingGlassIcon } from '@radix-ui/react-icons';
CaretDownIcon,
CopyIcon,
PersonIcon,
ReloadIcon,
} from '@radix-ui/react-icons';
import { import {
AlertDialog, AlertDialog,
Button, Button,
Checkbox, Card,
DropdownMenu,
Flex, Flex,
Heading, Link,
Text, Text,
TextField, TextField,
} from '@radix-ui/themes'; } from '@radix-ui/themes';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { ChangeEvent, useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import PageWrapper from '../../../components/PageWrapper'; 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, { import searchPlayersAcrossRegions, {
AccountListWithServer, AccountListWithServer,
} from '../../../core/blitz/searchPlayersAcrossRegions'; } from '../../../core/blitz/searchPlayersAcrossRegions';
import { resetSession } from '../../../core/blitzkit/resetSession'; import { useApp } from '../../../stores/app';
import { setSession } from '../../../core/blitzkit/setSession'; import mutateSession, {
import { useSession } from '../../../stores/session'; SessionTracking,
import SessionPage from '../../embeds/session/page'; useSession,
import { Menu } from './components/Menu'; } from '../../../stores/session';
import * as styles from './page.css';
export default function Page() { 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 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 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(() => { useEffect(() => {
if (session.isTracking) input.current!.value = session.nickname; (async () => {
}, [session]); 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 ( return (
<PageWrapper color="blue"> <PageWrapper>
<div className={styles.toolBar}> {/* omit totally to avoid accessing undefined player */}
<div style={{ flex: 1, boxSizing: 'border-box', position: 'relative' }}> {showFurtherVerification && (
<TextField.Root <AlertDialog.Root open>
defaultValue={session.isTracking ? session.nickname : undefined} <AlertDialog.Content>
ref={input} <AlertDialog.Title>We need further verification</AlertDialog.Title>
onChange={(event) => { <AlertDialog.Description>
event.stopPropagation(); 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) { {login && (
setShowSearchResults(true); <AlertDialog.Description mt="2" color="red">
setSearchResults(undefined); You are currently signed with a different account.
} 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>
</AlertDialog.Description> </AlertDialog.Description>
)}
<Text size="2"> <Flex gap="3" mt="4" justify="end">
<Checkbox <AlertDialog.Cancel>
mr="1" <Button
onCheckedChange={(checked) => variant="soft"
useSession.setState({ color="gray"
embedPrompt: !checked, onClick={() => {
}) mutateSession((draft) => {
} draft.tracking = false;
/> });
Don't show this again setShowFurtherVerification(false);
</Text> }}
>
<Flex gap="3" mt="4"> Cancel
<AlertDialog.Cancel> </Button>
<Button variant="soft">Got it!</Button> </AlertDialog.Cancel>
</AlertDialog.Cancel> <AlertDialog.Action>
</Flex> <Link
</AlertDialog.Content> href={`https://api.worldoftanks.${
</AlertDialog.Root> (session as SessionTracking).player.region
}/wot/auth/login/?application_id=${WARGAMING_APPLICATION_ID}&redirect_uri=${encodeURIComponent(
<DropdownMenu.Root> `${location.origin}/callback/wargaming/?return=${location.href}`,
<DropdownMenu.Trigger> )}`}
<Button variant="soft" className={styles.toolbarButton}> >
Options <Button variant="solid">Sign in</Button>
<CaretDownIcon /> </Link>
</Button> </AlertDialog.Action>
</DropdownMenu.Trigger> </Flex>
</AlertDialog.Content>
<Menu Builder={DropdownMenu} reset={resetSession} /> </AlertDialog.Root>
</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>
)} )}
<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> </PageWrapper>
); );
} }
+9
View File
@@ -11,3 +11,12 @@ export const UNLOCALIZED_REGION_NAMES: Record<Region, string> = {
asia: 'Asia', asia: 'Asia',
eu: 'Europe', eu: 'Europe',
}; };
/**
* @deprecated
*/
export const UNLOCALIZED_REGION_NAMES_SHORT: Record<Region, string> = {
com: 'NA',
asia: 'APAC',
eu: 'EU',
};
+12
View File
@@ -1,11 +1,19 @@
import { produce } from 'immer';
import { merge } from 'lodash'; import { merge } from 'lodash';
import { create } from 'zustand'; import { create } from 'zustand';
import { persist } from 'zustand/middleware'; import { persist } from 'zustand/middleware';
interface Login {
id: number;
token: string;
expiresAt: number;
}
export interface App { export interface App {
devBuildAgreementTime: number; devBuildAgreementTime: number;
developerMode: boolean; developerMode: boolean;
darkMode: boolean; darkMode: boolean;
login?: Login;
} }
export const useApp = create<App>()( export const useApp = create<App>()(
@@ -18,3 +26,7 @@ export const useApp = create<App>()(
{ name: 'app', merge: (a, b) => merge(b, a) }, { name: 'app', merge: (a, b) => merge(b, a) },
), ),
); );
export function mutateApp(recipe: (draft: App) => void) {
useApp.setState(produce(recipe));
}
+16 -42
View File
@@ -2,56 +2,30 @@ import { produce } from 'immer';
import { merge } from 'lodash'; import { merge } from 'lodash';
import { create } from 'zustand'; import { create } from 'zustand';
import { persist } from 'zustand/middleware'; 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 { Region } from '../constants/regions';
import { NormalizedTankStats } from '../types/tanksStats';
type Session = ( interface SessionBase {}
| {
isTracking: false;
}
| {
isTracking: true;
region: Region; export interface SessionTracking extends SessionBase {
id: number; tracking: true;
nickname: string; player: {
tankStats: NormalizedTankStats; id: number;
time: number; region: Region;
} };
) & { }
title: string;
resetPrompt: boolean; interface SessionNotTracking extends SessionBase {
embedPrompt: boolean; tracking: false;
customColumns: { }
display: CustomColumnDisplay;
showDelta: boolean; type Session = SessionTracking | SessionNotTracking;
}[] & { length: 4 };
showTotal: boolean;
showCareer: boolean;
color: AccentColor | GrayColor;
};
export const useSession = create<Session>()( export const useSession = create<Session>()(
persist( persist(
(set) => ({ (set) => ({
title: 'Untitled session', tracking: false,
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',
}), }),
{ name: 'session', merge: (a, b) => merge(b, a) }, { name: 'session-2', merge: (a, b) => merge(b, a) },
), ),
); );