mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 18:57:03 +02:00
feat: perf
This commit is contained in:
1 parent
df8b32b7cd
commit
a57734cdee
6 files changed
+225
-158
No files matched your search
@@ -1,50 +1,25 @@
|
||||
import {
|
||||
Callout,
|
||||
Flex,
|
||||
Grid,
|
||||
Heading,
|
||||
Link,
|
||||
Separator,
|
||||
Text,
|
||||
} from '@radix-ui/themes';
|
||||
import { Callout, Flex } from '@radix-ui/themes';
|
||||
import { range } from 'lodash';
|
||||
import { memo, use } from 'react';
|
||||
import { classIcons } from '../../../../components/ClassIcon';
|
||||
import { ExperimentIcon } from '../../../../components/ExperimentIcon';
|
||||
import { TANK_CLASSES } from '../../../../components/Tanks/components/Item/constants';
|
||||
import { asset } from '../../../../core/blitzkit/asset';
|
||||
import { gameDefinitions } from '../../../../core/blitzkit/gameDefinitions';
|
||||
import {
|
||||
Tier,
|
||||
tanksDefinitionsArray,
|
||||
} from '../../../../core/blitzkit/tankDefinitions';
|
||||
import { TIER_ROMAN_NUMERALS } from '../../../../core/blitzkit/tankDefinitions/constants';
|
||||
import { tankIcon } from '../../../../core/blitzkit/tankIcon';
|
||||
import { TankopediaSearchPageFilters } from '../page';
|
||||
import { useTankopediaFilters } from '../../../../stores/tankopediaFilters';
|
||||
import { treeTypeOrder } from './TankSearch/constants';
|
||||
import { TierCard } from './TierCard';
|
||||
|
||||
interface ResultsProps {
|
||||
filters: TankopediaSearchPageFilters;
|
||||
}
|
||||
|
||||
export const Results = memo<ResultsProps>(({ filters }) => {
|
||||
const awaitedTanksDefinitionsArray = use(tanksDefinitionsArray);
|
||||
const tanksFiltered = awaitedTanksDefinitionsArray.filter((tank) => {
|
||||
return (
|
||||
(filters.tier === undefined || filters.tier === tank.tier) &&
|
||||
(filters.nation === undefined || filters.nation === tank.nation) &&
|
||||
(filters.class === undefined || filters.class === tank.class) &&
|
||||
(filters.type === undefined || filters.type === tank.treeType) &&
|
||||
(filters.testing === 'include' ||
|
||||
(filters.testing === 'only' && tank.testing) ||
|
||||
(filters.testing === 'exclude' && !tank.testing))
|
||||
);
|
||||
});
|
||||
export const Results = memo(() => {
|
||||
const tanks = use(tanksDefinitionsArray);
|
||||
const awaitedGameDefinitions = use(gameDefinitions);
|
||||
const testing = useTankopediaFilters((state) => state.testing);
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="8" flexGrow="1">
|
||||
{filters.testing === 'only' && (
|
||||
{testing === 'only' && (
|
||||
<Callout.Root color="amber" style={{ width: 'fit-content' }}>
|
||||
<Callout.Icon>
|
||||
<ExperimentIcon style={{ width: '1em', height: '1em' }} />
|
||||
@@ -58,7 +33,7 @@ export const Results = memo<ResultsProps>(({ filters }) => {
|
||||
|
||||
{range(10, 0).map((tierUntyped) => {
|
||||
const tier = tierUntyped as Tier;
|
||||
const tierTanks = tanksFiltered
|
||||
const tierTanks = tanks
|
||||
.filter((tank) => tank.tier === tier)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
@@ -75,95 +50,7 @@ export const Results = memo<ResultsProps>(({ filters }) => {
|
||||
awaitedGameDefinitions.nations.indexOf(b.nation),
|
||||
);
|
||||
|
||||
if (tierTanks.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Flex key={tier} direction="column" gap="4">
|
||||
<Flex align="center" mb="4" gap="4">
|
||||
<Heading weight="regular">
|
||||
Tier {TIER_ROMAN_NUMERALS[tier]}
|
||||
</Heading>
|
||||
<Separator style={{ flex: 1 }} />
|
||||
</Flex>
|
||||
|
||||
<Grid
|
||||
flexGrow="1"
|
||||
columns="repeat(auto-fill, minmax(100px, 1fr))"
|
||||
gap="2"
|
||||
gapY="6"
|
||||
>
|
||||
{tierTanks.map((tank) => {
|
||||
const Icon = classIcons[tank.class];
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={tank.id}
|
||||
size="1"
|
||||
color={
|
||||
tank.treeType === 'collector'
|
||||
? 'blue'
|
||||
: tank.treeType === 'premium'
|
||||
? 'amber'
|
||||
: 'gray'
|
||||
}
|
||||
highContrast={tank.treeType === 'researchable'}
|
||||
underline="hover"
|
||||
href={`/tools/tankopedia/${tank.id}`}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: 'var(--space-2)',
|
||||
background: `url(${asset(`flags/scratched/${tank.nation}.webp`)})`,
|
||||
backgroundSize: 'contain',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: 'top left',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
alt={tank.name}
|
||||
src={tankIcon(tank.id)}
|
||||
height={64}
|
||||
style={{
|
||||
width: '100%',
|
||||
objectPosition: 'center right',
|
||||
objectFit: 'contain',
|
||||
}}
|
||||
/>
|
||||
|
||||
<Flex
|
||||
justify="center"
|
||||
gap="1"
|
||||
align="center"
|
||||
overflow="hidden"
|
||||
width="100%"
|
||||
maxWidth="100%"
|
||||
>
|
||||
<Icon
|
||||
style={{
|
||||
width: '1em',
|
||||
height: '1em',
|
||||
minWidth: '1em',
|
||||
minHeight: '1em',
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
align="center"
|
||||
style={{
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
>
|
||||
{tank.name}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
</Flex>
|
||||
);
|
||||
return <TierCard tanks={tierTanks} key={tier} tier={tier} />;
|
||||
})}
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { Flex, Link, Text } from '@radix-ui/themes';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { classIcons } from '../../../../components/ClassIcon';
|
||||
import { asset } from '../../../../core/blitzkit/asset';
|
||||
import { TankDefinition } from '../../../../core/blitzkit/tankDefinitions';
|
||||
import { tankIcon } from '../../../../core/blitzkit/tankIcon';
|
||||
import { tankopediaFilterTank } from '../../../../core/blitzkit/tankopediaFilterTank';
|
||||
import { useTankopediaFilters } from '../../../../stores/tankopediaFilters';
|
||||
|
||||
interface TankCardProps {
|
||||
tank: TankDefinition;
|
||||
}
|
||||
|
||||
export function TankCard({ tank }: TankCardProps) {
|
||||
const Icon = classIcons[tank.class];
|
||||
const link = useRef<HTMLAnchorElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = useTankopediaFilters.subscribe((filters) => {
|
||||
if (!link.current) return;
|
||||
|
||||
const visible = tankopediaFilterTank(filters, tank);
|
||||
link.current.style.display = visible ? 'flex' : 'none';
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Link
|
||||
ref={link}
|
||||
size="1"
|
||||
color={
|
||||
tank.treeType === 'collector'
|
||||
? 'blue'
|
||||
: tank.treeType === 'premium'
|
||||
? 'amber'
|
||||
: 'gray'
|
||||
}
|
||||
highContrast={tank.treeType === 'researchable'}
|
||||
underline="hover"
|
||||
href={`/tools/tankopedia/${tank.id}`}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: 'var(--space-2)',
|
||||
background: `url(${asset(`flags/scratched/${tank.nation}.webp`)})`,
|
||||
backgroundSize: 'contain',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: 'top left',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
alt={tank.name}
|
||||
src={tankIcon(tank.id)}
|
||||
height={64}
|
||||
style={{
|
||||
width: '100%',
|
||||
objectPosition: 'center right',
|
||||
objectFit: 'contain',
|
||||
}}
|
||||
/>
|
||||
|
||||
<Flex
|
||||
justify="center"
|
||||
gap="1"
|
||||
align="center"
|
||||
overflow="hidden"
|
||||
width="100%"
|
||||
maxWidth="100%"
|
||||
>
|
||||
<Icon
|
||||
style={{
|
||||
width: '1em',
|
||||
height: '1em',
|
||||
minWidth: '1em',
|
||||
minHeight: '1em',
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
align="center"
|
||||
style={{
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
>
|
||||
{tank.name}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Flex, Grid, Heading, Separator } from '@radix-ui/themes';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import {
|
||||
TankDefinition,
|
||||
Tier,
|
||||
} from '../../../../core/blitzkit/tankDefinitions';
|
||||
import { TIER_ROMAN_NUMERALS } from '../../../../core/blitzkit/tankDefinitions/constants';
|
||||
import { tankopediaFilterTank } from '../../../../core/blitzkit/tankopediaFilterTank';
|
||||
import { useTankopediaFilters } from '../../../../stores/tankopediaFilters';
|
||||
import { TankCard } from './TankCard';
|
||||
|
||||
interface TierCardProps {
|
||||
tier: Tier;
|
||||
tanks: TankDefinition[];
|
||||
}
|
||||
|
||||
export function TierCard({ tier, tanks }: TierCardProps) {
|
||||
const container = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = useTankopediaFilters.subscribe((filters) => {
|
||||
if (!container.current) return;
|
||||
|
||||
const visible = tanks.some((tank) => tankopediaFilterTank(filters, tank));
|
||||
container.current.style.display = visible ? 'flex' : 'none';
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
ref={container}
|
||||
direction="column"
|
||||
gap="4"
|
||||
display={tanks.length === 0 ? 'none' : 'flex'}
|
||||
>
|
||||
<Flex align="center" mb="4" gap="4">
|
||||
<Heading weight="regular">Tier {TIER_ROMAN_NUMERALS[tier]}</Heading>
|
||||
<Separator style={{ flex: 1 }} />
|
||||
</Flex>
|
||||
|
||||
<Grid
|
||||
flexGrow="1"
|
||||
columns="repeat(auto-fill, minmax(100px, 1fr))"
|
||||
gap="2"
|
||||
gapY="6"
|
||||
>
|
||||
{tanks.map((tank) => (
|
||||
<TankCard key={tank.id} tank={tank} />
|
||||
))}
|
||||
</Grid>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -2,44 +2,28 @@
|
||||
|
||||
import { TrashIcon } from '@radix-ui/react-icons';
|
||||
import { Box, Button, Flex, IconButton, Text } from '@radix-ui/themes';
|
||||
import { produce } from 'immer';
|
||||
import { times } from 'lodash';
|
||||
import { use, useState } from 'react';
|
||||
import { use } from 'react';
|
||||
import { classIcons } from '../../../components/ClassIcon';
|
||||
import { ExperimentIcon } from '../../../components/ExperimentIcon';
|
||||
import PageWrapper from '../../../components/PageWrapper';
|
||||
import { ResearchedIcon } from '../../../components/ResearchedIcon';
|
||||
import { ScienceIcon } from '../../../components/ScienceIcon';
|
||||
import { ScienceOffIcon } from '../../../components/ScienceOffIcon';
|
||||
import { TankClass, TreeType } from '../../../components/Tanks';
|
||||
import { TANK_CLASSES } from '../../../components/Tanks/components/Item/constants';
|
||||
import { asset } from '../../../core/blitzkit/asset';
|
||||
import { gameDefinitions } from '../../../core/blitzkit/gameDefinitions';
|
||||
import { Tier } from '../../../core/blitzkit/tankDefinitions';
|
||||
import { TIER_ROMAN_NUMERALS } from '../../../core/blitzkit/tankDefinitions/constants';
|
||||
import {
|
||||
mutateTankopediaFilters,
|
||||
useTankopediaFilters,
|
||||
} from '../../../stores/tankopediaFilters';
|
||||
import { Results } from './components/Results';
|
||||
|
||||
export interface TankopediaSearchPageFilters {
|
||||
tier?: Tier;
|
||||
nation?: string;
|
||||
class?: TankClass;
|
||||
type?: TreeType;
|
||||
testing: 'include' | 'exclude' | 'only';
|
||||
}
|
||||
|
||||
const defaultTankopediaSearchPageFilters: TankopediaSearchPageFilters = {
|
||||
testing: 'include',
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const awaitedGameDefinitions = use(gameDefinitions);
|
||||
const [filters, setFilters] = useState<TankopediaSearchPageFilters>(
|
||||
defaultTankopediaSearchPageFilters,
|
||||
);
|
||||
|
||||
function mutateFilters(recipe: (draft: TankopediaSearchPageFilters) => void) {
|
||||
setFilters(produce(recipe));
|
||||
}
|
||||
const filters = useTankopediaFilters();
|
||||
|
||||
return (
|
||||
<PageWrapper color="purple" size={1028 + 256}>
|
||||
@@ -98,7 +82,7 @@ export default function Page() {
|
||||
color={selected ? undefined : 'gray'}
|
||||
highContrast
|
||||
onClick={() =>
|
||||
mutateFilters((draft) => {
|
||||
mutateTankopediaFilters((draft) => {
|
||||
draft.tier = draft.tier === tier ? undefined : tier;
|
||||
})
|
||||
}
|
||||
@@ -126,7 +110,7 @@ export default function Page() {
|
||||
color={selected ? undefined : 'gray'}
|
||||
highContrast
|
||||
onClick={() =>
|
||||
mutateFilters((draft) => {
|
||||
mutateTankopediaFilters((draft) => {
|
||||
draft.tier = draft.tier === tier ? undefined : tier;
|
||||
})
|
||||
}
|
||||
@@ -166,7 +150,7 @@ export default function Page() {
|
||||
highContrast
|
||||
radius="none"
|
||||
onClick={() =>
|
||||
mutateFilters((draft) => {
|
||||
mutateTankopediaFilters((draft) => {
|
||||
draft.nation =
|
||||
draft.nation === nation ? undefined : nation;
|
||||
})
|
||||
@@ -199,7 +183,7 @@ export default function Page() {
|
||||
highContrast
|
||||
radius="none"
|
||||
onClick={() =>
|
||||
mutateFilters((draft) => {
|
||||
mutateTankopediaFilters((draft) => {
|
||||
draft.nation =
|
||||
draft.nation === nation ? undefined : nation;
|
||||
})
|
||||
@@ -237,7 +221,7 @@ export default function Page() {
|
||||
color={selected ? undefined : 'gray'}
|
||||
highContrast
|
||||
onClick={() =>
|
||||
mutateFilters((draft) => {
|
||||
mutateTankopediaFilters((draft) => {
|
||||
draft.class =
|
||||
draft.class === tankClass ? undefined : tankClass;
|
||||
})
|
||||
@@ -265,7 +249,7 @@ export default function Page() {
|
||||
color={filters.type === 'researchable' ? undefined : 'gray'}
|
||||
highContrast
|
||||
onClick={() =>
|
||||
mutateFilters((draft) => {
|
||||
mutateTankopediaFilters((draft) => {
|
||||
draft.type =
|
||||
draft.type === 'researchable' ? undefined : 'researchable';
|
||||
})
|
||||
@@ -279,7 +263,7 @@ export default function Page() {
|
||||
color={filters.type === 'premium' ? 'amber' : 'gray'}
|
||||
highContrast
|
||||
onClick={() =>
|
||||
mutateFilters((draft) => {
|
||||
mutateTankopediaFilters((draft) => {
|
||||
draft.type = draft.type === 'premium' ? undefined : 'premium';
|
||||
})
|
||||
}
|
||||
@@ -297,7 +281,7 @@ export default function Page() {
|
||||
color={filters.type === 'collector' ? 'blue' : 'gray'}
|
||||
highContrast
|
||||
onClick={() =>
|
||||
mutateFilters((draft) => {
|
||||
mutateTankopediaFilters((draft) => {
|
||||
draft.type =
|
||||
draft.type === 'collector' ? undefined : 'collector';
|
||||
})
|
||||
@@ -328,7 +312,7 @@ export default function Page() {
|
||||
color={filters.testing === 'include' ? undefined : 'gray'}
|
||||
highContrast
|
||||
onClick={() =>
|
||||
mutateFilters((draft) => {
|
||||
mutateTankopediaFilters((draft) => {
|
||||
draft.testing = 'include';
|
||||
})
|
||||
}
|
||||
@@ -343,7 +327,7 @@ export default function Page() {
|
||||
color={filters.testing === 'exclude' ? undefined : 'gray'}
|
||||
highContrast
|
||||
onClick={() =>
|
||||
mutateFilters((draft) => {
|
||||
mutateTankopediaFilters((draft) => {
|
||||
draft.testing = 'exclude';
|
||||
})
|
||||
}
|
||||
@@ -358,7 +342,7 @@ export default function Page() {
|
||||
color={filters.testing === 'only' ? undefined : 'gray'}
|
||||
highContrast
|
||||
onClick={() =>
|
||||
mutateFilters((draft) => {
|
||||
mutateTankopediaFilters((draft) => {
|
||||
draft.testing = 'only';
|
||||
})
|
||||
}
|
||||
@@ -373,7 +357,11 @@ export default function Page() {
|
||||
<IconButton
|
||||
color="red"
|
||||
ml="2"
|
||||
onClick={() => setFilters(defaultTankopediaSearchPageFilters)}
|
||||
onClick={() =>
|
||||
useTankopediaFilters.setState(
|
||||
useTankopediaFilters.getInitialState(),
|
||||
)
|
||||
}
|
||||
>
|
||||
<TrashIcon />
|
||||
</IconButton>
|
||||
@@ -382,14 +370,18 @@ export default function Page() {
|
||||
<Button
|
||||
color="red"
|
||||
mt="2"
|
||||
onClick={() => setFilters(defaultTankopediaSearchPageFilters)}
|
||||
onClick={() =>
|
||||
useTankopediaFilters.setState(
|
||||
useTankopediaFilters.getInitialState(),
|
||||
)
|
||||
}
|
||||
>
|
||||
<TrashIcon /> Clear
|
||||
</Button>
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
<Results filters={filters} />
|
||||
<Results />
|
||||
</Flex>
|
||||
</PageWrapper>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { TankopediaFilters } from '../../stores/tankopediaFilters';
|
||||
import { TankDefinition } from './tankDefinitions';
|
||||
|
||||
export function tankopediaFilterTank(
|
||||
filters: TankopediaFilters,
|
||||
tank: TankDefinition,
|
||||
) {
|
||||
return (
|
||||
(filters.tier === undefined || filters.tier === tank.tier) &&
|
||||
(filters.nation === undefined || filters.nation === tank.nation) &&
|
||||
(filters.class === undefined || filters.class === tank.class) &&
|
||||
(filters.type === undefined || filters.type === tank.treeType) &&
|
||||
(filters.testing === 'include' ||
|
||||
(filters.testing === 'only' && tank.testing) ||
|
||||
(filters.testing === 'exclude' && !tank.testing))
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { produce } from 'immer';
|
||||
import { create } from 'zustand';
|
||||
import { TankClass, TreeType } from '../components/Tanks';
|
||||
import { Tier } from '../core/blitzkit/tankDefinitions';
|
||||
|
||||
export interface TankopediaFilters {
|
||||
tier?: Tier;
|
||||
nation?: string;
|
||||
class?: TankClass;
|
||||
type?: TreeType;
|
||||
testing: 'include' | 'exclude' | 'only';
|
||||
}
|
||||
|
||||
export const useTankopediaFilters = create<TankopediaFilters>()(() => ({
|
||||
testing: 'include',
|
||||
}));
|
||||
|
||||
export function mutateTankopediaFilters(
|
||||
recipe: (draft: TankopediaFilters) => void,
|
||||
) {
|
||||
useTankopediaFilters.setState(produce(recipe));
|
||||
}
|
||||
Reference in new issue
Block a user