mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 02:37:05 +02:00
feat: upgrade TankFilters
This commit is contained in:
1 parent
d287e66265
commit
6d24ed92d0
10 files changed
+475
-501
No files matched your search
@@ -1,17 +1,16 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { Table } from '@radix-ui/themes';
|
||||
import { times } from 'lodash-es';
|
||||
import { Fragment, Suspense, useEffect, useMemo, useState } from 'react';
|
||||
import { awaitableAverageDefinitions } from '../../core/awaitables/averageDefinitions';
|
||||
import { awaitableTankDefinitions } from '../../core/awaitables/tankDefinitions';
|
||||
import { filterTank } from '../../core/blitzkit/filterTank';
|
||||
import { Performance } from '../../stores/performance';
|
||||
import { $tankFilters } from '../../stores/tankFilters';
|
||||
import { TankPerformanceSort } from '../../stores/tankPerformanceSort';
|
||||
import type { MaybeSkeletonComponentProps } from '../../types/maybeSkeletonComponentProps';
|
||||
import { RowLoader } from './RowLoader';
|
||||
import { TankRow } from './TankRow';
|
||||
import { Total } from './Total';
|
||||
import { Table } from "@radix-ui/themes";
|
||||
import { times } from "lodash-es";
|
||||
import { Fragment, Suspense, useEffect, useMemo, useState } from "react";
|
||||
import { awaitableAverageDefinitions } from "../../core/awaitables/averageDefinitions";
|
||||
import { awaitableTankDefinitions } from "../../core/awaitables/tankDefinitions";
|
||||
import { filterTank } from "../../core/blitzkit/filterTank";
|
||||
import { Performance } from "../../stores/performance";
|
||||
import { TankFilters } from "../../stores/tankFilters";
|
||||
import { TankPerformanceSort } from "../../stores/tankPerformanceSort";
|
||||
import type { MaybeSkeletonComponentProps } from "../../types/maybeSkeletonComponentProps";
|
||||
import { RowLoader } from "./RowLoader";
|
||||
import { TankRow } from "./TankRow";
|
||||
import { Total } from "./Total";
|
||||
|
||||
const PREVIEW_COUNT = 10;
|
||||
const DEFAULT_LOADED_ROWS = 25;
|
||||
@@ -25,7 +24,7 @@ const averageDefinitionsArray = Object.entries(averageDefinitions.averages).map(
|
||||
([id, average]) => ({
|
||||
id: Number(id),
|
||||
...average,
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
export function Tanks({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
@@ -40,97 +39,97 @@ export function Tanks({ skeleton }: MaybeSkeletonComponentProps) {
|
||||
}
|
||||
|
||||
const sort = TankPerformanceSort.use();
|
||||
const filters = useStore($tankFilters);
|
||||
const tanksSorted = useMemo(() => {
|
||||
const { playerCountPeriod } = Performance.state;
|
||||
|
||||
switch (sort.type) {
|
||||
case 'accuracy':
|
||||
case "accuracy":
|
||||
return averageDefinitionsArray.sort(
|
||||
(a, b) =>
|
||||
sort.direction * (a.mu.hits / a.mu.shots - b.mu.hits / b.mu.shots),
|
||||
sort.direction * (a.mu.hits / a.mu.shots - b.mu.hits / b.mu.shots)
|
||||
);
|
||||
case 'capture_points':
|
||||
case "capture_points":
|
||||
return averageDefinitionsArray.sort(
|
||||
(a, b) =>
|
||||
sort.direction *
|
||||
(a.mu.capture_points / a.mu.battles -
|
||||
b.mu.capture_points / b.mu.battles),
|
||||
b.mu.capture_points / b.mu.battles)
|
||||
);
|
||||
case 'damage':
|
||||
case "damage":
|
||||
return averageDefinitionsArray.sort(
|
||||
(a, b) =>
|
||||
sort.direction *
|
||||
(a.mu.damage_dealt / a.mu.battles -
|
||||
b.mu.damage_dealt / b.mu.battles),
|
||||
b.mu.damage_dealt / b.mu.battles)
|
||||
);
|
||||
case 'damage_ratio':
|
||||
case "damage_ratio":
|
||||
return averageDefinitionsArray.sort(
|
||||
(a, b) =>
|
||||
sort.direction *
|
||||
(a.mu.damage_dealt / a.mu.damage_received -
|
||||
b.mu.damage_dealt / b.mu.damage_received),
|
||||
b.mu.damage_dealt / b.mu.damage_received)
|
||||
);
|
||||
case 'damage_taken':
|
||||
case "damage_taken":
|
||||
return averageDefinitionsArray.sort(
|
||||
(a, b) =>
|
||||
sort.direction *
|
||||
(a.mu.damage_received / a.mu.battles -
|
||||
b.mu.damage_received / b.mu.battles),
|
||||
b.mu.damage_received / b.mu.battles)
|
||||
);
|
||||
case 'hits':
|
||||
case "hits":
|
||||
return averageDefinitionsArray.sort(
|
||||
(a, b) =>
|
||||
sort.direction *
|
||||
(a.mu.hits / a.mu.battles - b.mu.hits / b.mu.battles),
|
||||
(a.mu.hits / a.mu.battles - b.mu.hits / b.mu.battles)
|
||||
);
|
||||
case 'kills':
|
||||
case "kills":
|
||||
return averageDefinitionsArray.sort(
|
||||
(a, b) =>
|
||||
sort.direction *
|
||||
(a.mu.frags / a.mu.battles - b.mu.frags / b.mu.battles),
|
||||
(a.mu.frags / a.mu.battles - b.mu.frags / b.mu.battles)
|
||||
);
|
||||
case 'shots':
|
||||
case "shots":
|
||||
return averageDefinitionsArray.sort(
|
||||
(a, b) =>
|
||||
sort.direction *
|
||||
(a.mu.shots / a.mu.battles - b.mu.shots / b.mu.battles),
|
||||
(a.mu.shots / a.mu.battles - b.mu.shots / b.mu.battles)
|
||||
);
|
||||
case 'players':
|
||||
case "players":
|
||||
return averageDefinitionsArray.sort(
|
||||
(a, b) =>
|
||||
sort.direction *
|
||||
(a.samples[playerCountPeriod] - b.samples[playerCountPeriod]),
|
||||
(a.samples[playerCountPeriod] - b.samples[playerCountPeriod])
|
||||
);
|
||||
case 'spots':
|
||||
case "spots":
|
||||
return averageDefinitionsArray.sort(
|
||||
(a, b) =>
|
||||
sort.direction *
|
||||
(a.mu.spotted / a.mu.battles - b.mu.spotted / b.mu.battles),
|
||||
(a.mu.spotted / a.mu.battles - b.mu.spotted / b.mu.battles)
|
||||
);
|
||||
case 'survival':
|
||||
case "survival":
|
||||
return averageDefinitionsArray.sort(
|
||||
(a, b) =>
|
||||
sort.direction *
|
||||
(a.mu.survived_battles / a.mu.battles -
|
||||
b.mu.survived_battles / b.mu.battles),
|
||||
b.mu.survived_battles / b.mu.battles)
|
||||
);
|
||||
case 'winrate':
|
||||
case "winrate":
|
||||
return averageDefinitionsArray.sort(
|
||||
(a, b) =>
|
||||
sort.direction *
|
||||
(a.mu.wins / a.mu.battles - b.mu.wins / b.mu.battles),
|
||||
(a.mu.wins / a.mu.battles - b.mu.wins / b.mu.battles)
|
||||
);
|
||||
case 'xp':
|
||||
case "xp":
|
||||
return averageDefinitionsArray.sort(
|
||||
(a, b) =>
|
||||
sort.direction * (a.mu.xp / a.mu.battles - b.mu.xp / b.mu.battles),
|
||||
sort.direction * (a.mu.xp / a.mu.battles - b.mu.xp / b.mu.battles)
|
||||
);
|
||||
}
|
||||
}, [sort]);
|
||||
const filters = TankFilters.use();
|
||||
const tanks = useMemo(() => {
|
||||
return tanksSorted.filter((tank) =>
|
||||
filterTank(filters, tankDefinitions.tanks[tank.id]),
|
||||
filterTank(filters, tankDefinitions.tanks[tank.id])
|
||||
);
|
||||
}, [filters, sort]);
|
||||
const [loadedRows, setLoadedRows] = useState(DEFAULT_LOADED_ROWS);
|
||||
|
||||
@@ -4,8 +4,7 @@ import {
|
||||
TANK_CLASSES,
|
||||
TankType,
|
||||
TIER_ROMAN_NUMERALS,
|
||||
} from '@blitzkit/core';
|
||||
import { useStore } from '@nanostores/react';
|
||||
} from "@blitzkit/core";
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
@@ -14,26 +13,22 @@ import {
|
||||
Link,
|
||||
Popover,
|
||||
Text,
|
||||
} from '@radix-ui/themes';
|
||||
import { isEqual, times } from 'lodash-es';
|
||||
import { awaitableGameDefinitions } from '../../../core/awaitables/gameDefinitions';
|
||||
import { Var } from '../../../core/radix/var';
|
||||
import { useLocale } from '../../../hooks/useLocale';
|
||||
import {
|
||||
$tankFilters,
|
||||
initialTankFilters,
|
||||
type TankFilters,
|
||||
} from '../../../stores/tankFilters';
|
||||
import { TankSort } from '../../../stores/tankopediaSort';
|
||||
import { classIcons } from '../../ClassIcon';
|
||||
import { ExperimentIcon } from '../../ExperimentIcon';
|
||||
import { GunAutoloaderIcon } from '../../GunAutoloaderIcon';
|
||||
import { GunAutoreloaderIcon } from '../../GunAutoreloaderIcon';
|
||||
import { GunRegularIcon } from '../../GunRegularIcon';
|
||||
import { MissingShellIcon } from '../../MissingShellIcon';
|
||||
import { ResearchedIcon } from '../../ResearchedIcon';
|
||||
import { ScienceIcon } from '../../ScienceIcon';
|
||||
import { ScienceOffIcon } from '../../ScienceOffIcon';
|
||||
} from "@radix-ui/themes";
|
||||
import { isEqual, times } from "lodash-es";
|
||||
import { awaitableGameDefinitions } from "../../../core/awaitables/gameDefinitions";
|
||||
import { Var } from "../../../core/radix/var";
|
||||
import { useLocale } from "../../../hooks/useLocale";
|
||||
import { TankFilters } from "../../../stores/tankFilters";
|
||||
import { TankSort } from "../../../stores/tankopediaSort";
|
||||
import { classIcons } from "../../ClassIcon";
|
||||
import { ExperimentIcon } from "../../ExperimentIcon";
|
||||
import { GunAutoloaderIcon } from "../../GunAutoloaderIcon";
|
||||
import { GunAutoreloaderIcon } from "../../GunAutoreloaderIcon";
|
||||
import { GunRegularIcon } from "../../GunRegularIcon";
|
||||
import { MissingShellIcon } from "../../MissingShellIcon";
|
||||
import { ResearchedIcon } from "../../ResearchedIcon";
|
||||
import { ScienceIcon } from "../../ScienceIcon";
|
||||
import { ScienceOffIcon } from "../../ScienceOffIcon";
|
||||
|
||||
interface FilterControlProps {
|
||||
compact?: boolean;
|
||||
@@ -42,29 +37,29 @@ interface FilterControlProps {
|
||||
const gameDefinitions = await awaitableGameDefinitions;
|
||||
|
||||
const shellTypeIcons: Record<ShellType, string> = {
|
||||
[ShellType.AP]: 'ap',
|
||||
[ShellType.APCR]: 'ap_cr',
|
||||
[ShellType.HE]: 'he',
|
||||
[ShellType.HEAT]: 'hc',
|
||||
[ShellType.AP]: "ap",
|
||||
[ShellType.APCR]: "ap_cr",
|
||||
[ShellType.HE]: "he",
|
||||
[ShellType.HEAT]: "hc",
|
||||
};
|
||||
|
||||
function ShellFilter({ index, premium }: { index: number; premium?: boolean }) {
|
||||
const tankFilters = useStore($tankFilters);
|
||||
const shells = TankFilters.use((state) => state.shells);
|
||||
|
||||
return (
|
||||
<Popover.Root>
|
||||
<Popover.Trigger>
|
||||
<IconButton variant="soft" radius="none" color="gray" highContrast>
|
||||
{tankFilters.shells[index] === null && (
|
||||
<Text color="gray" style={{ display: 'contents' }}>
|
||||
{shells[index] === null && (
|
||||
<Text color="gray" style={{ display: "contents" }}>
|
||||
<MissingShellIcon width="1em" height="1em" />
|
||||
</Text>
|
||||
)}
|
||||
{tankFilters.shells[index] !== null && (
|
||||
{shells[index] !== null && (
|
||||
<img
|
||||
style={{ width: '1em', height: '1em' }}
|
||||
style={{ width: "1em", height: "1em" }}
|
||||
src={asset(
|
||||
`icons/shells/${shellTypeIcons[tankFilters.shells[index]]}${premium ? '_premium' : ''}.webp`,
|
||||
`icons/shells/${shellTypeIcons[shells[index]]}${premium ? "_premium" : ""}.webp`
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
@@ -75,35 +70,33 @@ function ShellFilter({ index, premium }: { index: number; premium?: boolean }) {
|
||||
<Inset>
|
||||
<Flex>
|
||||
{Object.values(ShellType).map((shellType) => {
|
||||
if (typeof shellType === 'string') return null;
|
||||
if (typeof shellType === "string") return null;
|
||||
|
||||
const selected = tankFilters.shells[index] === shellType;
|
||||
const selected = shells[index] === shellType;
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
key={shellType}
|
||||
value={`${shellType}`}
|
||||
radius="none"
|
||||
variant={selected ? 'solid' : 'soft'}
|
||||
variant={selected ? "solid" : "soft"}
|
||||
onClick={() => {
|
||||
const mutated = [
|
||||
tankFilters.shells[0],
|
||||
tankFilters.shells[1],
|
||||
tankFilters.shells[2],
|
||||
] as TankFilters['shells'];
|
||||
const mutated = [...shells] as TankFilters["shells"];
|
||||
|
||||
mutated[index] = selected ? null : shellType;
|
||||
|
||||
$tankFilters.setKey('shells', mutated);
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.shells = mutated;
|
||||
});
|
||||
}}
|
||||
highContrast={selected}
|
||||
color={selected ? undefined : 'gray'}
|
||||
color={selected ? undefined : "gray"}
|
||||
>
|
||||
<img
|
||||
src={asset(
|
||||
`icons/shells/${shellTypeIcons[shellType]}${premium ? '_premium' : ''}.webp`,
|
||||
`icons/shells/${shellTypeIcons[shellType]}${premium ? "_premium" : ""}.webp`
|
||||
)}
|
||||
style={{ width: '1em', height: '1em' }}
|
||||
style={{ width: "1em", height: "1em" }}
|
||||
/>
|
||||
</IconButton>
|
||||
);
|
||||
@@ -116,26 +109,26 @@ function ShellFilter({ index, premium }: { index: number; premium?: boolean }) {
|
||||
}
|
||||
|
||||
export function FilterControl({ compact }: FilterControlProps) {
|
||||
const tankFilters = useStore($tankFilters);
|
||||
const tankFilters = TankFilters.use();
|
||||
const { strings } = useLocale();
|
||||
const clearable = !isEqual(tankFilters, initialTankFilters);
|
||||
const clearable = !isEqual(tankFilters, TankFilters.initial);
|
||||
|
||||
return (
|
||||
<Flex direction="column" align="center">
|
||||
<Flex
|
||||
height="fit-content"
|
||||
gap="2"
|
||||
align={{ initial: 'start', md: 'center' }}
|
||||
align={{ initial: "start", md: "center" }}
|
||||
justify="center"
|
||||
>
|
||||
<Flex
|
||||
flexShrink="0"
|
||||
direction={compact ? 'row' : { initial: 'row', md: 'column' }}
|
||||
direction={compact ? "row" : { initial: "row", md: "column" }}
|
||||
overflow="hidden"
|
||||
style={{ borderRadius: 'var(--radius-5)' }}
|
||||
style={{ borderRadius: "var(--radius-5)" }}
|
||||
>
|
||||
<Flex
|
||||
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
|
||||
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||
>
|
||||
{times(5, (index) => {
|
||||
const tier = index + 1;
|
||||
@@ -144,21 +137,19 @@ export function FilterControl({ compact }: FilterControlProps) {
|
||||
return (
|
||||
<IconButton
|
||||
key={tier}
|
||||
variant={selected ? 'solid' : 'soft'}
|
||||
variant={selected ? "solid" : "soft"}
|
||||
radius="none"
|
||||
color={selected ? undefined : 'gray'}
|
||||
color={selected ? undefined : "gray"}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
if (tankFilters.tiers.includes(tier)) {
|
||||
$tankFilters.setKey(
|
||||
'tiers',
|
||||
tankFilters.tiers.filter((t) => t !== tier),
|
||||
);
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.tiers = draft.tiers.filter((t) => t !== tier);
|
||||
});
|
||||
} else {
|
||||
$tankFilters.setKey('tiers', [
|
||||
...tankFilters.tiers,
|
||||
tier,
|
||||
]);
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.tiers = [...draft.tiers, tier];
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -169,15 +160,15 @@ export function FilterControl({ compact }: FilterControlProps) {
|
||||
</Flex>
|
||||
|
||||
<Box
|
||||
display={{ initial: 'none', md: 'block' }}
|
||||
display={{ initial: "none", md: "block" }}
|
||||
style={{
|
||||
height: Var('space-1'),
|
||||
backgroundColor: Var('gray-a3'),
|
||||
height: Var("space-1"),
|
||||
backgroundColor: Var("gray-a3"),
|
||||
}}
|
||||
/>
|
||||
|
||||
<Flex
|
||||
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
|
||||
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||
>
|
||||
{times(5, (index) => {
|
||||
const tier = index + 6;
|
||||
@@ -186,21 +177,19 @@ export function FilterControl({ compact }: FilterControlProps) {
|
||||
return (
|
||||
<IconButton
|
||||
key={tier}
|
||||
variant={selected ? 'solid' : 'soft'}
|
||||
variant={selected ? "solid" : "soft"}
|
||||
radius="none"
|
||||
color={selected ? undefined : 'gray'}
|
||||
color={selected ? undefined : "gray"}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
if (tankFilters.tiers.includes(tier)) {
|
||||
$tankFilters.setKey(
|
||||
'tiers',
|
||||
tankFilters.tiers.filter((t) => t !== tier),
|
||||
);
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.tiers = draft.tiers.filter((t) => t !== tier);
|
||||
});
|
||||
} else {
|
||||
$tankFilters.setKey('tiers', [
|
||||
...tankFilters.tiers,
|
||||
tier,
|
||||
]);
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.tiers = [...draft.tiers, tier];
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -213,12 +202,12 @@ export function FilterControl({ compact }: FilterControlProps) {
|
||||
|
||||
<Flex
|
||||
flexShrink="0"
|
||||
direction={compact ? 'row' : { initial: 'row', md: 'column' }}
|
||||
direction={compact ? "row" : { initial: "row", md: "column" }}
|
||||
overflow="hidden"
|
||||
style={{ borderRadius: 'var(--radius-5)' }}
|
||||
style={{ borderRadius: "var(--radius-5)" }}
|
||||
>
|
||||
<Flex
|
||||
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
|
||||
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||
>
|
||||
{gameDefinitions.nations
|
||||
.slice(0, Math.ceil(gameDefinitions.nations.length / 2))
|
||||
@@ -228,26 +217,24 @@ export function FilterControl({ compact }: FilterControlProps) {
|
||||
return (
|
||||
<IconButton
|
||||
key={nation}
|
||||
variant={selected ? 'solid' : 'soft'}
|
||||
color={selected ? undefined : 'gray'}
|
||||
variant={selected ? "solid" : "soft"}
|
||||
color={selected ? undefined : "gray"}
|
||||
highContrast
|
||||
radius="none"
|
||||
onClick={() => {
|
||||
if (tankFilters.nations.includes(nation)) {
|
||||
$tankFilters.setKey(
|
||||
'nations',
|
||||
tankFilters.nations.filter((n) => n !== nation),
|
||||
);
|
||||
} else {
|
||||
$tankFilters.setKey('nations', [
|
||||
...tankFilters.nations,
|
||||
nation,
|
||||
]);
|
||||
}
|
||||
TankFilters.mutate((draft) => {
|
||||
if (tankFilters.nations.includes(nation)) {
|
||||
draft.nations = draft.nations.filter(
|
||||
(n) => n !== nation
|
||||
);
|
||||
} else {
|
||||
draft.nations = [...draft.nations, nation];
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<img
|
||||
style={{ width: '1em', height: '1em' }}
|
||||
style={{ width: "1em", height: "1em" }}
|
||||
src={asset(`flags/circle/${nation}.webp`)}
|
||||
/>
|
||||
</IconButton>
|
||||
@@ -256,15 +243,15 @@ export function FilterControl({ compact }: FilterControlProps) {
|
||||
</Flex>
|
||||
|
||||
<Box
|
||||
display={{ initial: 'none', md: 'block' }}
|
||||
display={{ initial: "none", md: "block" }}
|
||||
style={{
|
||||
height: Var('space-1'),
|
||||
backgroundColor: Var('gray-a3'),
|
||||
height: Var("space-1"),
|
||||
backgroundColor: Var("gray-a3"),
|
||||
}}
|
||||
/>
|
||||
|
||||
<Flex
|
||||
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
|
||||
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||
>
|
||||
{gameDefinitions.nations
|
||||
.slice(Math.ceil(gameDefinitions.nations.length / 2))
|
||||
@@ -275,26 +262,24 @@ export function FilterControl({ compact }: FilterControlProps) {
|
||||
<IconButton
|
||||
key={nation}
|
||||
style={{ flex: 1 }}
|
||||
variant={selected ? 'solid' : 'soft'}
|
||||
color={selected ? undefined : 'gray'}
|
||||
variant={selected ? "solid" : "soft"}
|
||||
color={selected ? undefined : "gray"}
|
||||
highContrast
|
||||
radius="none"
|
||||
onClick={() => {
|
||||
if (tankFilters.nations.includes(nation)) {
|
||||
$tankFilters.setKey(
|
||||
'nations',
|
||||
tankFilters.nations.filter((n) => n !== nation),
|
||||
);
|
||||
} else {
|
||||
$tankFilters.setKey('nations', [
|
||||
...tankFilters.nations,
|
||||
nation,
|
||||
]);
|
||||
}
|
||||
TankFilters.mutate((draft) => {
|
||||
if (tankFilters.nations.includes(nation)) {
|
||||
draft.nations = draft.nations.filter(
|
||||
(n) => n !== nation
|
||||
);
|
||||
} else {
|
||||
draft.nations = [...draft.nations, nation];
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<img
|
||||
style={{ width: '1em', height: '1em' }}
|
||||
style={{ width: "1em", height: "1em" }}
|
||||
src={asset(`flags/circle/${nation}.webp`)}
|
||||
/>
|
||||
</IconButton>
|
||||
@@ -304,15 +289,15 @@ export function FilterControl({ compact }: FilterControlProps) {
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
direction={compact ? 'row' : { initial: 'row', md: 'column' }}
|
||||
direction={compact ? "row" : { initial: "row", md: "column" }}
|
||||
align="start"
|
||||
gap={{ initial: '2', md: '1' }}
|
||||
gap={{ initial: "2", md: "1" }}
|
||||
>
|
||||
<Flex gap="2" align="start">
|
||||
<Flex
|
||||
overflow="hidden"
|
||||
style={{ borderRadius: 'var(--radius-full)' }}
|
||||
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
|
||||
style={{ borderRadius: "var(--radius-full)" }}
|
||||
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||
>
|
||||
{TANK_CLASSES.map((tankClass) => {
|
||||
const Icon = classIcons[tankClass];
|
||||
@@ -321,25 +306,23 @@ export function FilterControl({ compact }: FilterControlProps) {
|
||||
return (
|
||||
<IconButton
|
||||
key={tankClass}
|
||||
variant={selected ? 'solid' : 'soft'}
|
||||
variant={selected ? "solid" : "soft"}
|
||||
radius="none"
|
||||
color={selected ? undefined : 'gray'}
|
||||
color={selected ? undefined : "gray"}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
if (tankFilters.classes.includes(tankClass)) {
|
||||
$tankFilters.setKey(
|
||||
'classes',
|
||||
tankFilters.classes.filter((c) => c !== tankClass),
|
||||
);
|
||||
} else {
|
||||
$tankFilters.setKey('classes', [
|
||||
...tankFilters.classes,
|
||||
tankClass,
|
||||
]);
|
||||
}
|
||||
TankFilters.mutate((draft) => {
|
||||
if (tankFilters.classes.includes(tankClass)) {
|
||||
draft.classes = draft.classes.filter(
|
||||
(c) => c !== tankClass
|
||||
);
|
||||
} else {
|
||||
draft.classes = [...draft.classes, tankClass];
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Icon style={{ width: '1em', height: '1em' }} />
|
||||
<Icon style={{ width: "1em", height: "1em" }} />
|
||||
</IconButton>
|
||||
);
|
||||
})}
|
||||
@@ -347,96 +330,90 @@ export function FilterControl({ compact }: FilterControlProps) {
|
||||
|
||||
<Flex
|
||||
overflow="hidden"
|
||||
style={{ borderRadius: 'var(--radius-full)' }}
|
||||
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
|
||||
style={{ borderRadius: "var(--radius-full)" }}
|
||||
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||
>
|
||||
<IconButton
|
||||
variant={
|
||||
tankFilters.gunType.includes('regular') ? 'solid' : 'soft'
|
||||
tankFilters.gunType.includes("regular") ? "solid" : "soft"
|
||||
}
|
||||
radius="none"
|
||||
color={
|
||||
tankFilters.gunType.includes('regular') ? undefined : 'gray'
|
||||
tankFilters.gunType.includes("regular") ? undefined : "gray"
|
||||
}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
if (tankFilters.gunType.includes('regular')) {
|
||||
$tankFilters.setKey(
|
||||
'gunType',
|
||||
tankFilters.gunType.filter((t) => t !== 'regular'),
|
||||
);
|
||||
} else {
|
||||
$tankFilters.setKey('gunType', [
|
||||
...tankFilters.gunType,
|
||||
'regular',
|
||||
]);
|
||||
}
|
||||
TankFilters.mutate((draft) => {
|
||||
if (tankFilters.gunType.includes("regular")) {
|
||||
draft.gunType = draft.gunType.filter(
|
||||
(t) => t !== "regular"
|
||||
);
|
||||
} else {
|
||||
draft.gunType = [...draft.gunType, "regular"];
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<GunRegularIcon style={{ width: '1em', height: '1em' }} />
|
||||
<GunRegularIcon style={{ width: "1em", height: "1em" }} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant={
|
||||
tankFilters.gunType.includes('auto_loader') ? 'solid' : 'soft'
|
||||
tankFilters.gunType.includes("auto_loader") ? "solid" : "soft"
|
||||
}
|
||||
radius="none"
|
||||
color={
|
||||
tankFilters.gunType.includes('auto_loader')
|
||||
tankFilters.gunType.includes("auto_loader")
|
||||
? undefined
|
||||
: 'gray'
|
||||
: "gray"
|
||||
}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
if (tankFilters.gunType.includes('auto_loader')) {
|
||||
$tankFilters.setKey(
|
||||
'gunType',
|
||||
tankFilters.gunType.filter((t) => t !== 'auto_loader'),
|
||||
);
|
||||
} else {
|
||||
$tankFilters.setKey('gunType', [
|
||||
...tankFilters.gunType,
|
||||
'auto_loader',
|
||||
]);
|
||||
}
|
||||
TankFilters.mutate((draft) => {
|
||||
if (tankFilters.gunType.includes("auto_loader")) {
|
||||
draft.gunType = draft.gunType.filter(
|
||||
(t) => t !== "auto_loader"
|
||||
);
|
||||
} else {
|
||||
draft.gunType = [...draft.gunType, "auto_loader"];
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<GunAutoloaderIcon style={{ width: '1em', height: '1em' }} />
|
||||
<GunAutoloaderIcon style={{ width: "1em", height: "1em" }} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant={
|
||||
tankFilters.gunType.includes('auto_reloader')
|
||||
? 'solid'
|
||||
: 'soft'
|
||||
tankFilters.gunType.includes("auto_reloader")
|
||||
? "solid"
|
||||
: "soft"
|
||||
}
|
||||
radius="none"
|
||||
color={
|
||||
tankFilters.gunType.includes('auto_reloader')
|
||||
tankFilters.gunType.includes("auto_reloader")
|
||||
? undefined
|
||||
: 'gray'
|
||||
: "gray"
|
||||
}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
if (tankFilters.gunType.includes('auto_reloader')) {
|
||||
$tankFilters.setKey(
|
||||
'gunType',
|
||||
tankFilters.gunType.filter((t) => t !== 'auto_reloader'),
|
||||
);
|
||||
} else {
|
||||
$tankFilters.setKey('gunType', [
|
||||
...tankFilters.gunType,
|
||||
'auto_reloader',
|
||||
]);
|
||||
}
|
||||
TankFilters.mutate((draft) => {
|
||||
if (tankFilters.gunType.includes("auto_reloader")) {
|
||||
draft.gunType = draft.gunType.filter(
|
||||
(t) => t !== "auto_reloader"
|
||||
);
|
||||
} else {
|
||||
draft.gunType = [...draft.gunType, "auto_reloader"];
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<GunAutoreloaderIcon style={{ width: '1em', height: '1em' }} />
|
||||
<GunAutoreloaderIcon style={{ width: "1em", height: "1em" }} />
|
||||
</IconButton>
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
overflow="hidden"
|
||||
style={{ borderRadius: 'var(--radius-full)' }}
|
||||
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
|
||||
style={{ borderRadius: "var(--radius-full)" }}
|
||||
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||
>
|
||||
<ShellFilter index={0} />
|
||||
<ShellFilter index={1} premium />
|
||||
@@ -447,160 +424,158 @@ export function FilterControl({ compact }: FilterControlProps) {
|
||||
<Flex gap="2" align="start">
|
||||
<Flex
|
||||
overflow="hidden"
|
||||
style={{ borderRadius: 'var(--radius-full)' }}
|
||||
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
|
||||
style={{ borderRadius: "var(--radius-full)" }}
|
||||
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||
>
|
||||
<IconButton
|
||||
variant={
|
||||
tankFilters.types?.includes(TankType.RESEARCHABLE)
|
||||
? 'solid'
|
||||
: 'soft'
|
||||
? "solid"
|
||||
: "soft"
|
||||
}
|
||||
radius="none"
|
||||
color={
|
||||
tankFilters.types?.includes(TankType.RESEARCHABLE)
|
||||
? undefined
|
||||
: 'gray'
|
||||
: "gray"
|
||||
}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
if (tankFilters.types.includes(TankType.RESEARCHABLE)) {
|
||||
$tankFilters.setKey(
|
||||
'types',
|
||||
tankFilters.types.filter(
|
||||
(t) => t !== TankType.RESEARCHABLE,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
$tankFilters.setKey('types', [
|
||||
...tankFilters.types,
|
||||
TankType.RESEARCHABLE,
|
||||
]);
|
||||
}
|
||||
TankFilters.mutate((draft) => {
|
||||
if (tankFilters.types.includes(TankType.RESEARCHABLE)) {
|
||||
draft.types = draft.types.filter(
|
||||
(t) => t !== TankType.RESEARCHABLE
|
||||
);
|
||||
} else {
|
||||
draft.types = [...draft.types, TankType.RESEARCHABLE];
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ResearchedIcon style={{ width: '1em', height: '1em' }} />
|
||||
<ResearchedIcon style={{ width: "1em", height: "1em" }} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant={
|
||||
tankFilters.types?.includes(TankType.PREMIUM)
|
||||
? 'solid'
|
||||
: 'soft'
|
||||
? "solid"
|
||||
: "soft"
|
||||
}
|
||||
radius="none"
|
||||
color={
|
||||
tankFilters.types?.includes(TankType.PREMIUM)
|
||||
? 'amber'
|
||||
: 'gray'
|
||||
? "amber"
|
||||
: "gray"
|
||||
}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
if (tankFilters.types.includes(TankType.PREMIUM)) {
|
||||
$tankFilters.setKey(
|
||||
'types',
|
||||
tankFilters.types.filter((t) => t !== TankType.PREMIUM),
|
||||
);
|
||||
} else {
|
||||
$tankFilters.setKey('types', [
|
||||
...tankFilters.types,
|
||||
TankType.PREMIUM,
|
||||
]);
|
||||
}
|
||||
TankFilters.mutate((draft) => {
|
||||
if (tankFilters.types.includes(TankType.PREMIUM)) {
|
||||
draft.types = draft.types.filter(
|
||||
(t) => t !== TankType.PREMIUM
|
||||
);
|
||||
} else {
|
||||
draft.types = [...draft.types, TankType.PREMIUM];
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
color={
|
||||
tankFilters.types?.includes(TankType.PREMIUM)
|
||||
? undefined
|
||||
: 'amber'
|
||||
: "amber"
|
||||
}
|
||||
style={{ display: 'flex', justifyContent: 'center' }}
|
||||
style={{ display: "flex", justifyContent: "center" }}
|
||||
>
|
||||
<ResearchedIcon style={{ width: '1em', height: '1em' }} />
|
||||
<ResearchedIcon style={{ width: "1em", height: "1em" }} />
|
||||
</Text>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant={
|
||||
tankFilters.types?.includes(TankType.COLLECTOR)
|
||||
? 'solid'
|
||||
: 'soft'
|
||||
? "solid"
|
||||
: "soft"
|
||||
}
|
||||
radius="none"
|
||||
color={
|
||||
tankFilters.types?.includes(TankType.COLLECTOR)
|
||||
? 'blue'
|
||||
: 'gray'
|
||||
? "blue"
|
||||
: "gray"
|
||||
}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
if (tankFilters.types.includes(TankType.COLLECTOR)) {
|
||||
$tankFilters.setKey(
|
||||
'types',
|
||||
tankFilters.types.filter((t) => t !== TankType.COLLECTOR),
|
||||
);
|
||||
} else {
|
||||
$tankFilters.setKey('types', [
|
||||
...tankFilters.types,
|
||||
TankType.COLLECTOR,
|
||||
]);
|
||||
}
|
||||
TankFilters.mutate((draft) => {
|
||||
if (tankFilters.types.includes(TankType.COLLECTOR)) {
|
||||
draft.types = draft.types.filter(
|
||||
(t) => t !== TankType.COLLECTOR
|
||||
);
|
||||
} else {
|
||||
draft.types = [...draft.types, TankType.COLLECTOR];
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
color={
|
||||
tankFilters.types?.includes(TankType.COLLECTOR)
|
||||
? undefined
|
||||
: 'blue'
|
||||
: "blue"
|
||||
}
|
||||
style={{ display: 'flex', justifyContent: 'center' }}
|
||||
style={{ display: "flex", justifyContent: "center" }}
|
||||
>
|
||||
<ResearchedIcon style={{ width: '1em', height: '1em' }} />
|
||||
<ResearchedIcon style={{ width: "1em", height: "1em" }} />
|
||||
</Text>
|
||||
</IconButton>
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
overflow="hidden"
|
||||
style={{ borderRadius: 'var(--radius-full)' }}
|
||||
direction={compact ? 'column' : { md: 'row', initial: 'column' }}
|
||||
style={{ borderRadius: "var(--radius-full)" }}
|
||||
direction={compact ? "column" : { md: "row", initial: "column" }}
|
||||
>
|
||||
<IconButton
|
||||
variant={tankFilters.testing === 'include' ? 'solid' : 'soft'}
|
||||
variant={tankFilters.testing === "include" ? "solid" : "soft"}
|
||||
radius="none"
|
||||
color={tankFilters.testing === 'include' ? undefined : 'gray'}
|
||||
color={tankFilters.testing === "include" ? undefined : "gray"}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
$tankFilters.setKey('testing', 'include');
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.testing = "include";
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ExperimentIcon
|
||||
style={{ width: '1em', height: '1em', color: 'currentColor' }}
|
||||
style={{ width: "1em", height: "1em", color: "currentColor" }}
|
||||
/>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant={tankFilters.testing === 'exclude' ? 'solid' : 'soft'}
|
||||
variant={tankFilters.testing === "exclude" ? "solid" : "soft"}
|
||||
radius="none"
|
||||
color={tankFilters.testing === 'exclude' ? undefined : 'gray'}
|
||||
color={tankFilters.testing === "exclude" ? undefined : "gray"}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
$tankFilters.setKey('testing', 'exclude');
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.testing = "exclude";
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ScienceOffIcon
|
||||
style={{ width: '1em', height: '1em', color: 'currentColor' }}
|
||||
style={{ width: "1em", height: "1em", color: "currentColor" }}
|
||||
/>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant={tankFilters.testing === 'only' ? 'solid' : 'soft'}
|
||||
variant={tankFilters.testing === "only" ? "solid" : "soft"}
|
||||
radius="none"
|
||||
color={tankFilters.testing === 'only' ? undefined : 'gray'}
|
||||
color={tankFilters.testing === "only" ? undefined : "gray"}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
$tankFilters.setKey('testing', 'only');
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.testing = "only";
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ScienceIcon
|
||||
style={{ width: '1em', height: '1em', color: 'currentColor' }}
|
||||
style={{ width: "1em", height: "1em", color: "currentColor" }}
|
||||
/>
|
||||
</IconButton>
|
||||
</Flex>
|
||||
@@ -616,7 +591,7 @@ export function FilterControl({ compact }: FilterControlProps) {
|
||||
href="#"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
$tankFilters.set(initialTankFilters);
|
||||
TankFilters.set(TankFilters.initial);
|
||||
TankSort.set(TankSort.initial);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import { Flex, Link, Text } from '@radix-ui/themes';
|
||||
import { useLocale } from '../../../hooks/useLocale';
|
||||
import { $tankFilters, initialTankFilters } from '../../../stores/tankFilters';
|
||||
import { Flex, Link, Text } from "@radix-ui/themes";
|
||||
import { useLocale } from "../../../hooks/useLocale";
|
||||
import { TankFilters } from "../../../stores/tankFilters";
|
||||
|
||||
interface NoResultsProps {
|
||||
type?: 'filters' | 'search';
|
||||
type?: "filters" | "search";
|
||||
}
|
||||
|
||||
export function NoResults({ type = 'filters' }: NoResultsProps) {
|
||||
export function NoResults({ type = "filters" }: NoResultsProps) {
|
||||
const { strings } = useLocale();
|
||||
|
||||
return (
|
||||
<Flex flexGrow="1" align="center" justify="center">
|
||||
<Text color="gray">
|
||||
{strings.website.common.tank_search.no_tanks_found.body}{' '}
|
||||
{strings.website.common.tank_search.no_tanks_found.body}{" "}
|
||||
<Link
|
||||
href="#"
|
||||
underline="always"
|
||||
color="red"
|
||||
onClick={() => {
|
||||
$tankFilters.set(initialTankFilters);
|
||||
TankFilters.set(TankFilters.initial);
|
||||
}}
|
||||
>
|
||||
{type === 'filters'
|
||||
{type === "filters"
|
||||
? strings.website.common.tank_search.no_tanks_found.clear_filters
|
||||
: strings.website.common.tank_search.no_tanks_found.search_again}
|
||||
</Link>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { type TankDefinition } from '@blitzkit/core';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { CaretRightIcon } from '@radix-ui/react-icons';
|
||||
import { Button, TextField } from '@radix-ui/themes';
|
||||
import { useLocale } from '../../../hooks/useLocale';
|
||||
import { $tankFilters } from '../../../stores/tankFilters';
|
||||
import { LinkI18n } from '../../LinkI18n';
|
||||
import { type TankDefinition } from "@blitzkit/core";
|
||||
import { CaretRightIcon } from "@radix-ui/react-icons";
|
||||
import { Button, TextField } from "@radix-ui/themes";
|
||||
import { useLocale } from "../../../hooks/useLocale";
|
||||
import { TankFilters } from "../../../stores/tankFilters";
|
||||
import { LinkI18n } from "../../LinkI18n";
|
||||
|
||||
interface QuickLinkProps {
|
||||
topResult?: TankDefinition;
|
||||
}
|
||||
|
||||
export function QuickLink({ topResult }: QuickLinkProps) {
|
||||
const tankFilters = useStore($tankFilters);
|
||||
const { locale, unwrap } = useLocale();
|
||||
const search = TankFilters.use((state) => state.search);
|
||||
const searching = TankFilters.use((state) => state.searching);
|
||||
|
||||
if (!tankFilters.search || !topResult || tankFilters.searching) return null;
|
||||
if (!search || !topResult || searching) return null;
|
||||
|
||||
return (
|
||||
<TextField.Slot>
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { Flex, Separator, Text } from '@radix-ui/themes';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { useMemo } from 'react';
|
||||
import { awaitableTankDefinitions } from '../../../core/awaitables/tankDefinitions';
|
||||
import { useLocale } from '../../../hooks/useLocale';
|
||||
import { $tankFilters, initialTankFilters } from '../../../stores/tankFilters';
|
||||
import { TankopediaPersistent } from '../../../stores/tankopediaPersistent';
|
||||
import { TankSort } from '../../../stores/tankopediaSort';
|
||||
import { TankCard } from '../../TankCard';
|
||||
import { TankCardWrapper } from './TankCardWrapper';
|
||||
import { Flex, Separator, Text } from "@radix-ui/themes";
|
||||
import { isEqual } from "lodash-es";
|
||||
import { useMemo } from "react";
|
||||
import { awaitableTankDefinitions } from "../../../core/awaitables/tankDefinitions";
|
||||
import { useLocale } from "../../../hooks/useLocale";
|
||||
import { TankFilters } from "../../../stores/tankFilters";
|
||||
import { TankopediaPersistent } from "../../../stores/tankopediaPersistent";
|
||||
import { TankSort } from "../../../stores/tankopediaSort";
|
||||
import { TankCard } from "../../TankCard";
|
||||
import { TankCardWrapper } from "./TankCardWrapper";
|
||||
|
||||
const tankDefinitions = await awaitableTankDefinitions;
|
||||
|
||||
export function RecentlyViewed() {
|
||||
const tankopediaPersistentStore = TankopediaPersistent.useStore();
|
||||
const filters = useStore($tankFilters);
|
||||
const filters = TankFilters.use();
|
||||
// non-reactive because it is a little weird that it updates instantly even before the page loads
|
||||
const recentlyViewed = tankopediaPersistentStore
|
||||
.getState()
|
||||
@@ -24,15 +23,15 @@ export function RecentlyViewed() {
|
||||
Object.entries(filters).some(([key, value]) => {
|
||||
return !isEqual(
|
||||
value,
|
||||
initialTankFilters[key as keyof typeof initialTankFilters],
|
||||
TankFilters.initial[key as keyof typeof TankFilters.initial]
|
||||
);
|
||||
}),
|
||||
[filters],
|
||||
[filters]
|
||||
);
|
||||
const { strings } = useLocale();
|
||||
const by = TankSort.use((state) => state.by);
|
||||
|
||||
if (recentlyViewed.length === 0 || by !== 'meta.none' || hasFilters) {
|
||||
if (recentlyViewed.length === 0 || by !== "meta.none" || hasFilters) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import type { TankDefinition } from '@blitzkit/core';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { MagnifyingGlassIcon } from '@radix-ui/react-icons';
|
||||
import { Flex, Spinner, TextField } from '@radix-ui/themes';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { type KeyboardEventHandler, useCallback, useRef } from 'react';
|
||||
import { useLocale } from '../../../hooks/useLocale';
|
||||
import { $tankFilters } from '../../../stores/tankFilters';
|
||||
import type { MaybeSkeletonComponentProps } from '../../../types/maybeSkeletonComponentProps';
|
||||
import { QuickLink } from './QuickLink';
|
||||
import { Sort } from './Sort';
|
||||
import type { TankDefinition } from "@blitzkit/core";
|
||||
import { MagnifyingGlassIcon } from "@radix-ui/react-icons";
|
||||
import { Flex, Spinner, TextField } from "@radix-ui/themes";
|
||||
import { debounce } from "lodash-es";
|
||||
import { type KeyboardEventHandler, useCallback, useRef } from "react";
|
||||
import { useLocale } from "../../../hooks/useLocale";
|
||||
import { TankFilters } from "../../../stores/tankFilters";
|
||||
import type { MaybeSkeletonComponentProps } from "../../../types/maybeSkeletonComponentProps";
|
||||
import { QuickLink } from "./QuickLink";
|
||||
import { Sort } from "./Sort";
|
||||
|
||||
type SearchBarProps = MaybeSkeletonComponentProps & {
|
||||
topResult?: TankDefinition;
|
||||
@@ -17,30 +16,37 @@ type SearchBarProps = MaybeSkeletonComponentProps & {
|
||||
|
||||
export function SearchBar({ topResult, skeleton, onSelect }: SearchBarProps) {
|
||||
const { strings } = useLocale();
|
||||
const tankFilters = useStore($tankFilters);
|
||||
const search = TankFilters.use((state) => state.search);
|
||||
const searching = TankFilters.use((state) => state.searching);
|
||||
const input = useRef<HTMLInputElement>(null);
|
||||
const performSearch = useCallback(
|
||||
debounce(() => {
|
||||
$tankFilters.setKey('searching', false);
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.searching = false;
|
||||
});
|
||||
|
||||
if (!input.current) return;
|
||||
|
||||
const sanitized = input.current.value.trim();
|
||||
|
||||
$tankFilters.setKey('search', sanitized.length === 0 ? null : sanitized);
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.search = sanitized.length === 0 ? null : sanitized;
|
||||
});
|
||||
}, 500),
|
||||
[],
|
||||
[]
|
||||
);
|
||||
const handleChange = useCallback(() => {
|
||||
if (!tankFilters.searching) {
|
||||
$tankFilters.setKey('searching', true);
|
||||
if (!searching) {
|
||||
TankFilters.mutate((draft) => {
|
||||
draft.searching = true;
|
||||
});
|
||||
}
|
||||
|
||||
performSearch();
|
||||
}, [tankFilters]);
|
||||
}, [searching]);
|
||||
const handleKeyDown = useCallback<KeyboardEventHandler>(
|
||||
(event) => {
|
||||
if (event.key !== 'Enter' || !topResult || tankFilters.searching) return;
|
||||
if (event.key !== "Enter" || !topResult || searching) return;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
@@ -50,7 +56,7 @@ export function SearchBar({ topResult, skeleton, onSelect }: SearchBarProps) {
|
||||
window.location.href = `/tanks/${topResult.slug}`;
|
||||
}
|
||||
},
|
||||
[topResult],
|
||||
[topResult]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -59,7 +65,7 @@ export function SearchBar({ topResult, skeleton, onSelect }: SearchBarProps) {
|
||||
<TextField.Root
|
||||
variant="classic"
|
||||
disabled={skeleton}
|
||||
defaultValue={tankFilters.search ?? undefined}
|
||||
defaultValue={search ?? undefined}
|
||||
style={{ flex: 1 }}
|
||||
ref={input}
|
||||
placeholder={strings.website.common.tank_search.search_bar_hint}
|
||||
@@ -67,7 +73,7 @@ export function SearchBar({ topResult, skeleton, onSelect }: SearchBarProps) {
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<TextField.Slot>
|
||||
{tankFilters.searching ? <Spinner /> : <MagnifyingGlassIcon />}
|
||||
{searching ? <Spinner /> : <MagnifyingGlassIcon />}
|
||||
</TextField.Slot>
|
||||
|
||||
<QuickLink topResult={topResult} />
|
||||
|
||||
@@ -1,44 +1,43 @@
|
||||
import { TIER_ROMAN_NUMERALS } from '@blitzkit/core';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { Flex, IconButton, Text } from '@radix-ui/themes';
|
||||
import { times } from 'lodash-es';
|
||||
import { memo } from 'react';
|
||||
import { $tankFilters } from '../../../stores/tankFilters';
|
||||
import { TankSort } from '../../../stores/tankopediaSort';
|
||||
import { TIER_ROMAN_NUMERALS } from "@blitzkit/core";
|
||||
import { Flex, IconButton, Text } from "@radix-ui/themes";
|
||||
import { times } from "lodash-es";
|
||||
import { memo } from "react";
|
||||
import { TankFilters } from "../../../stores/tankFilters";
|
||||
import { TankSort } from "../../../stores/tankopediaSort";
|
||||
|
||||
export const TierFilter = memo(() => {
|
||||
const tankFilters = useStore($tankFilters);
|
||||
const by = TankSort.use((state) => state.by);
|
||||
const search = TankFilters.use((state) => state.search);
|
||||
const tiers = TankFilters.use((state) => state.tiers);
|
||||
|
||||
if (by !== 'meta.none' || tankFilters.search) return null;
|
||||
if (by !== "meta.none" || search) return null;
|
||||
|
||||
return (
|
||||
<Flex justify="center">
|
||||
<Flex
|
||||
direction="row"
|
||||
overflow="hidden"
|
||||
style={{ borderRadius: 'var(--radius-full)' }}
|
||||
style={{ borderRadius: "var(--radius-full)" }}
|
||||
>
|
||||
{times(10, (index) => {
|
||||
const tier = 10 - index;
|
||||
const selected = tankFilters.tiers.includes(tier);
|
||||
const selected = tiers.includes(tier);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
key={tier}
|
||||
variant={selected ? 'solid' : 'soft'}
|
||||
variant={selected ? "solid" : "soft"}
|
||||
radius="none"
|
||||
color={selected ? undefined : 'gray'}
|
||||
color={selected ? undefined : "gray"}
|
||||
highContrast
|
||||
onClick={() => {
|
||||
if (tankFilters.tiers.includes(tier)) {
|
||||
$tankFilters.setKey(
|
||||
'tiers',
|
||||
tankFilters.tiers.filter((t) => t !== tier),
|
||||
);
|
||||
} else {
|
||||
$tankFilters.setKey('tiers', [...tankFilters.tiers, tier]);
|
||||
}
|
||||
TankFilters.mutate((draft) => {
|
||||
if (tiers.includes(tier)) {
|
||||
draft.tiers = tiers.filter((t) => t !== tier);
|
||||
} else {
|
||||
draft.tiers = [...tiers, tier];
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Text size="2">{TIER_ROMAN_NUMERALS[tier]}</Text>
|
||||
|
||||
@@ -5,37 +5,36 @@ import {
|
||||
SEARCH_KEYS,
|
||||
unionBoundingBox,
|
||||
type TankDefinition,
|
||||
} from '@blitzkit/core';
|
||||
import { literals } from '@blitzkit/i18n/src/literals';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { Callout, Flex, Link, Text, type FlexProps } from '@radix-ui/themes';
|
||||
import fuzzysort from 'fuzzysort';
|
||||
import { times, uniq } from 'lodash-es';
|
||||
import { memo, useEffect, useMemo, useState } from 'react';
|
||||
import { awaitableGameDefinitions } from '../../core/awaitables/gameDefinitions';
|
||||
import { awaitableModelDefinitions } from '../../core/awaitables/modelDefinitions';
|
||||
import { awaitableTankDefinitions } from '../../core/awaitables/tankDefinitions';
|
||||
import { awaitableTankNames } from '../../core/awaitables/tankNames';
|
||||
import { filterTank } from '../../core/blitzkit/filterTank';
|
||||
import { resolveReload } from '../../core/blitzkit/resolveReload';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import { $tankFilters } from '../../stores/tankFilters';
|
||||
import { TankopediaPersistent } from '../../stores/tankopediaPersistent';
|
||||
import { SORT_UNITS } from '../../stores/tankopediaPersistent/constants';
|
||||
import { TankSort } from '../../stores/tankopediaSort';
|
||||
import type { MaybeSkeletonComponentProps } from '../../types/maybeSkeletonComponentProps';
|
||||
import { ExperimentIcon } from '../ExperimentIcon';
|
||||
import { TankSearchCard } from './components/Card';
|
||||
import { FilterControl } from './components/FilterControl';
|
||||
import { NoResults } from './components/NoResults';
|
||||
import { RecentlyViewed } from './components/RecentlyViewed';
|
||||
import { SearchBar } from './components/SearchBar';
|
||||
import { SkeletonTankCard } from './components/SkeletonTankCard';
|
||||
import { TankCardWrapper } from './components/TankCardWrapper';
|
||||
import { MAX_RECENTLY_VIEWED } from './constants';
|
||||
} from "@blitzkit/core";
|
||||
import { literals } from "@blitzkit/i18n/src/literals";
|
||||
import { Callout, Flex, Link, Text, type FlexProps } from "@radix-ui/themes";
|
||||
import fuzzysort from "fuzzysort";
|
||||
import { times, uniq } from "lodash-es";
|
||||
import { memo, useEffect, useMemo, useState } from "react";
|
||||
import { awaitableGameDefinitions } from "../../core/awaitables/gameDefinitions";
|
||||
import { awaitableModelDefinitions } from "../../core/awaitables/modelDefinitions";
|
||||
import { awaitableTankDefinitions } from "../../core/awaitables/tankDefinitions";
|
||||
import { awaitableTankNames } from "../../core/awaitables/tankNames";
|
||||
import { filterTank } from "../../core/blitzkit/filterTank";
|
||||
import { resolveReload } from "../../core/blitzkit/resolveReload";
|
||||
import { useLocale } from "../../hooks/useLocale";
|
||||
import { TankFilters } from "../../stores/tankFilters";
|
||||
import { TankopediaPersistent } from "../../stores/tankopediaPersistent";
|
||||
import { SORT_UNITS } from "../../stores/tankopediaPersistent/constants";
|
||||
import { TankSort } from "../../stores/tankopediaSort";
|
||||
import type { MaybeSkeletonComponentProps } from "../../types/maybeSkeletonComponentProps";
|
||||
import { ExperimentIcon } from "../ExperimentIcon";
|
||||
import { TankSearchCard } from "./components/Card";
|
||||
import { FilterControl } from "./components/FilterControl";
|
||||
import { NoResults } from "./components/NoResults";
|
||||
import { RecentlyViewed } from "./components/RecentlyViewed";
|
||||
import { SearchBar } from "./components/SearchBar";
|
||||
import { SkeletonTankCard } from "./components/SkeletonTankCard";
|
||||
import { TankCardWrapper } from "./components/TankCardWrapper";
|
||||
import { MAX_RECENTLY_VIEWED } from "./constants";
|
||||
|
||||
type TankSearchProps = MaybeSkeletonComponentProps &
|
||||
Omit<FlexProps, 'onSelect'> & {
|
||||
Omit<FlexProps, "onSelect"> & {
|
||||
compact?: boolean;
|
||||
onSelect?: (tank: TankDefinition) => void;
|
||||
onSelectAll?: (tanks: TankDefinition[]) => void;
|
||||
@@ -57,21 +56,21 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
const { strings, locale } = useLocale();
|
||||
const mutateTankopediaPersistent = TankopediaPersistent.useMutation();
|
||||
const awaitedTanksDefinitionsArray = Object.values(tankDefinitions.tanks);
|
||||
const tankFilters = useStore($tankFilters);
|
||||
const tankopediaSort = TankSort.use();
|
||||
const tankFilters = TankFilters.use();
|
||||
const tanksFiltered = useMemo(() => {
|
||||
if (tankFilters.search === null) {
|
||||
const filtered = awaitedTanksDefinitionsArray.filter((tank) =>
|
||||
filterTank(tankFilters, tank),
|
||||
filterTank(tankFilters, tank)
|
||||
);
|
||||
let sorted: TankDefinition[];
|
||||
|
||||
switch (tankopediaSort.by) {
|
||||
case 'meta.none':
|
||||
case "meta.none":
|
||||
sorted = metaSortTank(filtered, gameDefinitions);
|
||||
break;
|
||||
|
||||
case 'survivability.health': {
|
||||
case "survivability.health": {
|
||||
sorted = filtered.sort((a, b) => {
|
||||
const aHealth = a.health + a.turrets.at(-1)!.health;
|
||||
const bHealth = b.health + b.turrets.at(-1)!.health;
|
||||
@@ -81,36 +80,36 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
break;
|
||||
}
|
||||
|
||||
case 'survivability.viewRange':
|
||||
case "survivability.viewRange":
|
||||
sorted = filtered.sort(
|
||||
(a, b) =>
|
||||
a.turrets.at(-1)!.view_range - b.turrets.at(-1)!.view_range,
|
||||
a.turrets.at(-1)!.view_range - b.turrets.at(-1)!.view_range
|
||||
);
|
||||
break;
|
||||
|
||||
case 'survivability.camouflageStill':
|
||||
case "survivability.camouflageStill":
|
||||
sorted = filtered.sort(
|
||||
(a, b) => a.camouflage_still - b.camouflage_still,
|
||||
(a, b) => a.camouflage_still - b.camouflage_still
|
||||
);
|
||||
break;
|
||||
|
||||
case 'survivability.camouflageMoving':
|
||||
case "survivability.camouflageMoving":
|
||||
sorted = filtered.sort(
|
||||
(a, b) => a.camouflage_moving - b.camouflage_moving,
|
||||
(a, b) => a.camouflage_moving - b.camouflage_moving
|
||||
);
|
||||
break;
|
||||
|
||||
case 'survivability.camouflageShooting':
|
||||
case "survivability.camouflageShooting":
|
||||
sorted = filtered.sort(
|
||||
(a, b) =>
|
||||
a.camouflage_still *
|
||||
a.turrets.at(-1)!.guns.at(-1)!.camouflage_loss -
|
||||
b.camouflage_still *
|
||||
b.turrets.at(-1)!.guns.at(-1)!.camouflage_loss,
|
||||
b.turrets.at(-1)!.guns.at(-1)!.camouflage_loss
|
||||
);
|
||||
break;
|
||||
|
||||
case 'survivability.volume':
|
||||
case "survivability.volume":
|
||||
sorted = filtered.sort((a, b) => {
|
||||
const aTankModelDefinition = modelDefinitions.models[a.id];
|
||||
const bTankModelDefinition = modelDefinitions.models[b.id];
|
||||
@@ -121,14 +120,14 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
const aSize = normalizeBoundingBox(
|
||||
unionBoundingBox(
|
||||
aTankModelDefinition.bounding_box,
|
||||
aTurretModelDefinition.bounding_box,
|
||||
),
|
||||
aTurretModelDefinition.bounding_box
|
||||
)
|
||||
);
|
||||
const bSize = normalizeBoundingBox(
|
||||
unionBoundingBox(
|
||||
bTankModelDefinition.bounding_box,
|
||||
bTurretModelDefinition.bounding_box,
|
||||
),
|
||||
bTurretModelDefinition.bounding_box
|
||||
)
|
||||
);
|
||||
const aVolume = aSize.x * aSize.y * aSize.z;
|
||||
const bVolume = bSize.x * bSize.y * bSize.z;
|
||||
@@ -137,7 +136,7 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
});
|
||||
break;
|
||||
|
||||
case 'survivability.length':
|
||||
case "survivability.length":
|
||||
sorted = filtered.sort((a, b) => {
|
||||
const aTankModelDefinition = modelDefinitions.models[a.id];
|
||||
const bTankModelDefinition = modelDefinitions.models[b.id];
|
||||
@@ -148,14 +147,14 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
const aSize = normalizeBoundingBox(
|
||||
unionBoundingBox(
|
||||
aTankModelDefinition.bounding_box,
|
||||
aTurretModelDefinition.bounding_box,
|
||||
),
|
||||
aTurretModelDefinition.bounding_box
|
||||
)
|
||||
);
|
||||
const bSize = normalizeBoundingBox(
|
||||
unionBoundingBox(
|
||||
bTankModelDefinition.bounding_box,
|
||||
bTurretModelDefinition.bounding_box,
|
||||
),
|
||||
bTurretModelDefinition.bounding_box
|
||||
)
|
||||
);
|
||||
const aLength = Math.max(aSize.x, aSize.y, aSize.z);
|
||||
const bLength = Math.max(bSize.x, bSize.y, bSize.z);
|
||||
@@ -164,147 +163,147 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
});
|
||||
break;
|
||||
|
||||
case 'fire.dpm':
|
||||
case "fire.dpm":
|
||||
sorted = filtered.sort(
|
||||
(a, b) =>
|
||||
resolveDpm(
|
||||
a.turrets.at(-1)!.guns.at(-1)!,
|
||||
a.turrets.at(-1)!.guns.at(-1)!.shells[0],
|
||||
a.turrets.at(-1)!.guns.at(-1)!.shells[0]
|
||||
) -
|
||||
resolveDpm(
|
||||
b.turrets.at(-1)!.guns.at(-1)!,
|
||||
b.turrets.at(-1)!.guns.at(-1)!.shells[0],
|
||||
),
|
||||
b.turrets.at(-1)!.guns.at(-1)!.shells[0]
|
||||
)
|
||||
);
|
||||
break;
|
||||
|
||||
case 'fire.dpmPremium':
|
||||
case "fire.dpmPremium":
|
||||
sorted = filtered.sort(
|
||||
(a, b) =>
|
||||
(a.turrets.at(-1)!.guns.at(-1)!.shells[1]
|
||||
? resolveDpm(
|
||||
a.turrets.at(-1)!.guns.at(-1)!,
|
||||
a.turrets.at(-1)!.guns.at(-1)!.shells[1],
|
||||
a.turrets.at(-1)!.guns.at(-1)!.shells[1]
|
||||
)
|
||||
: 0) -
|
||||
(b.turrets.at(-1)!.guns.at(-1)!.shells[1]
|
||||
? resolveDpm(
|
||||
b.turrets.at(-1)!.guns.at(-1)!,
|
||||
b.turrets.at(-1)!.guns.at(-1)!.shells[1],
|
||||
b.turrets.at(-1)!.guns.at(-1)!.shells[1]
|
||||
)
|
||||
: 0),
|
||||
: 0)
|
||||
);
|
||||
break;
|
||||
|
||||
case 'fire.reload':
|
||||
case "fire.reload":
|
||||
sorted = filtered.sort(
|
||||
(a, b) =>
|
||||
resolveReload(a.turrets.at(-1)!.guns.at(-1)!) -
|
||||
resolveReload(b.turrets.at(-1)!.guns.at(-1)!),
|
||||
resolveReload(b.turrets.at(-1)!.guns.at(-1)!)
|
||||
);
|
||||
break;
|
||||
|
||||
case 'fire.caliber':
|
||||
case "fire.caliber":
|
||||
sorted = filtered.sort(
|
||||
(a, b) =>
|
||||
a.turrets.at(-1)!.guns.at(-1)!.shells[0].caliber -
|
||||
b.turrets.at(-1)!.guns.at(-1)!.shells[0].caliber,
|
||||
b.turrets.at(-1)!.guns.at(-1)!.shells[0].caliber
|
||||
);
|
||||
break;
|
||||
|
||||
case 'fire.standardPenetration':
|
||||
case "fire.standardPenetration":
|
||||
sorted = filtered.sort(
|
||||
(a, b) =>
|
||||
a.turrets.at(-1)!.guns.at(-1)!.shells[0].penetration.near -
|
||||
b.turrets.at(-1)!.guns.at(-1)!.shells[0].penetration.near,
|
||||
b.turrets.at(-1)!.guns.at(-1)!.shells[0].penetration.near
|
||||
);
|
||||
break;
|
||||
|
||||
case 'fire.premiumPenetration':
|
||||
case "fire.premiumPenetration":
|
||||
sorted = filtered.sort(
|
||||
(a, b) =>
|
||||
(a.turrets.at(-1)!.guns.at(-1)!.shells[1]?.penetration.near ??
|
||||
0) -
|
||||
(b.turrets.at(-1)!.guns.at(-1)!.shells[1]?.penetration.near ??
|
||||
0),
|
||||
0)
|
||||
);
|
||||
break;
|
||||
|
||||
case 'fire.tertiaryPenetration':
|
||||
case "fire.tertiaryPenetration":
|
||||
sorted = filtered.sort(
|
||||
(a, b) =>
|
||||
(a.turrets.at(-1)!.guns.at(-1)!.shells[2]?.penetration.near ??
|
||||
0) -
|
||||
(b.turrets.at(-1)!.guns.at(-1)!.shells[2]?.penetration.near ??
|
||||
0),
|
||||
0)
|
||||
);
|
||||
break;
|
||||
|
||||
case 'fire.damage':
|
||||
case "fire.damage":
|
||||
sorted = filtered.sort(
|
||||
(a, b) =>
|
||||
a.turrets.at(-1)!.guns.at(-1)!.shells[0].armor_damage -
|
||||
b.turrets.at(-1)!.guns.at(-1)!.shells[0].armor_damage,
|
||||
b.turrets.at(-1)!.guns.at(-1)!.shells[0].armor_damage
|
||||
);
|
||||
break;
|
||||
|
||||
case 'fire.moduleDamage':
|
||||
case "fire.moduleDamage":
|
||||
sorted = filtered.sort(
|
||||
(a, b) =>
|
||||
a.turrets.at(-1)!.guns.at(-1)!.shells[0].module_damage -
|
||||
b.turrets.at(-1)!.guns.at(-1)!.shells[0].module_damage,
|
||||
b.turrets.at(-1)!.guns.at(-1)!.shells[0].module_damage
|
||||
);
|
||||
break;
|
||||
|
||||
case 'fire.shellVelocity':
|
||||
case "fire.shellVelocity":
|
||||
sorted = filtered.sort(
|
||||
(a, b) =>
|
||||
a.turrets.at(-1)!.guns.at(-1)!.shells[0].velocity -
|
||||
b.turrets.at(-1)!.guns.at(-1)!.shells[0].velocity,
|
||||
b.turrets.at(-1)!.guns.at(-1)!.shells[0].velocity
|
||||
);
|
||||
break;
|
||||
|
||||
case 'fire.shellRange':
|
||||
case "fire.shellRange":
|
||||
sorted = filtered.sort(
|
||||
(a, b) =>
|
||||
a.turrets.at(-1)!.guns.at(-1)!.shells[0].range -
|
||||
b.turrets.at(-1)!.guns.at(-1)!.shells[0].range,
|
||||
b.turrets.at(-1)!.guns.at(-1)!.shells[0].range
|
||||
);
|
||||
break;
|
||||
|
||||
case 'fire.shellCapacity':
|
||||
case "fire.shellCapacity":
|
||||
sorted = filtered.sort(
|
||||
(a, b) =>
|
||||
a.turrets.at(-1)!.guns.at(-1)!.shell_capacity -
|
||||
b.turrets.at(-1)!.guns.at(-1)!.shell_capacity,
|
||||
b.turrets.at(-1)!.guns.at(-1)!.shell_capacity
|
||||
);
|
||||
break;
|
||||
|
||||
case 'fire.aimTime':
|
||||
case "fire.aimTime":
|
||||
sorted = filtered.sort(
|
||||
(a, b) =>
|
||||
a.turrets.at(-1)!.guns.at(-1)!.aim_time -
|
||||
b.turrets.at(-1)!.guns.at(-1)!.aim_time,
|
||||
b.turrets.at(-1)!.guns.at(-1)!.aim_time
|
||||
);
|
||||
break;
|
||||
|
||||
case 'fire.dispersionStill':
|
||||
case "fire.dispersionStill":
|
||||
sorted = filtered.sort(
|
||||
(a, b) =>
|
||||
a.turrets.at(-1)!.guns.at(-1)!.dispersion_base -
|
||||
b.turrets.at(-1)!.guns.at(-1)!.dispersion_base,
|
||||
b.turrets.at(-1)!.guns.at(-1)!.dispersion_base
|
||||
);
|
||||
break;
|
||||
|
||||
case 'fire.dispersionMoving':
|
||||
case "fire.dispersionMoving":
|
||||
sorted = filtered.sort(
|
||||
(a, b) =>
|
||||
a.tracks.at(-1)!.dispersion_move -
|
||||
b.tracks.at(-1)!.dispersion_move,
|
||||
b.tracks.at(-1)!.dispersion_move
|
||||
);
|
||||
break;
|
||||
|
||||
case 'fire.gunDepression':
|
||||
case "fire.gunDepression":
|
||||
sorted = filtered.sort(
|
||||
(a, b) =>
|
||||
modelDefinitions.models[a.id].turrets[a.turrets.at(-1)!.id]
|
||||
@@ -314,11 +313,11 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
modelDefinitions.models[b.id].turrets[b.turrets.at(-1)!.id]
|
||||
.guns[b.turrets.at(-1)!.guns.at(-1)!.id].pitch.max -
|
||||
(modelDefinitions.models[b.id].initial_turret_rotation?.pitch ??
|
||||
0),
|
||||
0)
|
||||
);
|
||||
break;
|
||||
|
||||
case 'fire.gunElevation':
|
||||
case "fire.gunElevation":
|
||||
sorted = filtered.sort(
|
||||
(a, b) =>
|
||||
modelDefinitions.models[b.id].turrets[b.turrets.at(-1)!.id]
|
||||
@@ -328,29 +327,29 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
modelDefinitions.models[a.id].turrets[a.turrets.at(-1)!.id]
|
||||
.guns[a.turrets.at(-1)!.guns.at(-1)!.id].pitch.min -
|
||||
(modelDefinitions.models[a.id].initial_turret_rotation?.pitch ??
|
||||
0),
|
||||
0)
|
||||
);
|
||||
break;
|
||||
|
||||
case 'maneuverability.forwardsSpeed':
|
||||
case "maneuverability.forwardsSpeed":
|
||||
sorted = filtered.sort(
|
||||
(a, b) => a.speed_forwards - b.speed_forwards,
|
||||
(a, b) => a.speed_forwards - b.speed_forwards
|
||||
);
|
||||
break;
|
||||
|
||||
case 'maneuverability.backwardsSpeed':
|
||||
case "maneuverability.backwardsSpeed":
|
||||
sorted = filtered.sort(
|
||||
(a, b) => a.speed_backwards - b.speed_backwards,
|
||||
(a, b) => a.speed_backwards - b.speed_backwards
|
||||
);
|
||||
break;
|
||||
|
||||
case 'maneuverability.power':
|
||||
case "maneuverability.power":
|
||||
sorted = filtered.sort(
|
||||
(a, b) => a.engines.at(-1)!.power - b.engines.at(-1)!.power,
|
||||
(a, b) => a.engines.at(-1)!.power - b.engines.at(-1)!.power
|
||||
);
|
||||
break;
|
||||
|
||||
case 'maneuverability.powerToWeight':
|
||||
case "maneuverability.powerToWeight":
|
||||
sorted = filtered.sort(
|
||||
(a, b) =>
|
||||
a.engines.at(-1)!.power /
|
||||
@@ -364,11 +363,11 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
b.engines.at(-1)!.weight +
|
||||
b.tracks.at(-1)!.weight +
|
||||
b.turrets.at(-1)!.weight +
|
||||
b.turrets.at(-1)!.guns.at(-1)!.weight),
|
||||
b.turrets.at(-1)!.guns.at(-1)!.weight)
|
||||
);
|
||||
break;
|
||||
|
||||
case 'maneuverability.weight':
|
||||
case "maneuverability.weight":
|
||||
sorted = filtered.sort(
|
||||
(a, b) =>
|
||||
a.weight +
|
||||
@@ -380,20 +379,20 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
b.engines.at(-1)!.weight +
|
||||
b.tracks.at(-1)!.weight +
|
||||
b.turrets.at(-1)!.weight +
|
||||
b.turrets.at(-1)!.guns.at(-1)!.weight),
|
||||
b.turrets.at(-1)!.guns.at(-1)!.weight)
|
||||
);
|
||||
break;
|
||||
|
||||
case 'maneuverability.traverseSpeed':
|
||||
case "maneuverability.traverseSpeed":
|
||||
sorted = filtered.sort(
|
||||
(a, b) =>
|
||||
a.tracks.at(-1)!.traverse_speed -
|
||||
b.tracks.at(-1)!.traverse_speed,
|
||||
b.tracks.at(-1)!.traverse_speed
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return tankopediaSort.direction === 'ascending'
|
||||
return tankopediaSort.direction === "ascending"
|
||||
? sorted
|
||||
: sorted.reverse();
|
||||
} else {
|
||||
@@ -401,7 +400,7 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
keys: SEARCH_KEYS,
|
||||
});
|
||||
const searchedTanks = searchedRaw.map(
|
||||
(result) => tankDefinitions.tanks[result.obj.id],
|
||||
(result) => tankDefinitions.tanks[result.obj.id]
|
||||
);
|
||||
return searchedTanks;
|
||||
}
|
||||
@@ -459,24 +458,24 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{tankopediaSort.by !== 'meta.none' && (
|
||||
{tankopediaSort.by !== "meta.none" && (
|
||||
<Text color="gray">
|
||||
{literals(strings.website.common.tank_search.sorting_by, [
|
||||
strings.website.common.tank_search.sort[tankopediaSort.by],
|
||||
])}
|
||||
{SORT_UNITS[tankopediaSort.by] === undefined
|
||||
? ''
|
||||
? ""
|
||||
: ` (${SORT_UNITS[tankopediaSort.by]})`}
|
||||
, {tankopediaSort.direction}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{tankFilters.testing === 'only' && (
|
||||
{tankFilters.testing === "only" && (
|
||||
<Flex justify="center" mt="4">
|
||||
<Callout.Root color="amber">
|
||||
<Callout.Icon>
|
||||
<ExperimentIcon style={{ width: '1em', height: '1em' }} />
|
||||
<ExperimentIcon style={{ width: "1em", height: "1em" }} />
|
||||
</Callout.Icon>
|
||||
<Callout.Text>
|
||||
{strings.website.common.warnings.test}
|
||||
@@ -505,12 +504,12 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
key={index}
|
||||
onIntersection={() => {
|
||||
setLoadedCards((state) =>
|
||||
Math.min(state + 2, tanksFiltered.length),
|
||||
Math.min(state + 2, tanksFiltered.length)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
}
|
||||
)}
|
||||
</TankCardWrapper>
|
||||
)}
|
||||
@@ -523,15 +522,15 @@ export const TankSearch = memo<TankSearchProps>(
|
||||
<TankCardWrapper>
|
||||
{times(
|
||||
Math.round(
|
||||
skeleton ? DEFAULT_LOADED_CARDS : 10 + 10 * Math.random(),
|
||||
skeleton ? DEFAULT_LOADED_CARDS : 10 + 10 * Math.random()
|
||||
),
|
||||
(index) => (
|
||||
<SkeletonTankCard key={index} />
|
||||
),
|
||||
)
|
||||
)}
|
||||
</TankCardWrapper>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -1,15 +1,14 @@
|
||||
import { metaSortTank } from '@blitzkit/core';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { times } from 'lodash-es';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { awaitableGameDefinitions } from '../../core/awaitables/gameDefinitions';
|
||||
import { awaitableTankDefinitions } from '../../core/awaitables/tankDefinitions';
|
||||
import { filterTank } from '../../core/blitzkit/filterTank';
|
||||
import { $tankFilters } from '../../stores/tankFilters';
|
||||
import { TierList } from '../../stores/tierList';
|
||||
import { SkeletonTankCard } from '../TankSearch/components/SkeletonTankCard';
|
||||
import { TankCardWrapper } from '../TankSearch/components/TankCardWrapper';
|
||||
import { TierListTile } from './Tile';
|
||||
import { metaSortTank } from "@blitzkit/core";
|
||||
import { times } from "lodash-es";
|
||||
import { useMemo, useState } from "react";
|
||||
import { awaitableGameDefinitions } from "../../core/awaitables/gameDefinitions";
|
||||
import { awaitableTankDefinitions } from "../../core/awaitables/tankDefinitions";
|
||||
import { filterTank } from "../../core/blitzkit/filterTank";
|
||||
import { TankFilters } from "../../stores/tankFilters";
|
||||
import { TierList } from "../../stores/tierList";
|
||||
import { SkeletonTankCard } from "../TankSearch/components/SkeletonTankCard";
|
||||
import { TankCardWrapper } from "../TankSearch/components/TankCardWrapper";
|
||||
import { TierListTile } from "./Tile";
|
||||
|
||||
const [tankDefinitions, gameDefinitions] = await Promise.all([
|
||||
awaitableTankDefinitions,
|
||||
@@ -22,17 +21,17 @@ const PREVIEW_COUNT = 32;
|
||||
const DEFAULT_LOADED_CARDS = 75;
|
||||
|
||||
export function TierListTiles() {
|
||||
const filters = useStore($tankFilters);
|
||||
const filters = TankFilters.use();
|
||||
const placedTanks = TierList.use((state) => state.placedTanks);
|
||||
const sorted = useMemo(
|
||||
() =>
|
||||
metaSortTank(
|
||||
tanks.filter(
|
||||
(tank) => filterTank(filters, tank) && !placedTanks.has(tank.id),
|
||||
(tank) => filterTank(filters, tank) && !placedTanks.has(tank.id)
|
||||
),
|
||||
gameDefinitions,
|
||||
gameDefinitions
|
||||
).reverse(),
|
||||
[filters, placedTanks],
|
||||
[filters, placedTanks]
|
||||
);
|
||||
const [loadedTiles, setLoadedTiles] = useState(DEFAULT_LOADED_CARDS);
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ import type {
|
||||
ShellType,
|
||||
TankClass,
|
||||
TankType,
|
||||
} from '@blitzkit/core';
|
||||
import { map } from 'nanostores';
|
||||
} from "@blitzkit/core";
|
||||
import { Varuna } from "varuna";
|
||||
|
||||
export type CaseType<T> = T extends {
|
||||
gun_type?: { $case: infer U; value: any };
|
||||
@@ -17,23 +17,21 @@ export interface TankFilters {
|
||||
nations: string[];
|
||||
classes: TankClass[];
|
||||
types: TankType[];
|
||||
testing: 'include' | 'exclude' | 'only';
|
||||
testing: "include" | "exclude" | "only";
|
||||
search: string | null;
|
||||
searching: boolean;
|
||||
gunType: CaseType<GunDefinition>[];
|
||||
shells: [ShellType | null, ShellType | null, ShellType | null];
|
||||
}
|
||||
|
||||
export const initialTankFilters: TankFilters = {
|
||||
export const TankFilters = new Varuna<TankFilters>({
|
||||
tiers: [],
|
||||
nations: [],
|
||||
classes: [],
|
||||
types: [],
|
||||
testing: 'include',
|
||||
testing: "include",
|
||||
search: null,
|
||||
searching: false,
|
||||
gunType: [],
|
||||
shells: [null, null, null],
|
||||
};
|
||||
|
||||
export const $tankFilters = map<TankFilters>(initialTankFilters);
|
||||
});
|
||||
Reference in new issue
Block a user