mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 11:39:50 +02:00
Merge branch '1.16.0' into main
This commit is contained in:
64 files changed
+5805
-1714
No files matched your search
@@ -1,6 +0,0 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { tankopedia } from '../../../core/blitzstars/tankopedia';
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json(await tankopedia);
|
||||
}
|
||||
@@ -6,11 +6,11 @@ import * as Breakdown from '../../../components/Breakdown';
|
||||
import { TreeTypeEnum } from '../../../components/Tanks';
|
||||
import { WARGAMING_APPLICATION_ID } from '../../../constants/wargamingApplicationID';
|
||||
import fetchBlitz from '../../../core/blitz/fetchBlitz';
|
||||
import { tankAverages } from '../../../core/blitzstars/tankAverages';
|
||||
import {
|
||||
TankopediaEntry,
|
||||
tankopedia,
|
||||
} from '../../../core/blitzstars/tankopedia';
|
||||
BlitzkriegTankDefinition,
|
||||
tankDefinitions,
|
||||
} from '../../../core/blitzkrieg/definitions/tanks';
|
||||
import { tankAverages } from '../../../core/blitzstars/tankAverages';
|
||||
import calculateWN8 from '../../../core/statistics/calculateWN8';
|
||||
import { deltaTankStats } from '../../../core/statistics/deltaTankStats';
|
||||
import getWN8Percentile from '../../../core/statistics/getWN8Percentile';
|
||||
@@ -30,7 +30,7 @@ export default function SessionPage() {
|
||||
|
||||
list: {
|
||||
stats: IndividualTankStats;
|
||||
tankopedia?: TankopediaEntry;
|
||||
tankDefinitions?: BlitzkriegTankDefinition;
|
||||
career: IndividualTankStats;
|
||||
currentWN8?: number;
|
||||
careerWN8?: number;
|
||||
@@ -88,7 +88,7 @@ export default function SessionPage() {
|
||||
}),
|
||||
{},
|
||||
);
|
||||
const awaitedTankopedia = await tankopedia;
|
||||
const awaitedTankDefinitions = await tankDefinitions;
|
||||
const awaitedTankAverages = await tankAverages;
|
||||
const careerWN8 =
|
||||
careerRaw[id].reduce(
|
||||
@@ -119,7 +119,7 @@ export default function SessionPage() {
|
||||
).map((stats) => {
|
||||
return {
|
||||
stats,
|
||||
tankopedia: awaitedTankopedia[stats.tank_id],
|
||||
tankDefinitions: awaitedTankDefinitions[stats.tank_id],
|
||||
career: career[stats.tank_id],
|
||||
currentWN8: awaitedTankAverages[stats.tank_id]
|
||||
? calculateWN8(
|
||||
@@ -231,90 +231,95 @@ export default function SessionPage() {
|
||||
.sort(
|
||||
(a, b) => b.stats.last_battle_time - a.stats.last_battle_time,
|
||||
)
|
||||
.map(({ stats, tankopedia, career, careerWN8, currentWN8 }) => {
|
||||
const rowStats: Record<
|
||||
CustomColumnDisplay,
|
||||
Breakdown.RowStatItem | undefined
|
||||
> = {
|
||||
battles: {
|
||||
title: 'Battles',
|
||||
current: stats.all.battles.toLocaleString(),
|
||||
career: career.all.battles.toLocaleString(),
|
||||
},
|
||||
winrate: {
|
||||
title: 'Winrate',
|
||||
current: `${(
|
||||
100 *
|
||||
(stats.all.wins / stats.all.battles)
|
||||
).toFixed(2)}%`,
|
||||
career: `${(
|
||||
100 *
|
||||
(career.all.wins / career.all.battles)
|
||||
).toFixed(2)}%`,
|
||||
delta:
|
||||
stats.all.wins / stats.all.battles -
|
||||
career.all.wins / career.all.battles,
|
||||
},
|
||||
wn8: {
|
||||
title: 'WN8',
|
||||
current:
|
||||
currentWN8 === undefined
|
||||
? undefined
|
||||
: Math.round(currentWN8).toLocaleString(),
|
||||
career:
|
||||
careerWN8 === undefined
|
||||
? undefined
|
||||
: Math.round(careerWN8).toLocaleString(),
|
||||
percentile:
|
||||
currentWN8 === undefined
|
||||
? undefined
|
||||
: getWN8Percentile(currentWN8),
|
||||
},
|
||||
damage: {
|
||||
title: 'Damage',
|
||||
current: Math.round(
|
||||
stats.all.damage_dealt / stats.all.battles,
|
||||
).toLocaleString(),
|
||||
career: Math.round(
|
||||
career.all.damage_dealt / career.all.battles,
|
||||
).toLocaleString(),
|
||||
delta:
|
||||
stats.all.damage_dealt / stats.all.battles -
|
||||
career.all.damage_dealt / career.all.battles,
|
||||
},
|
||||
none: undefined,
|
||||
};
|
||||
.map(
|
||||
({ stats, tankDefinitions, career, careerWN8, currentWN8 }) => {
|
||||
const rowStats: Record<
|
||||
CustomColumnDisplay,
|
||||
Breakdown.RowStatItem | undefined
|
||||
> = {
|
||||
battles: {
|
||||
title: 'Battles',
|
||||
current: stats.all.battles.toLocaleString(),
|
||||
career: career.all.battles.toLocaleString(),
|
||||
},
|
||||
winrate: {
|
||||
title: 'Winrate',
|
||||
current: `${(
|
||||
100 *
|
||||
(stats.all.wins / stats.all.battles)
|
||||
).toFixed(2)}%`,
|
||||
career: `${(
|
||||
100 *
|
||||
(career.all.wins / career.all.battles)
|
||||
).toFixed(2)}%`,
|
||||
delta:
|
||||
stats.all.wins / stats.all.battles -
|
||||
career.all.wins / career.all.battles,
|
||||
},
|
||||
wn8: {
|
||||
title: 'WN8',
|
||||
current:
|
||||
currentWN8 === undefined
|
||||
? undefined
|
||||
: Math.round(currentWN8).toLocaleString(),
|
||||
career:
|
||||
careerWN8 === undefined
|
||||
? undefined
|
||||
: Math.round(careerWN8).toLocaleString(),
|
||||
percentile:
|
||||
currentWN8 === undefined
|
||||
? undefined
|
||||
: getWN8Percentile(currentWN8),
|
||||
},
|
||||
damage: {
|
||||
title: 'Damage',
|
||||
current: Math.round(
|
||||
stats.all.damage_dealt / stats.all.battles,
|
||||
).toLocaleString(),
|
||||
career: Math.round(
|
||||
career.all.damage_dealt / career.all.battles,
|
||||
).toLocaleString(),
|
||||
delta:
|
||||
stats.all.damage_dealt / stats.all.battles -
|
||||
career.all.damage_dealt / career.all.battles,
|
||||
},
|
||||
none: undefined,
|
||||
};
|
||||
|
||||
return (
|
||||
<Breakdown.Row
|
||||
color={session.color}
|
||||
key={stats.tank_id}
|
||||
minimized={!session.showCareer}
|
||||
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];
|
||||
return (
|
||||
<Breakdown.Row
|
||||
color={session.color}
|
||||
key={stats.tank_id}
|
||||
minimized={!session.showCareer}
|
||||
title={
|
||||
tankDefinitions?.name ?? `Unknown tank ${stats.tank_id}`
|
||||
}
|
||||
type="tank"
|
||||
tankType={tankDefinitions?.type}
|
||||
treeType={(() => {
|
||||
if (tankDefinitions?.tree_type === 'collector')
|
||||
return TreeTypeEnum.Collector;
|
||||
if (tankDefinitions?.tree_type === 'premium')
|
||||
return TreeTypeEnum.Premium;
|
||||
})()}
|
||||
stats={session.customColumns.map((customColumn) => {
|
||||
const rowStat = rowStats[customColumn.display];
|
||||
|
||||
if (!rowStat) return undefined;
|
||||
if (!rowStat) return undefined;
|
||||
|
||||
return {
|
||||
...rowStat,
|
||||
delta:
|
||||
customColumn.showDelta &&
|
||||
rowStat.percentile === undefined
|
||||
? rowStat.delta
|
||||
: undefined,
|
||||
};
|
||||
})}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
return {
|
||||
...rowStat,
|
||||
delta:
|
||||
customColumn.showDelta &&
|
||||
rowStat.percentile === undefined
|
||||
? rowStat.delta
|
||||
: undefined,
|
||||
};
|
||||
})}
|
||||
/>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</Breakdown.Root>
|
||||
</div>
|
||||
</ContextMenu.Trigger>
|
||||
|
||||
+8
-1
@@ -36,12 +36,19 @@ export default function RootLayout({ children }: RootLayoutProps) {
|
||||
<body
|
||||
style={{
|
||||
margin: 0,
|
||||
paddingTop: isEmbed ? 0 : '3.25rem',
|
||||
background: isEmbed
|
||||
? 'transparent'
|
||||
: 'url(https://i.imgur.com/PhS06NJ.png)',
|
||||
}}
|
||||
>
|
||||
<Theme appearance="dark" panelBackground="translucent" radius="full">
|
||||
<Theme
|
||||
appearance="dark"
|
||||
panelBackground="translucent"
|
||||
radius="full"
|
||||
suppressHydrationWarning
|
||||
suppressContentEditableWarning
|
||||
>
|
||||
{!isEmbed && <Navbar />}
|
||||
|
||||
<Flex direction="column">{children}</Flex>
|
||||
|
||||
@@ -11,7 +11,7 @@ interface ToolsLayoutProps {
|
||||
|
||||
export default function ToolsLayout({ children }: ToolsLayoutProps) {
|
||||
const pathname = usePathname();
|
||||
const toolId = pathname.split('/').at(-1);
|
||||
const toolId = pathname.split('/').at(2);
|
||||
const tool = TOOLS.find((tool) => tool.id === toolId);
|
||||
|
||||
return (
|
||||
|
||||
@@ -59,6 +59,8 @@ export function PageTurner({
|
||||
type="number"
|
||||
ref={pageInput}
|
||||
style={{ width: 64, textAlign: 'center' }}
|
||||
min={1}
|
||||
max={pages}
|
||||
onBlur={(event) => {
|
||||
onPageChange(
|
||||
Math.max(
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
'use client';
|
||||
|
||||
import { ArrowLeftIcon, CaretRightIcon } from '@radix-ui/react-icons';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
Dialog,
|
||||
Flex,
|
||||
Heading,
|
||||
Slider,
|
||||
Tabs,
|
||||
Text,
|
||||
Theme,
|
||||
Tooltip,
|
||||
} from '@radix-ui/themes';
|
||||
import { OrbitControls } from '@react-three/drei';
|
||||
import { Canvas } from '@react-three/fiber';
|
||||
import Link from 'next/link';
|
||||
import { use, useEffect, useState } from 'react';
|
||||
import { Flag } from '../../../../components/Flag';
|
||||
import PageWrapper from '../../../../components/PageWrapper';
|
||||
import { asset } from '../../../../core/blitzkrieg/asset';
|
||||
import { gunDefinitions } from '../../../../core/blitzkrieg/definitions/guns';
|
||||
import { shellDefinitions } from '../../../../core/blitzkrieg/definitions/shells';
|
||||
import {
|
||||
TIER_ROMAN_NUMERALS,
|
||||
tankDefinitions,
|
||||
tanksDefinitionsArray,
|
||||
} from '../../../../core/blitzkrieg/definitions/tanks';
|
||||
import { turretDefinitions } from '../../../../core/blitzkrieg/definitions/turrets';
|
||||
import * as styles from '../page.css';
|
||||
|
||||
type Mode = 'model' | 'armor';
|
||||
|
||||
export default function Page({ params }: { params: { id: string } }) {
|
||||
const id = parseInt(params.id);
|
||||
const awaitedTankDefinitions = use(tankDefinitions);
|
||||
const awaitedTanksDefinitionsArray = use(tanksDefinitionsArray);
|
||||
const awaitedTurretDefinitions = use(turretDefinitions);
|
||||
const awaitedGunDefinitions = use(gunDefinitions);
|
||||
const awaitedShellDefinitions = use(shellDefinitions);
|
||||
const tank = awaitedTankDefinitions[id];
|
||||
const [turret, setTurret] = useState(
|
||||
awaitedTurretDefinitions[tank.turrets[0]],
|
||||
);
|
||||
const [gun, setGun] = useState(awaitedGunDefinitions[turret.guns[0]]);
|
||||
const [crew, setCrew] = useState(100);
|
||||
const [versusTank, setVersusTank] = useState(
|
||||
awaitedTanksDefinitionsArray[0].id,
|
||||
);
|
||||
const [mode, setMode] = useState<Mode>('model');
|
||||
|
||||
console.log(versusTank);
|
||||
|
||||
useEffect(() => setGun(awaitedGunDefinitions[turret.guns[0]]), [turret]);
|
||||
|
||||
return (
|
||||
<PageWrapper>
|
||||
<Flex gap="8" direction="column">
|
||||
<Flex gap="4" direction="column">
|
||||
<Flex justify="between" align="center">
|
||||
<Link
|
||||
href="/tools/tankopedia"
|
||||
style={{ color: 'unset', textDecoration: 'none' }}
|
||||
>
|
||||
<Flex gap="1" align="center">
|
||||
<ArrowLeftIcon />
|
||||
<Text>Back to tankopedia</Text>
|
||||
</Flex>
|
||||
</Link>
|
||||
|
||||
<Flex gap="2" align="center">
|
||||
<Flag nation={tank.nation} />
|
||||
<Heading>{tank.name}</Heading>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<Card style={{ position: 'relative' }}>
|
||||
<Flex direction="column" gap="2">
|
||||
<Tabs.Root
|
||||
value={mode}
|
||||
onValueChange={(value) => setMode(value as Mode)}
|
||||
>
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger value="model">Model</Tabs.Trigger>
|
||||
<Tabs.Trigger value="armor">Armor</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
</Tabs.Root>
|
||||
|
||||
<div style={{ height: '50vh', maxHeight: 576 }}>
|
||||
<Canvas onPointerDown={(event) => event.preventDefault()}>
|
||||
<OrbitControls />
|
||||
|
||||
<ambientLight />
|
||||
<directionalLight />
|
||||
|
||||
<mesh>
|
||||
<torusKnotGeometry args={[undefined, undefined, 128, 16]} />
|
||||
<meshStandardMaterial color="cyan" />
|
||||
</mesh>
|
||||
</Canvas>
|
||||
</div>
|
||||
|
||||
{mode === 'armor' && (
|
||||
<Flex gap="2" align="center" className={styles.enhancedArmor}>
|
||||
<Checkbox defaultChecked />
|
||||
<Text>Enhanced armor</Text>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
</Card>
|
||||
|
||||
{mode === 'armor' && (
|
||||
<Card>
|
||||
<Flex align="center" justify="between" gap="2">
|
||||
<Flex align="center" gap="4">
|
||||
<Text>Versus</Text>
|
||||
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger>
|
||||
<Button variant="ghost">
|
||||
{awaitedTankDefinitions[versusTank].name}
|
||||
<CaretRightIcon />
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
|
||||
<Dialog.Content>Do something lol</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
</Flex>
|
||||
|
||||
<Flex gap="2" align="center">
|
||||
<Checkbox defaultChecked />
|
||||
<Text>Calibrated shells</Text>
|
||||
</Flex>
|
||||
|
||||
<Flex gap="1">
|
||||
<Button variant="solid" radius="small">
|
||||
<img src={asset('shells/ap.webp')} width={24} height={24} />
|
||||
</Button>
|
||||
<Button variant="soft" radius="small" color="gray">
|
||||
<img
|
||||
src={asset('shells/hc_premium.webp')}
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
</Button>
|
||||
<Button variant="soft" radius="small" color="gray">
|
||||
<img src={asset('shells/he.webp')} width={24} height={24} />
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Card>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<Flex gap="6" direction="column">
|
||||
<Heading>Build</Heading>
|
||||
|
||||
<Flex gap="2" direction="column">
|
||||
<Heading size="5" weight="regular">
|
||||
Modules
|
||||
</Heading>
|
||||
<Theme radius="small">
|
||||
<Flex gap="4">
|
||||
<Flex gap="1">
|
||||
{tank.turrets.map((turretId) => {
|
||||
const thisTurret = awaitedTurretDefinitions[turretId];
|
||||
|
||||
return (
|
||||
<Tooltip content={thisTurret.name}>
|
||||
<Button
|
||||
onClick={() => setTurret(thisTurret)}
|
||||
variant={
|
||||
turret.id === thisTurret.id ? 'solid' : 'soft'
|
||||
}
|
||||
>
|
||||
<img
|
||||
src="/images/modules/turret.png"
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
{TIER_ROMAN_NUMERALS[thisTurret.tier]}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
|
||||
<Flex gap="1">
|
||||
{turret.guns.map((gunId, index) => {
|
||||
const thisGun = awaitedGunDefinitions[gunId];
|
||||
|
||||
return (
|
||||
<Tooltip content={thisGun.name}>
|
||||
<Button
|
||||
onClick={() => setGun(thisGun)}
|
||||
variant={gun.id === thisGun.id ? 'solid' : 'soft'}
|
||||
>
|
||||
<img
|
||||
src="/images/modules/gun.png"
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
{TIER_ROMAN_NUMERALS[thisGun.tier]}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Theme>
|
||||
</Flex>
|
||||
|
||||
<Flex gap="2" direction="column">
|
||||
<Heading size="5" weight="regular">
|
||||
Crew
|
||||
</Heading>
|
||||
|
||||
<Flex gap="4" align="center">
|
||||
<Text>{crew}%</Text>
|
||||
|
||||
<Slider
|
||||
style={{ flex: 1 }}
|
||||
value={[crew]}
|
||||
min={50}
|
||||
max={100}
|
||||
onValueChange={([crew]) => setCrew(crew)}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<Flex gap="2" direction="column">
|
||||
<Heading size="5">Shells</Heading>
|
||||
|
||||
{gun.shells.map((shellId) => {
|
||||
const shell = awaitedShellDefinitions[shellId];
|
||||
|
||||
return (
|
||||
<Text>
|
||||
{shell.name} ({shell.type}): {shell.damage.armor}
|
||||
</Text>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const splitFilters = style({
|
||||
'@media': {
|
||||
'screen and (min-width: 513px)': {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const oneLineFilters = style({
|
||||
'@media': {
|
||||
'screen and (max-width: 512px)': {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,388 @@
|
||||
'use client';
|
||||
|
||||
import { CaretDownIcon } from '@radix-ui/react-icons';
|
||||
import { Button, DropdownMenu, Flex } from '@radix-ui/themes';
|
||||
import { use } from 'react';
|
||||
import { Flag } from '../../../../../components/Flag';
|
||||
import {
|
||||
TANK_TYPES,
|
||||
TREE_TYPES,
|
||||
TREE_TYPE_ICONS,
|
||||
TREE_TYPE_IMAGES,
|
||||
TreeTypeEnum,
|
||||
} from '../../../../../components/Tanks';
|
||||
import {
|
||||
NATIONS,
|
||||
TIERS,
|
||||
TIER_ROMAN_NUMERALS,
|
||||
} from '../../../../../core/blitzkrieg/definitions/tanks';
|
||||
import mutateTankopedia, {
|
||||
TankopediaTestTankDisplay,
|
||||
useTankopedia,
|
||||
} from '../../../../../stores/tankopedia';
|
||||
import * as styles from './index.css';
|
||||
|
||||
export function Options() {
|
||||
const nations = use(NATIONS);
|
||||
const tankopediaState = useTankopedia();
|
||||
|
||||
return (
|
||||
<Flex justify="center" direction="column" gap="2">
|
||||
<Flex gap="2" style={{ flex: 1 }} wrap="wrap">
|
||||
<Button
|
||||
color="red"
|
||||
onClick={() =>
|
||||
useTankopedia.setState({
|
||||
filters: {
|
||||
nations: [],
|
||||
tiers: [],
|
||||
treeTypes: [],
|
||||
types: [],
|
||||
test: 'include',
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<Button variant="soft">
|
||||
Test tanks
|
||||
<CaretDownIcon />
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content>
|
||||
<DropdownMenu.RadioGroup
|
||||
value={tankopediaState.filters.test}
|
||||
onValueChange={(value) =>
|
||||
mutateTankopedia((draft) => {
|
||||
draft.filters.test = value as TankopediaTestTankDisplay;
|
||||
})
|
||||
}
|
||||
>
|
||||
<DropdownMenu.RadioItem value="include">
|
||||
Include
|
||||
</DropdownMenu.RadioItem>
|
||||
<DropdownMenu.RadioItem value="exclude">
|
||||
Exclude
|
||||
</DropdownMenu.RadioItem>
|
||||
<DropdownMenu.RadioItem value="only">Only</DropdownMenu.RadioItem>
|
||||
</DropdownMenu.RadioGroup>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
<Flex style={{ flex: 1 }}>
|
||||
{TREE_TYPES.map((type, index) => (
|
||||
<Button
|
||||
key={type}
|
||||
variant={
|
||||
tankopediaState.filters.treeTypes.includes(type)
|
||||
? 'solid'
|
||||
: 'soft'
|
||||
}
|
||||
style={{
|
||||
flex: 1,
|
||||
margin: -0.5,
|
||||
borderTopLeftRadius: index === 0 ? undefined : 0,
|
||||
borderBottomLeftRadius: index === 0 ? undefined : 0,
|
||||
borderTopRightRadius:
|
||||
index === TREE_TYPES.length - 1 ? undefined : 0,
|
||||
borderBottomRightRadius:
|
||||
index === TREE_TYPES.length - 1 ? undefined : 0,
|
||||
}}
|
||||
onClick={() =>
|
||||
mutateTankopedia((draft) => {
|
||||
if (draft.filters.treeTypes.includes(type)) {
|
||||
draft.filters.treeTypes = draft.filters.treeTypes.filter(
|
||||
(preexistingType) => preexistingType !== type,
|
||||
);
|
||||
} else {
|
||||
draft.filters.treeTypes.push(type);
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
<img
|
||||
src={TREE_TYPE_IMAGES[type]}
|
||||
style={{ width: '1em', height: '1em' }}
|
||||
/>
|
||||
</Button>
|
||||
))}
|
||||
</Flex>
|
||||
<Flex style={{ flex: 1 }}>
|
||||
{[...TANK_TYPES].reverse().map((type, index) => (
|
||||
<Button
|
||||
key={type}
|
||||
variant={
|
||||
tankopediaState.filters.types.includes(type) ? 'solid' : 'soft'
|
||||
}
|
||||
style={{
|
||||
flex: 1,
|
||||
margin: -0.5,
|
||||
borderTopLeftRadius: index === 0 ? undefined : 0,
|
||||
borderBottomLeftRadius: index === 0 ? undefined : 0,
|
||||
borderTopRightRadius:
|
||||
index === TANK_TYPES.length - 1 ? undefined : 0,
|
||||
borderBottomRightRadius:
|
||||
index === TANK_TYPES.length - 1 ? undefined : 0,
|
||||
}}
|
||||
onClick={() =>
|
||||
mutateTankopedia((draft) => {
|
||||
if (draft.filters.types.includes(type)) {
|
||||
draft.filters.types = draft.filters.types.filter(
|
||||
(preexistingType) => preexistingType !== type,
|
||||
);
|
||||
} else {
|
||||
draft.filters.types.push(type);
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
<img
|
||||
src={TREE_TYPE_ICONS[TreeTypeEnum.TechTree][type]}
|
||||
style={{ width: '1em', height: '1em' }}
|
||||
/>
|
||||
</Button>
|
||||
))}
|
||||
</Flex>
|
||||
<Flex
|
||||
direction="column"
|
||||
className={styles.splitFilters}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<Flex>
|
||||
{[...TIERS]
|
||||
.reverse()
|
||||
.slice(0, 5)
|
||||
.map((tier, index) => (
|
||||
<Button
|
||||
key={tier}
|
||||
variant={
|
||||
tankopediaState.filters.tiers.includes(tier)
|
||||
? 'solid'
|
||||
: 'soft'
|
||||
}
|
||||
style={{
|
||||
flex: 1,
|
||||
margin: -0.5,
|
||||
borderTopLeftRadius: index === 0 ? undefined : 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
borderTopRightRadius:
|
||||
index === TIERS.length - Math.round(TIERS.length / 2) - 1
|
||||
? undefined
|
||||
: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
}}
|
||||
onClick={() =>
|
||||
mutateTankopedia((draft) => {
|
||||
if (draft.filters.tiers.includes(tier)) {
|
||||
draft.filters.tiers = draft.filters.tiers.filter(
|
||||
(preexistingTier) => preexistingTier !== tier,
|
||||
);
|
||||
} else {
|
||||
draft.filters.tiers.push(tier);
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
{TIER_ROMAN_NUMERALS[tier]}
|
||||
</Button>
|
||||
))}
|
||||
</Flex>
|
||||
<Flex>
|
||||
{[...TIERS]
|
||||
.reverse()
|
||||
.slice(5, 10)
|
||||
.map((tier, index) => (
|
||||
<Button
|
||||
key={tier}
|
||||
variant={
|
||||
tankopediaState.filters.tiers.includes(tier)
|
||||
? 'solid'
|
||||
: 'soft'
|
||||
}
|
||||
style={{
|
||||
flex: 1,
|
||||
margin: -0.5,
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: index === 0 ? undefined : 0,
|
||||
borderTopRightRadius: 0,
|
||||
borderBottomRightRadius:
|
||||
index === TIERS.length - Math.round(TIERS.length / 2) - 1
|
||||
? undefined
|
||||
: 0,
|
||||
}}
|
||||
onClick={() =>
|
||||
mutateTankopedia((draft) => {
|
||||
if (draft.filters.tiers.includes(tier)) {
|
||||
draft.filters.tiers = draft.filters.tiers.filter(
|
||||
(preexistingTier) => preexistingTier !== tier,
|
||||
);
|
||||
} else {
|
||||
draft.filters.tiers.push(tier);
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
{TIER_ROMAN_NUMERALS[tier]}
|
||||
</Button>
|
||||
))}
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex
|
||||
direction="column"
|
||||
className={styles.splitFilters}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<Flex>
|
||||
{nations.slice(0, 5).map((nation, index) => (
|
||||
<Button
|
||||
key={nation}
|
||||
variant={
|
||||
tankopediaState.filters.nations.includes(nation)
|
||||
? 'solid'
|
||||
: 'soft'
|
||||
}
|
||||
style={{
|
||||
flex: 1,
|
||||
margin: -0.5,
|
||||
borderTopLeftRadius: index === 0 ? undefined : 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
borderTopRightRadius:
|
||||
index === nations.length - Math.round(nations.length / 2)
|
||||
? undefined
|
||||
: 0,
|
||||
borderBottomRightRadius:
|
||||
index === nations.length - 1 ? undefined : 0,
|
||||
}}
|
||||
onClick={() =>
|
||||
mutateTankopedia((draft) => {
|
||||
if (draft.filters.nations.includes(nation)) {
|
||||
draft.filters.nations = draft.filters.nations.filter(
|
||||
(preexistingType) => preexistingType !== nation,
|
||||
);
|
||||
} else {
|
||||
draft.filters.nations.push(nation);
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
<Flag nation={nation} />
|
||||
</Button>
|
||||
))}
|
||||
</Flex>
|
||||
|
||||
<Flex>
|
||||
{nations.slice(5, 10).map((nation, index) => (
|
||||
<Button
|
||||
key={nation}
|
||||
variant={
|
||||
tankopediaState.filters.nations.includes(nation)
|
||||
? 'solid'
|
||||
: 'soft'
|
||||
}
|
||||
style={{
|
||||
flex: 1,
|
||||
margin: -0.5,
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: index === 0 ? undefined : 0,
|
||||
borderTopRightRadius:
|
||||
index === nations.length - 1 ? undefined : 0,
|
||||
borderBottomRightRadius:
|
||||
index ===
|
||||
nations.length - Math.round(nations.length / 2) - 1
|
||||
? undefined
|
||||
: 0,
|
||||
}}
|
||||
onClick={() =>
|
||||
mutateTankopedia((draft) => {
|
||||
if (draft.filters.nations.includes(nation)) {
|
||||
draft.filters.nations = draft.filters.nations.filter(
|
||||
(preexistingType) => preexistingType !== nation,
|
||||
);
|
||||
} else {
|
||||
draft.filters.nations.push(nation);
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
<Flag nation={nation} />
|
||||
</Button>
|
||||
))}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<Flex gap="2" style={{ flex: 1 }} wrap="wrap">
|
||||
<Flex className={styles.oneLineFilters} style={{ flex: 1 }}>
|
||||
{[...TIERS].reverse().map((tier, index) => (
|
||||
<Button
|
||||
key={tier}
|
||||
variant={
|
||||
tankopediaState.filters.tiers.includes(tier) ? 'solid' : 'soft'
|
||||
}
|
||||
style={{
|
||||
flex: 1,
|
||||
margin: -0.5,
|
||||
borderTopLeftRadius: index === 0 ? undefined : 0,
|
||||
borderBottomLeftRadius: index === 0 ? undefined : 0,
|
||||
borderTopRightRadius:
|
||||
index === TIERS.length - 1 ? undefined : 0,
|
||||
borderBottomRightRadius:
|
||||
index === TIERS.length - 1 ? undefined : 0,
|
||||
}}
|
||||
onClick={() =>
|
||||
mutateTankopedia((draft) => {
|
||||
if (draft.filters.tiers.includes(tier)) {
|
||||
draft.filters.tiers = draft.filters.tiers.filter(
|
||||
(preexistingTier) => preexistingTier !== tier,
|
||||
);
|
||||
} else {
|
||||
draft.filters.tiers.push(tier);
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
{TIER_ROMAN_NUMERALS[tier]}
|
||||
</Button>
|
||||
))}
|
||||
</Flex>
|
||||
<Flex className={styles.oneLineFilters} style={{ flex: 1 }}>
|
||||
{nations.map((nation, index) => (
|
||||
<Button
|
||||
key={nation}
|
||||
variant={
|
||||
tankopediaState.filters.nations.includes(nation)
|
||||
? 'solid'
|
||||
: 'soft'
|
||||
}
|
||||
style={{
|
||||
flex: 1,
|
||||
margin: -0.5,
|
||||
borderTopLeftRadius: index === 0 ? undefined : 0,
|
||||
borderBottomLeftRadius: index === 0 ? undefined : 0,
|
||||
borderTopRightRadius:
|
||||
index === nations.length - 1 ? undefined : 0,
|
||||
borderBottomRightRadius:
|
||||
index === nations.length - 1 ? undefined : 0,
|
||||
}}
|
||||
onClick={() =>
|
||||
mutateTankopedia((draft) => {
|
||||
if (draft.filters.nations.includes(nation)) {
|
||||
draft.filters.nations = draft.filters.nations.filter(
|
||||
(preexistingType) => preexistingType !== nation,
|
||||
);
|
||||
} else {
|
||||
draft.filters.nations.push(nation);
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
<Flag nation={nation} />
|
||||
</Button>
|
||||
))}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { CaretLeftIcon, CaretRightIcon } from '@radix-ui/react-icons';
|
||||
import { Button, Flex, TextField } from '@radix-ui/themes';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { BlitzkriegTankDefinition } from '../../../../core/blitzkrieg/definitions/tanks';
|
||||
|
||||
interface PageTurnerProps {
|
||||
page: number;
|
||||
setPage: (page: number) => void;
|
||||
searchedList: BlitzkriegTankDefinition[];
|
||||
tanksPerPage: number;
|
||||
}
|
||||
|
||||
export function PageTurner({
|
||||
page,
|
||||
setPage,
|
||||
searchedList,
|
||||
tanksPerPage,
|
||||
}: PageTurnerProps) {
|
||||
const pageInput = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (pageInput.current) pageInput.current.value = `${page + 1}`;
|
||||
}, [page]);
|
||||
|
||||
return (
|
||||
<Flex align="center" justify="center" gap="2">
|
||||
<Button
|
||||
variant="soft"
|
||||
disabled={page === 0}
|
||||
onClick={() => setPage(Math.max(0, page - 1))}
|
||||
>
|
||||
<CaretLeftIcon />
|
||||
</Button>
|
||||
<TextField.Root>
|
||||
<TextField.Slot>Page</TextField.Slot>
|
||||
<TextField.Input
|
||||
defaultValue={1}
|
||||
type="number"
|
||||
ref={pageInput}
|
||||
min={1}
|
||||
max={Math.floor(searchedList.length / tanksPerPage) + 1}
|
||||
style={{ width: 64, textAlign: 'center' }}
|
||||
onBlur={(event) => {
|
||||
setPage(
|
||||
Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
Math.floor(searchedList.length / tanksPerPage),
|
||||
event.target.valueAsNumber - 1,
|
||||
),
|
||||
),
|
||||
);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
(event.target as HTMLInputElement).blur();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<TextField.Slot>
|
||||
out of {Math.floor(searchedList.length / tanksPerPage) + 1}
|
||||
</TextField.Slot>
|
||||
</TextField.Root>
|
||||
<Button
|
||||
variant="soft"
|
||||
disabled={Math.floor(searchedList.length / tanksPerPage) === page}
|
||||
onClick={() =>
|
||||
setPage(
|
||||
Math.min(Math.floor(searchedList.length / tanksPerPage), page + 1),
|
||||
)
|
||||
}
|
||||
>
|
||||
<CaretRightIcon />
|
||||
</Button>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
import { theme } from '../../../stitches.config';
|
||||
|
||||
export const listing = style({});
|
||||
|
||||
export const flag = style({
|
||||
transitionDuration: theme.durations.regular,
|
||||
left: '0',
|
||||
top: '0',
|
||||
|
||||
[`.${listing}:hover &, .${listing}:active &`]: {
|
||||
transform: 'translateX(-25%)',
|
||||
filter: 'blur(3rem)',
|
||||
},
|
||||
});
|
||||
|
||||
export const listingShadow = style({
|
||||
boxShadow: `inset 128px -128px 128px -128px ${theme.colors.appBackground1}`,
|
||||
|
||||
[`.${listing}:hover &, .${listing}:active &`]: {
|
||||
boxShadow: `inset 0 0 0 -128px ${theme.colors.appBackground1}`,
|
||||
},
|
||||
});
|
||||
|
||||
export const listingLabel = style({
|
||||
transition: `transform ${theme.durations.regular}`,
|
||||
transform: 'translateY(0)',
|
||||
|
||||
[`.${listing}:hover &, .${listing}:active &`]: {
|
||||
transform: 'translateY(200%)',
|
||||
},
|
||||
});
|
||||
|
||||
export const listingImage = style({
|
||||
transition: `transform ${theme.durations.regular}`,
|
||||
position: 'absolute',
|
||||
left: '50%',
|
||||
|
||||
[`.${listing}:hover &, .${listing}:active &`]: {
|
||||
transform: 'translate(-25%, 10%)',
|
||||
},
|
||||
});
|
||||
|
||||
export const enhancedArmor = style({
|
||||
'@media': {
|
||||
'screen and (min-width: 400px)': {
|
||||
position: 'absolute',
|
||||
right: 16,
|
||||
top: 16,
|
||||
},
|
||||
|
||||
'screen and (max-width: 399px)': {
|
||||
// left: 16,
|
||||
// bottom: 16,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,329 @@
|
||||
'use client';
|
||||
|
||||
import { slateDark } from '@radix-ui/colors';
|
||||
import {
|
||||
CaretDownIcon,
|
||||
CaretUpIcon,
|
||||
MagnifyingGlassIcon,
|
||||
} from '@radix-ui/react-icons';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
DropdownMenu,
|
||||
Flex,
|
||||
Inset,
|
||||
Text,
|
||||
TextField,
|
||||
} from '@radix-ui/themes';
|
||||
import { go } from 'fuzzysort';
|
||||
import { Suspense, use, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import PageWrapper from '../../../components/PageWrapper';
|
||||
import { asset } from '../../../core/blitzkrieg/asset';
|
||||
import {
|
||||
BlitzkriegTankDefinition,
|
||||
TANK_ICONS,
|
||||
TANK_ICONS_COLLECTOR,
|
||||
TANK_ICONS_PREMIUM,
|
||||
TIER_ROMAN_NUMERALS,
|
||||
tanksDefinitionsArray,
|
||||
} from '../../../core/blitzkrieg/definitions/tanks';
|
||||
import { tankIcon } from '../../../core/blitzkrieg/tankIcon';
|
||||
import { theme } from '../../../stitches.config';
|
||||
import mutateTankopedia, {
|
||||
TankopediaSortBy,
|
||||
TankopediaSortDirection,
|
||||
useTankopedia,
|
||||
} from '../../../stores/tankopedia';
|
||||
import { Options } from './components/Options';
|
||||
import { PageTurner } from './components/PageTurner';
|
||||
import * as styles from './page.css';
|
||||
|
||||
const TANKS_PER_PAGE = 3 * 2 ** 3;
|
||||
|
||||
export default function Page() {
|
||||
const tankopediaState = useTankopedia();
|
||||
const awaitedTanks = use(tanksDefinitionsArray);
|
||||
const input = useRef<HTMLInputElement>(null);
|
||||
const searchableTanks = useMemo(
|
||||
() =>
|
||||
awaitedTanks
|
||||
.filter(
|
||||
(tank) =>
|
||||
(tankopediaState.filters.tiers.length === 0
|
||||
? true
|
||||
: tankopediaState.filters.tiers.includes(tank.tier)) &&
|
||||
(tankopediaState.filters.types.length === 0
|
||||
? true
|
||||
: tankopediaState.filters.types.includes(tank.type)) &&
|
||||
(tankopediaState.filters.treeTypes.length === 0
|
||||
? true
|
||||
: tankopediaState.filters.treeTypes.includes(tank.tree_type)) &&
|
||||
(tankopediaState.filters.nations.length === 0
|
||||
? true
|
||||
: tankopediaState.filters.nations.includes(tank.nation)) &&
|
||||
(tankopediaState.filters.test === 'include'
|
||||
? true
|
||||
: tankopediaState.filters.test === 'exclude'
|
||||
? !tank.testing
|
||||
: tank.testing),
|
||||
)
|
||||
.sort((a, b) => {
|
||||
let diff = 0;
|
||||
|
||||
if (tankopediaState.sort.by === 'tier') {
|
||||
diff = a.tier - b.tier;
|
||||
}
|
||||
if (tankopediaState.sort.by === 'name') {
|
||||
diff = a.name.localeCompare(b.name);
|
||||
}
|
||||
|
||||
return tankopediaState.sort.direction === 'ascending' ? diff : -diff;
|
||||
}),
|
||||
[tankopediaState.filters, tankopediaState.sort],
|
||||
);
|
||||
const [searchedList, setSearchedList] = useState(searchableTanks);
|
||||
const [page, setPage] = useState(0);
|
||||
const [showFilters, setShowFilters] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setSearchedList(searchableTanks);
|
||||
if (input.current) input.current.value = '';
|
||||
}, [searchableTanks]);
|
||||
|
||||
useEffect(() => {
|
||||
setPage(
|
||||
Math.min(
|
||||
Math.max(0, page),
|
||||
Math.ceil(searchedList.length / TANKS_PER_PAGE) - 1,
|
||||
),
|
||||
);
|
||||
}, [searchedList]);
|
||||
|
||||
return (
|
||||
<PageWrapper size="wide" color="purple">
|
||||
<Suspense fallback={<Text>Loading...</Text>}>
|
||||
<Flex direction="column" gap="4">
|
||||
<Flex gap="2">
|
||||
<TextField.Root style={{ flex: 1 }}>
|
||||
<TextField.Slot>
|
||||
<MagnifyingGlassIcon height="16" width="16" />
|
||||
</TextField.Slot>
|
||||
<TextField.Input
|
||||
ref={input}
|
||||
placeholder="Search tanks..."
|
||||
onChange={(event) => {
|
||||
if (event.target.value.length === 0) {
|
||||
setSearchedList(searchableTanks);
|
||||
} else {
|
||||
setSearchedList(
|
||||
go(event.target.value, searchableTanks, {
|
||||
keys: [
|
||||
'name',
|
||||
'id',
|
||||
] satisfies (keyof BlitzkriegTankDefinition)[],
|
||||
}).map(({ obj }) => obj),
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</TextField.Root>
|
||||
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<Button variant="soft">
|
||||
Sort
|
||||
<CaretDownIcon />
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content>
|
||||
<DropdownMenu.Label>By</DropdownMenu.Label>
|
||||
<DropdownMenu.RadioGroup
|
||||
value={tankopediaState.sort.by}
|
||||
onValueChange={(value) =>
|
||||
mutateTankopedia((draft) => {
|
||||
draft.sort.by = value as TankopediaSortBy;
|
||||
})
|
||||
}
|
||||
>
|
||||
<DropdownMenu.RadioItem value="tier">
|
||||
Tier
|
||||
</DropdownMenu.RadioItem>
|
||||
<DropdownMenu.RadioItem value="name">
|
||||
Name
|
||||
</DropdownMenu.RadioItem>
|
||||
</DropdownMenu.RadioGroup>
|
||||
|
||||
<DropdownMenu.Separator />
|
||||
|
||||
<DropdownMenu.Label>Direction</DropdownMenu.Label>
|
||||
<DropdownMenu.RadioGroup
|
||||
value={tankopediaState.sort.direction}
|
||||
onValueChange={(value) =>
|
||||
mutateTankopedia((draft) => {
|
||||
draft.sort.direction = value as TankopediaSortDirection;
|
||||
})
|
||||
}
|
||||
>
|
||||
<DropdownMenu.RadioItem value="ascending">
|
||||
Ascending
|
||||
</DropdownMenu.RadioItem>
|
||||
<DropdownMenu.RadioItem value="descending">
|
||||
Descending
|
||||
</DropdownMenu.RadioItem>
|
||||
</DropdownMenu.RadioGroup>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
|
||||
<Button
|
||||
variant="soft"
|
||||
onClick={() => setShowFilters((state) => !state)}
|
||||
>
|
||||
Filters {showFilters ? <CaretUpIcon /> : <CaretDownIcon />}
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
{showFilters && (
|
||||
<Card>
|
||||
<Suspense fallback={<Text>Loading...</Text>}>
|
||||
<Options />
|
||||
</Suspense>
|
||||
</Card>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<PageTurner
|
||||
page={page}
|
||||
setPage={setPage}
|
||||
tanksPerPage={TANKS_PER_PAGE}
|
||||
searchedList={searchedList}
|
||||
/>
|
||||
|
||||
<Flex wrap="wrap" gap="3" justify="center">
|
||||
{searchedList
|
||||
.map((tank) => (
|
||||
<a
|
||||
key={tank.id}
|
||||
href={`/tools/tankopedia/${tank.id}`}
|
||||
style={{
|
||||
flex: 1,
|
||||
textDecoration: 'none',
|
||||
color: 'unset',
|
||||
}}
|
||||
>
|
||||
<Card className={styles.listing}>
|
||||
<Inset
|
||||
key={tank.id}
|
||||
style={{
|
||||
minWidth: 256,
|
||||
minHeight: 128,
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
className={styles.flag}
|
||||
src={asset(`flags/scratched/${tank.nation}.webp`)}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'contain',
|
||||
position: 'absolute',
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
width: 256,
|
||||
height: 128,
|
||||
}}
|
||||
className={styles.listingImage}
|
||||
>
|
||||
<img
|
||||
src={tankIcon(tank.id)}
|
||||
style={{
|
||||
objectFit: 'contain',
|
||||
objectPosition: 'left center',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={styles.listingShadow}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
transition: `box-shadow ${theme.durations.regular}`,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Flex
|
||||
align="center"
|
||||
justify="between"
|
||||
className={styles.listingLabel}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 8,
|
||||
padding: '0 12px',
|
||||
width: '100%',
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
>
|
||||
<Flex align="center" justify="center" gap="1">
|
||||
<img
|
||||
src={
|
||||
(tank.tree_type === 'collector'
|
||||
? TANK_ICONS_COLLECTOR
|
||||
: tank.tree_type === 'premium'
|
||||
? TANK_ICONS_PREMIUM
|
||||
: TANK_ICONS)[tank.type]
|
||||
}
|
||||
style={{ width: '1em', height: '1em' }}
|
||||
/>
|
||||
<Text
|
||||
size="4"
|
||||
color={
|
||||
tank.tree_type === 'collector'
|
||||
? 'blue'
|
||||
: tank.tree_type === 'premium'
|
||||
? 'amber'
|
||||
: undefined
|
||||
}
|
||||
weight="medium"
|
||||
style={{
|
||||
color:
|
||||
tank.tree_type === 'researchable'
|
||||
? slateDark.slate12
|
||||
: undefined,
|
||||
whiteSpace: 'nowrap',
|
||||
maxWidth: 160,
|
||||
display: 'block',
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{tank.name}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Text size="4" color="gray" highContrast={false}>
|
||||
{TIER_ROMAN_NUMERALS[tank.tier]}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Inset>
|
||||
</Card>
|
||||
</a>
|
||||
))
|
||||
.slice(page * TANKS_PER_PAGE, (page + 1) * TANKS_PER_PAGE)}
|
||||
</Flex>
|
||||
|
||||
<PageTurner
|
||||
page={page}
|
||||
setPage={setPage}
|
||||
tanksPerPage={TANKS_PER_PAGE}
|
||||
searchedList={searchedList}
|
||||
/>
|
||||
</Suspense>
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user