feat: added options

This commit is contained in:
TrèsAbhi committed 2023-10-01 18:43:40 -05:00
1 parent 8264f19ad4
commit 7f431b07e1
7 files changed
+250 -87

No files matched your search

+1
View File
@@ -37,6 +37,7 @@
"esbuild": "^0.19.3", "esbuild": "^0.19.3",
"express": "^4.18.2", "express": "^4.18.2",
"fuzzysort": "^2.0.4", "fuzzysort": "^2.0.4",
"immer": "^10.0.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"markdown-escape": "^2.0.0", "markdown-escape": "^2.0.0",
"next": "^13.5.2", "next": "^13.5.2",
+109 -75
View File
@@ -12,6 +12,7 @@ import { TankopediaEntry, tankopedia } from '../../../core/blitz/tankopedia';
import { tankAverages } from '../../../core/blitzstars/tankAverages'; import { tankAverages } from '../../../core/blitzstars/tankAverages';
import { useSession } from '../../../stores/session'; import { useSession } from '../../../stores/session';
import { IndividualTankStats, TanksStats } from '../../../types/tanksStats'; import { IndividualTankStats, TanksStats } from '../../../types/tanksStats';
import { CustomColumnDisplay } from '../../tools/session/components/CustomColumn';
const REFRESH_RATE = 1 / 5; const REFRESH_RATE = 1 / 5;
@@ -139,9 +140,64 @@ export default function SessionPage() {
return () => clearInterval(interval); return () => clearInterval(interval);
}, [session]); }, [session]);
const totalRowStats: Record<
CustomColumnDisplay,
Breakdown.RowStat | undefined
> = {
battles: {
title: 'Battles',
current: sessionBattles?.toLocaleString(),
career: tankStatsArray
.reduce((accumulator, stats) => accumulator + stats.all.battles, 0)
.toLocaleString(),
},
winrate: {
title: 'Winrate',
current: sessionBattles ? `${(100 * sessionWinrate).toFixed(2)}%` : '--',
career: `${(100 * careerWinrate).toFixed(2)}%`,
delta: sessionBattles === 0 ? 0 : sessionWinrate - careerWinrate,
},
wn8: (() => {
if (!diff) return undefined;
const battlesWithWN8 = diff.list.reduce(
(accumulator, { currentWN8, stats }) =>
(currentWN8 === undefined ? 0 : stats.all.battles) + accumulator,
0,
);
const currentWN8 =
diff.list.reduce(
(accumulator, { currentWN8, stats }) =>
stats.all.battles * (currentWN8 ?? 0) + accumulator,
0,
) / battlesWithWN8!;
return {
title: 'WN8',
current: battlesWithWN8
? Math.round(currentWN8!).toLocaleString()
: '--',
percentile: battlesWithWN8 ? getWN8Percentile(currentWN8!) : undefined,
career: Math.round(diff.careerWN8).toLocaleString(),
};
})(),
damage: {
title: 'Damage',
delta: sessionBattles === 0 ? 0 : currentDamage - careerDamage,
current: sessionBattles
? Math.round(currentDamage).toLocaleString()
: '--',
career: Math.round(careerDamage).toLocaleString(),
},
none: undefined,
};
return ( return (
<Breakdown.Root> <Breakdown.Root>
{session.isTracking && since !== undefined && diff !== undefined && ( {session.showTotal &&
session.isTracking &&
since !== undefined &&
diff !== undefined && (
<Breakdown.Row <Breakdown.Row
title={`Since ${since.toLocaleString(undefined, { title={`Since ${since.toLocaleString(undefined, {
hour: 'numeric', hour: 'numeric',
@@ -151,95 +207,47 @@ export default function SessionPage() {
year: 'numeric', year: 'numeric',
day: 'numeric', day: 'numeric',
})}`} })}`}
stats={[ stats={session.customColumns.map((customColumn) => {
{ const rowStat = totalRowStats[customColumn.display];
title: 'Battles',
current: sessionBattles!.toLocaleString(), if (!rowStat) return undefined;
career: tankStatsArray
.reduce(
(accumulator, stats) => accumulator + stats.all.battles,
0,
)
.toLocaleString(),
},
{
title: 'Winrate',
current: sessionBattles
? `${(100 * sessionWinrate).toFixed(2)}%`
: '--',
career: `${(100 * careerWinrate).toFixed(2)}%`,
delta: sessionBattles === 0 ? 0 : sessionWinrate - careerWinrate,
},
(() => {
const battlesWithWN8 = diff.list.reduce(
(accumulator, { currentWN8, stats }) =>
(currentWN8 === undefined ? 0 : stats.all.battles) +
accumulator,
0,
);
const currentWN8 =
diff.list.reduce(
(accumulator, { currentWN8, stats }) =>
stats.all.battles * (currentWN8 ?? 0) + accumulator,
0,
) / battlesWithWN8;
return { return {
title: 'WN8', ...rowStat,
current: battlesWithWN8 delta:
? Math.round(currentWN8).toLocaleString() customColumn.showDelta && rowStat.percentile === undefined
: '--', ? rowStat.delta
percentile: battlesWithWN8
? getWN8Percentile(currentWN8)
: undefined, : undefined,
career: Math.round(diff.careerWN8).toLocaleString(),
}; };
})(), })}
{
title: 'Damage',
delta: sessionBattles === 0 ? 0 : currentDamage - careerDamage,
current: sessionBattles
? Math.round(currentDamage).toLocaleString()
: '--',
career: Math.round(careerDamage).toLocaleString(),
},
]}
/> />
)} )}
{diff?.list {diff?.list
.sort((a, b) => b.stats.last_battle_time - a.stats.last_battle_time) .sort((a, b) => b.stats.last_battle_time - a.stats.last_battle_time)
.map(({ stats, tankopedia, career, careerWN8, currentWN8 }) => ( .map(({ stats, tankopedia, career, careerWN8, currentWN8 }) => {
<Breakdown.Row const rowStats: Record<
key={stats.tank_id} CustomColumnDisplay,
title={tankopedia?.name ?? `Unknown tank ${stats.tank_id}`} Breakdown.RowStat | undefined
type="tank" > = {
tankType={tankopedia?.type} battles: {
treeType={(() => {
if (tankopedia?.is_collectible) return TreeTypeEnum.Collector;
if (tankopedia?.is_premium) return TreeTypeEnum.Premium;
})()}
stats={[
{
title: 'Battles', title: 'Battles',
current: stats.all.battles.toLocaleString(), current: stats.all.battles.toLocaleString(),
career: career.all.battles.toLocaleString(), career: career.all.battles.toLocaleString(),
}, },
{ winrate: {
title: 'Winrate', title: 'Winrate',
current: `${( current: `${(100 * (stats.all.wins / stats.all.battles)).toFixed(
100 * 2,
(stats.all.wins / stats.all.battles) )}%`,
).toFixed(2)}%`, career: `${(100 * (career.all.wins / career.all.battles)).toFixed(
career: `${( 2,
100 * )}%`,
(career.all.wins / career.all.battles)
).toFixed(2)}%`,
delta: delta:
stats.all.wins / stats.all.battles - stats.all.wins / stats.all.battles -
career.all.wins / career.all.battles, career.all.wins / career.all.battles,
}, },
{ wn8: {
title: 'WN8', title: 'WN8',
current: current:
currentWN8 === undefined currentWN8 === undefined
@@ -254,7 +262,7 @@ export default function SessionPage() {
? undefined ? undefined
: getWN8Percentile(currentWN8), : getWN8Percentile(currentWN8),
}, },
{ damage: {
title: 'Damage', title: 'Damage',
current: Math.round( current: Math.round(
stats.all.damage_dealt / stats.all.battles, stats.all.damage_dealt / stats.all.battles,
@@ -266,9 +274,35 @@ export default function SessionPage() {
stats.all.damage_dealt / stats.all.battles - stats.all.damage_dealt / stats.all.battles -
career.all.damage_dealt / career.all.battles, career.all.damage_dealt / career.all.battles,
}, },
]} none: undefined,
};
return (
<Breakdown.Row
key={stats.tank_id}
title={tankopedia?.name ?? `Unknown tank ${stats.tank_id}`}
type="tank"
tankType={tankopedia?.type}
treeType={(() => {
if (tankopedia?.is_collectible) return TreeTypeEnum.Collector;
if (tankopedia?.is_premium) return TreeTypeEnum.Premium;
})()}
stats={session.customColumns.map((customColumn) => {
const rowStat = rowStats[customColumn.display];
if (!rowStat) return undefined;
return {
...rowStat,
delta:
customColumn.showDelta && rowStat.percentile === undefined
? rowStat.delta
: undefined,
};
})}
/> />
))} );
})}
</Breakdown.Root> </Breakdown.Root>
); );
} }
@@ -0,0 +1,66 @@
import { DropdownMenu } from '@radix-ui/themes';
import mutateSession, { useSession } from '../../../../stores/session';
export type CustomColumnDisplay =
| 'battles'
| 'winrate'
| 'wn8'
| 'damage'
| 'none';
const NO_DELTA: CustomColumnDisplay[] = ['battles', 'wn8', 'none'];
interface CustomColumn {
column: number;
}
export function CustomColumn({ column }: CustomColumn) {
const customColumn = useSession((state) => state.customColumns[column]);
return (
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger>Column {column + 1}</DropdownMenu.SubTrigger>
<DropdownMenu.SubContent>
<DropdownMenu.Label>Display</DropdownMenu.Label>
<DropdownMenu.RadioGroup
value={customColumn.display}
onValueChange={(event) =>
mutateSession((draft) => {
draft.customColumns[column].display =
event as CustomColumnDisplay;
})
}
>
<DropdownMenu.RadioItem value="battles">
Battles
</DropdownMenu.RadioItem>
<DropdownMenu.RadioItem value="winrate">
Winrate
</DropdownMenu.RadioItem>
<DropdownMenu.RadioItem value="wn8">WN8</DropdownMenu.RadioItem>
<DropdownMenu.RadioItem value="damage">Damage</DropdownMenu.RadioItem>
<DropdownMenu.RadioItem value="none">None</DropdownMenu.RadioItem>
</DropdownMenu.RadioGroup>
{!NO_DELTA.includes(customColumn.display) && (
<>
<DropdownMenu.Separator />
<DropdownMenu.CheckboxItem
checked={customColumn.showDelta}
onCheckedChange={(checked) =>
mutateSession((draft) => {
draft.customColumns[column].showDelta = checked;
})
}
>
Show delta
</DropdownMenu.CheckboxItem>
</>
)}
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
);
}
+36 -3
View File
@@ -1,6 +1,11 @@
'use client'; 'use client';
import { CopyIcon, PersonIcon, ReloadIcon } from '@radix-ui/react-icons'; import {
CaretDownIcon,
CopyIcon,
PersonIcon,
ReloadIcon,
} from '@radix-ui/react-icons';
import { import {
AlertDialog, AlertDialog,
Button, Button,
@@ -23,10 +28,9 @@ import listPlayers, {
import { useSession } from '../../../stores/session'; import { useSession } from '../../../stores/session';
import { NormalizedTankStats, TanksStats } from '../../../types/tanksStats'; import { NormalizedTankStats, TanksStats } from '../../../types/tanksStats';
import SessionPage from '../../embeds/session/page'; import SessionPage from '../../embeds/session/page';
import { CustomColumn } from './components/CustomColumn';
import * as styles from './page.css'; import * as styles from './page.css';
// 1 once per 5 seconds
export default function Page() { export default function Page() {
const input = useRef<HTMLInputElement>(null); const input = useRef<HTMLInputElement>(null);
const [searchResults, setSearchResults] = useState< const [searchResults, setSearchResults] = useState<
@@ -150,6 +154,7 @@ export default function Page() {
<AlertDialog.Root> <AlertDialog.Root>
<AlertDialog.Trigger> <AlertDialog.Trigger>
<Button <Button
variant="soft"
onClick={(event) => { onClick={(event) => {
if (!session.promptBeforeReset) { if (!session.promptBeforeReset) {
event.preventDefault(); event.preventDefault();
@@ -206,6 +211,7 @@ export default function Page() {
<Tooltip content="Copy to clipboard"> <Tooltip content="Copy to clipboard">
<Button <Button
variant="soft"
className={styles.toolbarButton} className={styles.toolbarButton}
onClick={() => onClick={() =>
navigator.clipboard.writeText( navigator.clipboard.writeText(
@@ -216,6 +222,33 @@ export default function Page() {
<CopyIcon width="16" height="16" /> Embed <CopyIcon width="16" height="16" /> Embed
</Button> </Button>
</Tooltip> </Tooltip>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Button variant="soft" className={styles.toolbarButton}>
Options
<CaretDownIcon />
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<CustomColumn column={0} />
<CustomColumn column={1} />
<CustomColumn column={2} />
<CustomColumn column={3} />
<DropdownMenu.Separator />
<DropdownMenu.CheckboxItem
checked={session.showTotal}
onCheckedChange={(checked) =>
useSession.setState({ showTotal: checked })
}
>
Show total
</DropdownMenu.CheckboxItem>
</DropdownMenu.Content>
</DropdownMenu.Root>
</div> </div>
</div> </div>
+14 -9
View File
@@ -4,6 +4,14 @@ import { theme } from '../../../stitches.config';
import { TREE_TYPE_ICONS, TreeTypeEnum } from '../../Tanks'; import { TREE_TYPE_ICONS, TreeTypeEnum } from '../../Tanks';
import { RowStat } from './RowStat'; import { RowStat } from './RowStat';
export interface RowStat {
title: string;
current?: number | string;
career?: number | string;
delta?: number;
percentile?: Percentile;
}
interface RowProps { interface RowProps {
type?: 'tank' | 'summary'; type?: 'tank' | 'summary';
title: string; title: string;
@@ -11,13 +19,7 @@ interface RowProps {
treeType?: TreeTypeEnum; treeType?: TreeTypeEnum;
tankType?: TankType; tankType?: TankType;
stats: { stats: (RowStat | undefined)[];
title: string;
current?: number | string;
career?: number | string;
delta?: number;
percentile?: Percentile;
}[];
} }
export function Row(props: RowProps) { export function Row(props: RowProps) {
@@ -77,7 +79,9 @@ export function Row(props: RowProps) {
paddingBottom: 8, paddingBottom: 8,
}} }}
> >
{props.stats.map((row, index) => ( {props.stats.map(
(row, index) =>
row && (
<RowStat <RowStat
minimized={props.minimized} minimized={props.minimized}
key={index} key={index}
@@ -86,7 +90,8 @@ export function Row(props: RowProps) {
delta={row.delta} delta={row.delta}
percentile={row.percentile} percentile={row.percentile}
/> />
))} ),
)}
</div> </div>
</div> </div>
); );
+19
View File
@@ -1,5 +1,7 @@
import { produce } from 'immer';
import { create } from 'zustand'; import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware'; import { devtools, persist } from 'zustand/middleware';
import { CustomColumnDisplay } from '../app/tools/session/components/CustomColumn';
import { Region } from '../constants/regions'; import { Region } from '../constants/regions';
import { NormalizedTankStats } from '../types/tanksStats'; import { NormalizedTankStats } from '../types/tanksStats';
@@ -18,6 +20,11 @@ type Session = (
} }
) & { ) & {
promptBeforeReset: boolean; promptBeforeReset: boolean;
customColumns: {
display: CustomColumnDisplay;
showDelta: boolean;
}[] & { length: 4 };
showTotal: boolean;
}; };
export const useSession = create<Session>()( export const useSession = create<Session>()(
@@ -26,6 +33,14 @@ export const useSession = create<Session>()(
(set) => ({ (set) => ({
isTracking: false, isTracking: false,
promptBeforeReset: true, promptBeforeReset: true,
showTotal: true,
customColumns: [
{ display: 'battles', showDelta: false },
{ display: 'winrate', showDelta: true },
{ display: 'wn8', showDelta: false },
{ display: 'damage', showDelta: true },
],
}), }),
{ {
name: 'session', name: 'session',
@@ -33,3 +48,7 @@ export const useSession = create<Session>()(
), ),
), ),
); );
export default function mutateSession(recipe: (draft: Session) => void) {
useSession.setState(produce(recipe));
}
+5
View File
@@ -3078,6 +3078,11 @@ ignore@^5.1.8, ignore@^5.2.4:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324"
integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==
immer@^10.0.2:
version "10.0.2"
resolved "https://registry.yarnpkg.com/immer/-/immer-10.0.2.tgz#11636c5b77acf529e059582d76faf338beb56141"
integrity sha512-Rx3CqeqQ19sxUtYV9CU911Vhy8/721wRFnJv3REVGWUmoAcIwzifTsdmJte/MV+0/XpM35LZdQMBGkRIoLPwQA==
indent-string@^4.0.0: indent-string@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"