mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 19:49:50 +02:00
feat: search page
This commit is contained in:
1 parent
408b561b75
commit
ffa1395b50
70 files changed
+492
-468
No files matched your search
@@ -0,0 +1,10 @@
|
||||
import { CaretRightIcon } from '@radix-ui/react-icons';
|
||||
import { Text } from '@radix-ui/themes';
|
||||
|
||||
export function Arrow() {
|
||||
return (
|
||||
<Text color="gray">
|
||||
<CaretRightIcon />
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import {
|
||||
asset,
|
||||
averageDefinitions,
|
||||
formatCompact,
|
||||
tankDefinitions,
|
||||
TIER_ROMAN_NUMERALS,
|
||||
} from '@blitzkit/core';
|
||||
import { Box, Flex, Text } from '@radix-ui/themes';
|
||||
import Link from 'next/link';
|
||||
import * as TankopediaEphemeral from '../../../../../../../../stores/tankopediaEphemeral';
|
||||
|
||||
interface NodeProps {
|
||||
id: number;
|
||||
nextIds?: number[];
|
||||
highlight?: boolean;
|
||||
}
|
||||
|
||||
const awaitedTankDefinitions = await tankDefinitions;
|
||||
const awaitedAverageDefinitions = await averageDefinitions;
|
||||
|
||||
export function Node({ id, highlight, nextIds }: NodeProps) {
|
||||
const xpMultiplier = TankopediaEphemeral.use((state) => state.xpMultiplier);
|
||||
const tank = awaitedTankDefinitions[id];
|
||||
const nextTanks = nextIds?.map((id) => awaitedTankDefinitions[id]);
|
||||
const thisTankXp =
|
||||
tank.xp === undefined || tank.tier === 1 ? 0 : tank.xp! / xpMultiplier;
|
||||
const nextTanksXp = nextTanks?.reduce(
|
||||
(xp, tank) =>
|
||||
xp +
|
||||
(tank.xp === undefined || tank.tier === 1 ? 0 : tank.xp! / xpMultiplier),
|
||||
0,
|
||||
);
|
||||
const averages = awaitedAverageDefinitions.averages[tank.id]?.mu;
|
||||
const averageXp = averages ? averages.xp / averages.battles : undefined;
|
||||
const games =
|
||||
averages && nextTanks ? Math.round(nextTanksXp! / averageXp!) : undefined;
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`/tools/tankopedia/${id}`}
|
||||
key={id}
|
||||
style={{
|
||||
textDecoration: 'none',
|
||||
color: 'inherit',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
mx={highlight ? '2' : '0'}
|
||||
style={{
|
||||
background: highlight
|
||||
? `url(${asset(`flags/fade_small/${tank.nation}.webp`)})`
|
||||
: undefined,
|
||||
backgroundPosition: 'center',
|
||||
backgroundSize: 'cover',
|
||||
boxShadow: highlight ? 'var(--shadow-4)' : undefined,
|
||||
borderRadius: 'var(--radius-4)',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
direction="column"
|
||||
align="center"
|
||||
py="4"
|
||||
px={highlight ? '4' : '2'}
|
||||
style={{
|
||||
backdropFilter: highlight ? 'brightness(0.25)' : undefined,
|
||||
WebkitBackdropFilter: highlight ? 'brightness(0.25)' : undefined,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
alt={tank.name}
|
||||
src={asset(`icons/tanks/big/${id}.webp`)}
|
||||
width={64}
|
||||
height={64}
|
||||
style={{
|
||||
objectFit: 'contain',
|
||||
}}
|
||||
/>
|
||||
|
||||
<Flex direction="column" align="center">
|
||||
<Flex gap="2" align="center">
|
||||
<Text color="gray" size="1">
|
||||
{TIER_ROMAN_NUMERALS[tank.tier]}
|
||||
</Text>
|
||||
<Text wrap="nowrap">{tank.name}</Text>
|
||||
</Flex>
|
||||
<Flex gap="2" align="center">
|
||||
<Text color="gray" size="1" wrap="nowrap">
|
||||
<Flex gap="1" align="center">
|
||||
<img
|
||||
alt="XP"
|
||||
src={asset('icons/currencies/xp.webp')}
|
||||
style={{
|
||||
width: '1em',
|
||||
height: '1em',
|
||||
objectFit: 'contain',
|
||||
objectPosition: 'center',
|
||||
}}
|
||||
/>
|
||||
{formatCompact(thisTankXp)}
|
||||
</Flex>
|
||||
</Text>
|
||||
<Text color="gray" size="1" wrap="nowrap">
|
||||
<Flex gap="1" align="center">
|
||||
<img
|
||||
alt="Silver"
|
||||
src={asset('icons/currencies/silver.webp')}
|
||||
style={{
|
||||
width: '1em',
|
||||
height: '1em',
|
||||
objectFit: 'contain',
|
||||
objectPosition: 'center',
|
||||
}}
|
||||
/>
|
||||
{formatCompact(tank.price.value)}
|
||||
</Flex>
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
{averages && nextTanks && (
|
||||
<Text color="gray" size="1" mt="2">
|
||||
{games!} {games === 1 ? 'battle' : 'battles'}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
import { asset, imgur, tankDefinitions } from '@blitzkit/core';
|
||||
import { CaretLeftIcon, CaretRightIcon, PlusIcon } from '@radix-ui/react-icons';
|
||||
import { Flex, Heading, IconButton, ScrollArea, Text } from '@radix-ui/themes';
|
||||
import { Fragment, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import * as Duel from '../../../../../../../stores/duel';
|
||||
import * as TankopediaEphemeral from '../../../../../../../stores/tankopediaEphemeral';
|
||||
import { Arrow } from './components/Arrow';
|
||||
import { Node } from './components/Node';
|
||||
|
||||
type Line = number[];
|
||||
|
||||
const awaitedTankDefinitions = await tankDefinitions;
|
||||
|
||||
export const XP_MULTIPLIERS = [1, 2, 3, 4, 5, 10];
|
||||
|
||||
export function TechTreeSection() {
|
||||
const xpMultiplier = TankopediaEphemeral.use((state) => state.xpMultiplier);
|
||||
const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation();
|
||||
const master = Duel.use((state) => state.protagonist.tank);
|
||||
const container = useRef<HTMLDivElement>(null);
|
||||
const lines = useMemo(() => {
|
||||
function extend(line: Line): Line[] {
|
||||
const root = awaitedTankDefinitions[line.at(-1)!];
|
||||
|
||||
if (root.ancestors === undefined || root.tier === 1) {
|
||||
return [line];
|
||||
} else {
|
||||
if (root.ancestors.length === 1 || root.tier === 2) {
|
||||
line.push(
|
||||
root.ancestors.find(
|
||||
(ancestor) => !awaitedTankDefinitions[ancestor].deprecated,
|
||||
) ?? root.ancestors[0],
|
||||
);
|
||||
return extend(line);
|
||||
} else {
|
||||
return root.ancestors
|
||||
.map((ancestor) => {
|
||||
const newLine = [...line, ancestor];
|
||||
return extend(newLine);
|
||||
})
|
||||
.flat();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return extend([master.id]);
|
||||
}, [master]);
|
||||
const [lineIndex, setLineIndex] = useState(0);
|
||||
const line = useMemo(
|
||||
() => [...lines[lineIndex]].reverse(),
|
||||
[master, lineIndex],
|
||||
);
|
||||
const totalXp = line.reduce(
|
||||
(xp, id) =>
|
||||
xp +
|
||||
(awaitedTankDefinitions[id].xp === undefined ||
|
||||
awaitedTankDefinitions[id].tier === 1
|
||||
? 0
|
||||
: awaitedTankDefinitions[id].xp),
|
||||
0,
|
||||
);
|
||||
const totalCredits = line.reduce(
|
||||
(credits, id) => credits + awaitedTankDefinitions[id].price.value,
|
||||
0,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!container.current) return;
|
||||
container.current.scrollLeft = container.current.scrollWidth;
|
||||
});
|
||||
|
||||
if (
|
||||
master.treeType !== 'researchable' ||
|
||||
master.ancestors === undefined ||
|
||||
master.ancestors.length === 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex
|
||||
direction="column"
|
||||
align="center"
|
||||
gap="0"
|
||||
style={{ backgroundColor: 'var(--color-surface)' }}
|
||||
py="6"
|
||||
>
|
||||
<Flex direction="column" align="center">
|
||||
<Heading size="6">Tech tree</Heading>
|
||||
|
||||
{master.tier > 1 && (
|
||||
<Flex gap="4" align="center">
|
||||
<Text color="gray" wrap="nowrap">
|
||||
<Flex gap="1" align="center">
|
||||
<img
|
||||
alt="XP"
|
||||
src={asset('icons/currencies/xp.webp')}
|
||||
style={{
|
||||
width: '1em',
|
||||
height: '1em',
|
||||
objectFit: 'contain',
|
||||
objectPosition: 'center',
|
||||
}}
|
||||
/>
|
||||
{Math.round(totalXp / xpMultiplier).toLocaleString()}
|
||||
</Flex>
|
||||
</Text>
|
||||
|
||||
<Text color="gray" wrap="nowrap">
|
||||
<Flex gap="1" align="center">
|
||||
<img
|
||||
alt="Silver"
|
||||
src={asset('icons/currencies/silver.webp')}
|
||||
style={{
|
||||
width: '1em',
|
||||
height: '1em',
|
||||
objectFit: 'contain',
|
||||
objectPosition: 'center',
|
||||
}}
|
||||
/>
|
||||
{totalCredits.toLocaleString()}
|
||||
</Flex>
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
<Flex gap="1" mt="2">
|
||||
{XP_MULTIPLIERS.map((multiplier) => {
|
||||
const selected = xpMultiplier === multiplier;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
key={multiplier}
|
||||
style={{
|
||||
background: `url(${imgur('7hDltb4')})`,
|
||||
backgroundSize: 'contain',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: 'center',
|
||||
opacity: selected ? 1 : 0.5,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => {
|
||||
mutateTankopediaEphemeral((draft) => {
|
||||
draft.xpMultiplier = multiplier;
|
||||
});
|
||||
}}
|
||||
width="2.5rem"
|
||||
justify="center"
|
||||
align="center"
|
||||
pt="1"
|
||||
pl="1"
|
||||
>
|
||||
<Text size="2">
|
||||
x<b>{multiplier}</b>
|
||||
</Text>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<ScrollArea type="hover" scrollbars="horizontal" ref={container}>
|
||||
<Flex align="center" gap="2" justify="center" p="4">
|
||||
{line.map((id, index) => {
|
||||
const last = index === line.length - 1;
|
||||
const tank = awaitedTankDefinitions[id];
|
||||
|
||||
return (
|
||||
<Fragment key={id}>
|
||||
{index > 0 && <Arrow key={`${id}-arrow`} />}
|
||||
<Node
|
||||
key={`${id}-node`}
|
||||
id={id}
|
||||
nextIds={last ? tank.successors : [line[index + 1]]}
|
||||
highlight={last}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
|
||||
{master.tier < 10 && (
|
||||
<>
|
||||
<Arrow />
|
||||
{master.successors!.map((id, index) => (
|
||||
<Fragment key={id}>
|
||||
{index > 0 && (
|
||||
<Text color="gray" key={`${id}-plus`}>
|
||||
<PlusIcon />
|
||||
</Text>
|
||||
)}
|
||||
<Node key={`${id}-node}`} id={id} />
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
</ScrollArea>
|
||||
|
||||
<Flex>
|
||||
{lines.length > 1 && (
|
||||
<Flex align="center" gap="2">
|
||||
<IconButton
|
||||
size="2"
|
||||
variant="soft"
|
||||
onClick={() =>
|
||||
setLineIndex((lines.length + lineIndex - 1) % lines.length)
|
||||
}
|
||||
>
|
||||
<CaretLeftIcon />
|
||||
</IconButton>
|
||||
<Text>
|
||||
Route {lineIndex + 1} of {lines.length}
|
||||
</Text>
|
||||
<IconButton
|
||||
size="2"
|
||||
variant="soft"
|
||||
onClick={() => setLineIndex((lineIndex + 1) % lines.length)}
|
||||
>
|
||||
<CaretRightIcon />
|
||||
</IconButton>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user