mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 19:49:50 +02:00
feat: search bar
This commit is contained in:
1 parent
c8e207b6a2
commit
8e438757fd
7 files changed
+450
-421
No files matched your search
@@ -0,0 +1,318 @@
|
|||||||
|
import { TrashIcon } from '@radix-ui/react-icons';
|
||||||
|
import { Box, Button, Flex, IconButton, Text } from '@radix-ui/themes';
|
||||||
|
import { times } from 'lodash';
|
||||||
|
import { use } from 'react';
|
||||||
|
import { classIcons } from '../../../../components/ClassIcon';
|
||||||
|
import { ExperimentIcon } from '../../../../components/ExperimentIcon';
|
||||||
|
import { NAVBAR_HEIGHT } from '../../../../components/Navbar';
|
||||||
|
import { ResearchedIcon } from '../../../../components/ResearchedIcon';
|
||||||
|
import { ScienceIcon } from '../../../../components/ScienceIcon';
|
||||||
|
import { ScienceOffIcon } from '../../../../components/ScienceOffIcon';
|
||||||
|
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';
|
||||||
|
|
||||||
|
export function FilterControl() {
|
||||||
|
const awaitedGameDefinitions = use(gameDefinitions);
|
||||||
|
const filters = useTankopediaFilters();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
direction={{ initial: 'row', sm: 'column' }}
|
||||||
|
height="fit-content"
|
||||||
|
gap="2"
|
||||||
|
align={{ initial: 'start', sm: 'start' }}
|
||||||
|
justify={{ initial: 'center', sm: 'start' }}
|
||||||
|
position={{ initial: 'static', sm: 'sticky' }}
|
||||||
|
style={{ top: `calc(${NAVBAR_HEIGHT}px + var(--space-3))` }}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
direction={{ initial: 'row', sm: 'column' }}
|
||||||
|
overflow="hidden"
|
||||||
|
style={{ borderRadius: 'var(--radius-4)' }}
|
||||||
|
>
|
||||||
|
<Flex direction={{ sm: 'row', initial: 'column' }}>
|
||||||
|
{times(5, (index) => {
|
||||||
|
const tier = (index + 1) as Tier;
|
||||||
|
const selected = filters.tier === tier;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
key={tier}
|
||||||
|
variant={selected ? 'solid' : 'soft'}
|
||||||
|
radius="none"
|
||||||
|
color={selected ? undefined : 'gray'}
|
||||||
|
highContrast
|
||||||
|
onClick={() =>
|
||||||
|
mutateTankopediaFilters((draft) => {
|
||||||
|
draft.tier = draft.tier === tier ? undefined : tier;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text size="2">{TIER_ROMAN_NUMERALS[tier]}</Text>
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Flex>
|
||||||
|
<Flex direction={{ sm: 'row', initial: 'column' }}>
|
||||||
|
{times(5, (index) => {
|
||||||
|
const tier = (index + 6) as Tier;
|
||||||
|
const selected = filters.tier === tier;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
key={tier}
|
||||||
|
variant={selected ? 'solid' : 'soft'}
|
||||||
|
radius="none"
|
||||||
|
color={selected ? undefined : 'gray'}
|
||||||
|
highContrast
|
||||||
|
onClick={() =>
|
||||||
|
mutateTankopediaFilters((draft) => {
|
||||||
|
draft.tier = draft.tier === tier ? undefined : tier;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text size="2">{TIER_ROMAN_NUMERALS[tier]}</Text>
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex
|
||||||
|
direction={{ initial: 'row', sm: 'column' }}
|
||||||
|
overflow="hidden"
|
||||||
|
style={{ borderRadius: 'var(--radius-4)' }}
|
||||||
|
>
|
||||||
|
<Flex direction={{ sm: 'row', initial: 'column' }}>
|
||||||
|
{awaitedGameDefinitions.nations
|
||||||
|
.slice(0, Math.ceil(awaitedGameDefinitions.nations.length / 2))
|
||||||
|
.map((nation) => {
|
||||||
|
const selected = filters.nation === nation;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
key={nation}
|
||||||
|
variant={selected ? 'solid' : 'soft'}
|
||||||
|
color={selected ? undefined : 'gray'}
|
||||||
|
highContrast
|
||||||
|
radius="none"
|
||||||
|
onClick={() =>
|
||||||
|
mutateTankopediaFilters((draft) => {
|
||||||
|
draft.nation =
|
||||||
|
draft.nation === nation ? undefined : nation;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
style={{ width: '1em', height: '1em' }}
|
||||||
|
src={asset(`flags/circle/${nation}.webp`)}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Flex>
|
||||||
|
<Flex direction={{ sm: 'row', initial: 'column' }}>
|
||||||
|
{awaitedGameDefinitions.nations
|
||||||
|
.slice(Math.ceil(awaitedGameDefinitions.nations.length / 2))
|
||||||
|
.map((nation) => {
|
||||||
|
const selected = filters.nation === nation;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
key={nation}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
variant={selected ? 'solid' : 'soft'}
|
||||||
|
color={selected ? undefined : 'gray'}
|
||||||
|
highContrast
|
||||||
|
radius="none"
|
||||||
|
onClick={() =>
|
||||||
|
mutateTankopediaFilters((draft) => {
|
||||||
|
draft.nation =
|
||||||
|
draft.nation === nation ? undefined : nation;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
style={{ width: '1em', height: '1em' }}
|
||||||
|
src={asset(`flags/circle/${nation}.webp`)}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex
|
||||||
|
overflow="hidden"
|
||||||
|
style={{ borderRadius: 'var(--radius-full)' }}
|
||||||
|
direction={{ sm: 'row', initial: 'column' }}
|
||||||
|
>
|
||||||
|
{TANK_CLASSES.map((tankClass) => {
|
||||||
|
const Icon = classIcons[tankClass];
|
||||||
|
const selected = filters.class === tankClass;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
key={tankClass}
|
||||||
|
variant={selected ? 'solid' : 'soft'}
|
||||||
|
radius="none"
|
||||||
|
color={selected ? undefined : 'gray'}
|
||||||
|
highContrast
|
||||||
|
onClick={() =>
|
||||||
|
mutateTankopediaFilters((draft) => {
|
||||||
|
draft.class =
|
||||||
|
draft.class === tankClass ? undefined : tankClass;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon style={{ width: '1em', height: '1em' }} />
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex
|
||||||
|
overflow="hidden"
|
||||||
|
style={{ borderRadius: 'var(--radius-full)' }}
|
||||||
|
direction={{ sm: 'row', initial: 'column' }}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
variant={filters.type === 'researchable' ? 'solid' : 'soft'}
|
||||||
|
radius="none"
|
||||||
|
color={filters.type === 'researchable' ? undefined : 'gray'}
|
||||||
|
highContrast
|
||||||
|
onClick={() =>
|
||||||
|
mutateTankopediaFilters((draft) => {
|
||||||
|
draft.type =
|
||||||
|
draft.type === 'researchable' ? undefined : 'researchable';
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ResearchedIcon style={{ width: '1em', height: '1em' }} />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
variant={filters.type === 'premium' ? 'solid' : 'soft'}
|
||||||
|
radius="none"
|
||||||
|
color={filters.type === 'premium' ? 'amber' : 'gray'}
|
||||||
|
highContrast
|
||||||
|
onClick={() =>
|
||||||
|
mutateTankopediaFilters((draft) => {
|
||||||
|
draft.type = draft.type === 'premium' ? undefined : 'premium';
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
color={filters.type === 'premium' ? undefined : 'amber'}
|
||||||
|
style={{ display: 'flex', justifyContent: 'center' }}
|
||||||
|
>
|
||||||
|
<ResearchedIcon style={{ width: '1em', height: '1em' }} />
|
||||||
|
</Text>
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
variant={filters.type === 'collector' ? 'solid' : 'soft'}
|
||||||
|
radius="none"
|
||||||
|
color={filters.type === 'collector' ? 'blue' : 'gray'}
|
||||||
|
highContrast
|
||||||
|
onClick={() =>
|
||||||
|
mutateTankopediaFilters((draft) => {
|
||||||
|
draft.type = draft.type === 'collector' ? undefined : 'collector';
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
color={filters.type === 'premium' ? undefined : 'blue'}
|
||||||
|
style={{ display: 'flex', justifyContent: 'center' }}
|
||||||
|
>
|
||||||
|
<ResearchedIcon style={{ width: '1em', height: '1em' }} />
|
||||||
|
</Text>
|
||||||
|
</IconButton>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex
|
||||||
|
overflow="hidden"
|
||||||
|
style={{ borderRadius: 'var(--radius-full)' }}
|
||||||
|
direction={{ sm: 'row', initial: 'column' }}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
variant={filters.testing === 'include' ? 'solid' : 'soft'}
|
||||||
|
radius="none"
|
||||||
|
color={filters.testing === 'include' ? undefined : 'gray'}
|
||||||
|
highContrast
|
||||||
|
onClick={() =>
|
||||||
|
mutateTankopediaFilters((draft) => {
|
||||||
|
draft.testing = 'include';
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ExperimentIcon
|
||||||
|
style={{ width: '1em', height: '1em', color: 'currentColor' }}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
variant={filters.testing === 'exclude' ? 'solid' : 'soft'}
|
||||||
|
radius="none"
|
||||||
|
color={filters.testing === 'exclude' ? undefined : 'gray'}
|
||||||
|
highContrast
|
||||||
|
onClick={() =>
|
||||||
|
mutateTankopediaFilters((draft) => {
|
||||||
|
draft.testing = 'exclude';
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ScienceOffIcon
|
||||||
|
style={{ width: '1em', height: '1em', color: 'currentColor' }}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
variant={filters.testing === 'only' ? 'solid' : 'soft'}
|
||||||
|
radius="none"
|
||||||
|
color={filters.testing === 'only' ? undefined : 'gray'}
|
||||||
|
highContrast
|
||||||
|
onClick={() =>
|
||||||
|
mutateTankopediaFilters((draft) => {
|
||||||
|
draft.testing = 'only';
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ScienceIcon
|
||||||
|
style={{ width: '1em', height: '1em', color: 'currentColor' }}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Box display={{ initial: 'block', sm: 'none' }}>
|
||||||
|
<IconButton
|
||||||
|
color="red"
|
||||||
|
ml="2"
|
||||||
|
onClick={() =>
|
||||||
|
useTankopediaFilters.setState(
|
||||||
|
useTankopediaFilters.getInitialState(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<TrashIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
<Box display={{ initial: 'none', sm: 'block' }}>
|
||||||
|
<Button
|
||||||
|
color="red"
|
||||||
|
mt="2"
|
||||||
|
onClick={() =>
|
||||||
|
useTankopediaFilters.setState(
|
||||||
|
useTankopediaFilters.getInitialState(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<TrashIcon /> Clear
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,26 +1,41 @@
|
|||||||
import { Callout, Flex } from '@radix-ui/themes';
|
import { Callout, Flex } from '@radix-ui/themes';
|
||||||
|
import { go } from 'fuzzysort';
|
||||||
import { range } from 'lodash';
|
import { range } from 'lodash';
|
||||||
import { memo, use } from 'react';
|
import { memo, use, useMemo } from 'react';
|
||||||
import { ExperimentIcon } from '../../../../components/ExperimentIcon';
|
import { ExperimentIcon } from '../../../../components/ExperimentIcon';
|
||||||
import { TANK_CLASSES } from '../../../../components/Tanks/components/Item/constants';
|
|
||||||
import { gameDefinitions } from '../../../../core/blitzkit/gameDefinitions';
|
|
||||||
import {
|
import {
|
||||||
Tier,
|
Tier,
|
||||||
tanksDefinitionsArray,
|
tankDefinitions,
|
||||||
|
tankNames,
|
||||||
} from '../../../../core/blitzkit/tankDefinitions';
|
} from '../../../../core/blitzkit/tankDefinitions';
|
||||||
import { useTankopediaFilters } from '../../../../stores/tankopediaFilters';
|
import { useTankopediaFilters } from '../../../../stores/tankopediaFilters';
|
||||||
import { treeTypeOrder } from './TankSearch/constants';
|
import { SearchBar } from './SearchBar';
|
||||||
|
import { TankCard } from './TankCard';
|
||||||
|
import { TankCardWrapper } from './TankCardWrapper';
|
||||||
import { TierCard } from './TierCard';
|
import { TierCard } from './TierCard';
|
||||||
|
|
||||||
export const Results = memo(() => {
|
export const Results = memo(() => {
|
||||||
const tanks = use(tanksDefinitionsArray);
|
const awaitedTankDefinitions = use(tankDefinitions);
|
||||||
const awaitedGameDefinitions = use(gameDefinitions);
|
const awaitedTankNames = use(tankNames);
|
||||||
const testing = useTankopediaFilters((state) => state.testing);
|
const testing = useTankopediaFilters((state) => state.testing);
|
||||||
|
const search = useTankopediaFilters((state) => state.search);
|
||||||
|
const searchedTanks = useMemo(() => {
|
||||||
|
if (search === undefined) return undefined;
|
||||||
|
|
||||||
|
const searchedRaw = go(search, awaitedTankNames, { key: 'combined' });
|
||||||
|
const searchedTanks = searchedRaw.map(
|
||||||
|
(result) => awaitedTankDefinitions[result.obj.id],
|
||||||
|
);
|
||||||
|
|
||||||
|
return searchedTanks;
|
||||||
|
}, [search]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" gap="8" flexGrow="1">
|
<Flex direction="column" gap="4" flexGrow="1">
|
||||||
|
<SearchBar topResult={searchedTanks?.[0]} />
|
||||||
|
|
||||||
{testing === 'only' && (
|
{testing === 'only' && (
|
||||||
<Callout.Root color="amber" style={{ width: 'fit-content' }}>
|
<Callout.Root color="amber">
|
||||||
<Callout.Icon>
|
<Callout.Icon>
|
||||||
<ExperimentIcon style={{ width: '1em', height: '1em' }} />
|
<ExperimentIcon style={{ width: '1em', height: '1em' }} />
|
||||||
</Callout.Icon>
|
</Callout.Icon>
|
||||||
@@ -31,27 +46,14 @@ export const Results = memo(() => {
|
|||||||
</Callout.Root>
|
</Callout.Root>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{range(10, 0).map((tierUntyped) => {
|
{!search &&
|
||||||
const tier = tierUntyped as Tier;
|
range(10, 0).map((tier) => <TierCard key={tier} tier={tier as Tier} />)}
|
||||||
const tierTanks = tanks
|
|
||||||
.filter((tank) => tank.tier === tier)
|
|
||||||
.sort(
|
|
||||||
(a, b) =>
|
|
||||||
treeTypeOrder.indexOf(a.treeType) -
|
|
||||||
treeTypeOrder.indexOf(b.treeType),
|
|
||||||
)
|
|
||||||
.sort(
|
|
||||||
(a, b) =>
|
|
||||||
TANK_CLASSES.indexOf(a.class) - TANK_CLASSES.indexOf(b.class),
|
|
||||||
)
|
|
||||||
.sort(
|
|
||||||
(a, b) =>
|
|
||||||
awaitedGameDefinitions.nations.indexOf(a.nation) -
|
|
||||||
awaitedGameDefinitions.nations.indexOf(b.nation),
|
|
||||||
);
|
|
||||||
|
|
||||||
return <TierCard tanks={tierTanks} key={tier} tier={tier} />;
|
{search && (
|
||||||
})}
|
<TankCardWrapper py="4">
|
||||||
|
{searchedTanks?.map((tank) => <TankCard key={tank.id} tank={tank} />)}
|
||||||
|
</TankCardWrapper>
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { CaretRightIcon, MagnifyingGlassIcon } from '@radix-ui/react-icons';
|
||||||
|
import { Button, Spinner, TextField } from '@radix-ui/themes';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { KeyboardEventHandler, useCallback, useRef, useState } from 'react';
|
||||||
|
import { Link } from '../../../../components/Link';
|
||||||
|
import { TankDefinition } from '../../../../core/blitzkit/tankDefinitions';
|
||||||
|
import { useTankopediaFilters } from '../../../../stores/tankopediaFilters';
|
||||||
|
|
||||||
|
interface SearchBarProps {
|
||||||
|
topResult?: TankDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SearchBar({ topResult }: SearchBarProps) {
|
||||||
|
const router = useRouter();
|
||||||
|
const [searching, setSearching] = useState(false);
|
||||||
|
const input = useRef<HTMLInputElement>(null);
|
||||||
|
const search = useCallback(
|
||||||
|
debounce(() => {
|
||||||
|
setSearching(false);
|
||||||
|
|
||||||
|
if (!input.current) return;
|
||||||
|
|
||||||
|
const sanitized = input.current.value.trim();
|
||||||
|
|
||||||
|
useTankopediaFilters.setState({
|
||||||
|
search: sanitized.length === 0 ? undefined : sanitized,
|
||||||
|
});
|
||||||
|
}, 500),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
const handleChange = useCallback(() => {
|
||||||
|
setSearching(true);
|
||||||
|
search();
|
||||||
|
}, []);
|
||||||
|
const handleKeyDown = useCallback<KeyboardEventHandler>(
|
||||||
|
(event) => {
|
||||||
|
if (event.key !== 'Enter' || !topResult) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
router.push(`/tools/tankopedia/${topResult.id}`);
|
||||||
|
},
|
||||||
|
[topResult],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextField.Root
|
||||||
|
ref={input}
|
||||||
|
placeholder="Search tanks..."
|
||||||
|
size="3"
|
||||||
|
onChange={handleChange}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
>
|
||||||
|
<TextField.Slot>
|
||||||
|
{searching ? <Spinner /> : <MagnifyingGlassIcon />}
|
||||||
|
</TextField.Slot>
|
||||||
|
|
||||||
|
{topResult && (
|
||||||
|
<TextField.Slot>
|
||||||
|
<Link href={`/tools/tankopedia/${topResult.id}`}>
|
||||||
|
<Button variant="ghost" size="3">
|
||||||
|
{topResult.name} <CaretRightIcon />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</TextField.Slot>
|
||||||
|
)}
|
||||||
|
</TextField.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Grid, GridProps } from '@radix-ui/themes';
|
||||||
|
|
||||||
|
export function TankCardWrapper(props: GridProps) {
|
||||||
|
return (
|
||||||
|
<Grid
|
||||||
|
flexGrow="1"
|
||||||
|
columns="repeat(auto-fill, minmax(100px, 1fr))"
|
||||||
|
gap="2"
|
||||||
|
gapY="6"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,22 +1,27 @@
|
|||||||
import { Flex, Grid, Heading, Separator } from '@radix-ui/themes';
|
import { Flex, Heading, Separator } from '@radix-ui/themes';
|
||||||
import { memo, useEffect, useRef } from 'react';
|
import { memo, use, useEffect, useMemo, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
TankDefinition,
|
|
||||||
Tier,
|
Tier,
|
||||||
|
tanksDefinitionsArray,
|
||||||
} from '../../../../core/blitzkit/tankDefinitions';
|
} from '../../../../core/blitzkit/tankDefinitions';
|
||||||
import { TIER_ROMAN_NUMERALS } from '../../../../core/blitzkit/tankDefinitions/constants';
|
import { TIER_ROMAN_NUMERALS } from '../../../../core/blitzkit/tankDefinitions/constants';
|
||||||
import { tankopediaFilterTank } from '../../../../core/blitzkit/tankopediaFilterTank';
|
import { tankopediaFilterTank } from '../../../../core/blitzkit/tankopediaFilterTank';
|
||||||
import { useTankopediaFilters } from '../../../../stores/tankopediaFilters';
|
import { useTankopediaFilters } from '../../../../stores/tankopediaFilters';
|
||||||
import { TankCard } from './TankCard';
|
import { TankCard } from './TankCard';
|
||||||
|
import { TankCardWrapper } from './TankCardWrapper';
|
||||||
|
|
||||||
interface TierCardProps {
|
interface TierCardProps {
|
||||||
tier: Tier;
|
tier: Tier;
|
||||||
tanks: TankDefinition[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TierCard = memo(
|
export const TierCard = memo(
|
||||||
({ tier, tanks }: TierCardProps) => {
|
({ tier }: TierCardProps) => {
|
||||||
|
const awaitedTanksDefinitionsArray = use(tanksDefinitionsArray);
|
||||||
const container = useRef<HTMLDivElement>(null);
|
const container = useRef<HTMLDivElement>(null);
|
||||||
|
const tanks = useMemo(
|
||||||
|
() => awaitedTanksDefinitionsArray.filter((tank) => tank.tier === tier),
|
||||||
|
[tier],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribe = useTankopediaFilters.subscribe((filters) => {
|
const unsubscribe = useTankopediaFilters.subscribe((filters) => {
|
||||||
@@ -37,22 +42,18 @@ export const TierCard = memo(
|
|||||||
direction="column"
|
direction="column"
|
||||||
gap="4"
|
gap="4"
|
||||||
display={tanks.length === 0 ? 'none' : 'flex'}
|
display={tanks.length === 0 ? 'none' : 'flex'}
|
||||||
|
py="4"
|
||||||
>
|
>
|
||||||
<Flex align="center" mb="4" gap="4">
|
<Flex align="center" mb="4" gap="4">
|
||||||
<Heading weight="regular">Tier {TIER_ROMAN_NUMERALS[tier]}</Heading>
|
<Heading weight="regular">Tier {TIER_ROMAN_NUMERALS[tier]}</Heading>
|
||||||
<Separator style={{ flex: 1 }} />
|
<Separator style={{ flex: 1 }} />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Grid
|
<TankCardWrapper>
|
||||||
flexGrow="1"
|
|
||||||
columns="repeat(auto-fill, minmax(100px, 1fr))"
|
|
||||||
gap="2"
|
|
||||||
gapY="6"
|
|
||||||
>
|
|
||||||
{tanks.map((tank) => (
|
{tanks.map((tank) => (
|
||||||
<TankCard key={tank.id} tank={tank} />
|
<TankCard key={tank.id} tank={tank} />
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</TankCardWrapper>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,395 +1,20 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { TrashIcon } from '@radix-ui/react-icons';
|
import { Flex } from '@radix-ui/themes';
|
||||||
import { Box, Button, Flex, IconButton, Text } from '@radix-ui/themes';
|
|
||||||
import { times } from 'lodash';
|
|
||||||
import { use } from 'react';
|
|
||||||
import { classIcons } from '../../../components/ClassIcon';
|
|
||||||
import { ExperimentIcon } from '../../../components/ExperimentIcon';
|
|
||||||
import { NAVBAR_HEIGHT } from '../../../components/Navbar';
|
|
||||||
import PageWrapper from '../../../components/PageWrapper';
|
import PageWrapper from '../../../components/PageWrapper';
|
||||||
import { ResearchedIcon } from '../../../components/ResearchedIcon';
|
import { FilterControl } from './components/FilterControl';
|
||||||
import { ScienceIcon } from '../../../components/ScienceIcon';
|
|
||||||
import { ScienceOffIcon } from '../../../components/ScienceOffIcon';
|
|
||||||
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';
|
import { Results } from './components/Results';
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const awaitedGameDefinitions = use(gameDefinitions);
|
|
||||||
const filters = useTankopediaFilters();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapper color="purple" size={1028 + 256}>
|
<PageWrapper color="purple" size={1028 + 256}>
|
||||||
<Flex
|
<Flex
|
||||||
gap="8"
|
gap="8"
|
||||||
direction={{
|
direction={{ initial: 'column', sm: 'row' }}
|
||||||
initial: 'column',
|
pt={{ initial: '0', sm: '6' }}
|
||||||
sm: 'row',
|
|
||||||
}}
|
|
||||||
pt={{
|
|
||||||
initial: '0',
|
|
||||||
sm: '6',
|
|
||||||
}}
|
|
||||||
pb="6"
|
pb="6"
|
||||||
>
|
>
|
||||||
<Flex
|
<FilterControl />
|
||||||
direction={{
|
|
||||||
initial: 'row',
|
|
||||||
sm: 'column',
|
|
||||||
}}
|
|
||||||
height="fit-content"
|
|
||||||
gap="2"
|
|
||||||
align={{
|
|
||||||
initial: 'start',
|
|
||||||
sm: 'start',
|
|
||||||
}}
|
|
||||||
justify={{
|
|
||||||
initial: 'center',
|
|
||||||
sm: 'start',
|
|
||||||
}}
|
|
||||||
position={{
|
|
||||||
initial: 'static',
|
|
||||||
sm: 'sticky',
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
top: `calc(${NAVBAR_HEIGHT}px + var(--space-3))`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Flex
|
|
||||||
direction={{
|
|
||||||
initial: 'row',
|
|
||||||
sm: 'column',
|
|
||||||
}}
|
|
||||||
overflow="hidden"
|
|
||||||
style={{
|
|
||||||
borderRadius: 'var(--radius-4)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Flex
|
|
||||||
direction={{
|
|
||||||
sm: 'row',
|
|
||||||
initial: 'column',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{times(5, (index) => {
|
|
||||||
const tier = (index + 1) as Tier;
|
|
||||||
const selected = filters.tier === tier;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<IconButton
|
|
||||||
key={tier}
|
|
||||||
variant={selected ? 'solid' : 'soft'}
|
|
||||||
radius="none"
|
|
||||||
color={selected ? undefined : 'gray'}
|
|
||||||
highContrast
|
|
||||||
onClick={() =>
|
|
||||||
mutateTankopediaFilters((draft) => {
|
|
||||||
draft.tier = draft.tier === tier ? undefined : tier;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Text size="2">{TIER_ROMAN_NUMERALS[tier]}</Text>
|
|
||||||
</IconButton>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Flex>
|
|
||||||
<Flex
|
|
||||||
direction={{
|
|
||||||
sm: 'row',
|
|
||||||
initial: 'column',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{times(5, (index) => {
|
|
||||||
const tier = (index + 6) as Tier;
|
|
||||||
const selected = filters.tier === tier;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<IconButton
|
|
||||||
key={tier}
|
|
||||||
variant={selected ? 'solid' : 'soft'}
|
|
||||||
radius="none"
|
|
||||||
color={selected ? undefined : 'gray'}
|
|
||||||
highContrast
|
|
||||||
onClick={() =>
|
|
||||||
mutateTankopediaFilters((draft) => {
|
|
||||||
draft.tier = draft.tier === tier ? undefined : tier;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Text size="2">{TIER_ROMAN_NUMERALS[tier]}</Text>
|
|
||||||
</IconButton>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Flex
|
|
||||||
direction={{
|
|
||||||
initial: 'row',
|
|
||||||
sm: 'column',
|
|
||||||
}}
|
|
||||||
overflow="hidden"
|
|
||||||
style={{
|
|
||||||
borderRadius: 'var(--radius-4)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Flex
|
|
||||||
direction={{
|
|
||||||
sm: 'row',
|
|
||||||
initial: 'column',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{awaitedGameDefinitions.nations
|
|
||||||
.slice(0, Math.ceil(awaitedGameDefinitions.nations.length / 2))
|
|
||||||
.map((nation) => {
|
|
||||||
const selected = filters.nation === nation;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<IconButton
|
|
||||||
variant={selected ? 'solid' : 'soft'}
|
|
||||||
color={selected ? undefined : 'gray'}
|
|
||||||
highContrast
|
|
||||||
radius="none"
|
|
||||||
onClick={() =>
|
|
||||||
mutateTankopediaFilters((draft) => {
|
|
||||||
draft.nation =
|
|
||||||
draft.nation === nation ? undefined : nation;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
style={{ width: '1em', height: '1em' }}
|
|
||||||
src={asset(`flags/circle/${nation}.webp`)}
|
|
||||||
/>
|
|
||||||
</IconButton>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Flex>
|
|
||||||
<Flex
|
|
||||||
direction={{
|
|
||||||
sm: 'row',
|
|
||||||
initial: 'column',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{awaitedGameDefinitions.nations
|
|
||||||
.slice(Math.ceil(awaitedGameDefinitions.nations.length / 2))
|
|
||||||
.map((nation) => {
|
|
||||||
const selected = filters.nation === nation;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<IconButton
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
variant={selected ? 'solid' : 'soft'}
|
|
||||||
color={selected ? undefined : 'gray'}
|
|
||||||
highContrast
|
|
||||||
radius="none"
|
|
||||||
onClick={() =>
|
|
||||||
mutateTankopediaFilters((draft) => {
|
|
||||||
draft.nation =
|
|
||||||
draft.nation === nation ? undefined : nation;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
style={{ width: '1em', height: '1em' }}
|
|
||||||
src={asset(`flags/circle/${nation}.webp`)}
|
|
||||||
/>
|
|
||||||
</IconButton>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Flex
|
|
||||||
overflow="hidden"
|
|
||||||
style={{
|
|
||||||
borderRadius: 'var(--radius-full)',
|
|
||||||
}}
|
|
||||||
direction={{
|
|
||||||
sm: 'row',
|
|
||||||
initial: 'column',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{TANK_CLASSES.map((tankClass) => {
|
|
||||||
const Icon = classIcons[tankClass];
|
|
||||||
const selected = filters.class === tankClass;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<IconButton
|
|
||||||
key={tankClass}
|
|
||||||
variant={selected ? 'solid' : 'soft'}
|
|
||||||
radius="none"
|
|
||||||
color={selected ? undefined : 'gray'}
|
|
||||||
highContrast
|
|
||||||
onClick={() =>
|
|
||||||
mutateTankopediaFilters((draft) => {
|
|
||||||
draft.class =
|
|
||||||
draft.class === tankClass ? undefined : tankClass;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Icon style={{ width: '1em', height: '1em' }} />
|
|
||||||
</IconButton>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Flex
|
|
||||||
overflow="hidden"
|
|
||||||
style={{
|
|
||||||
borderRadius: 'var(--radius-full)',
|
|
||||||
}}
|
|
||||||
direction={{
|
|
||||||
sm: 'row',
|
|
||||||
initial: 'column',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
variant={filters.type === 'researchable' ? 'solid' : 'soft'}
|
|
||||||
radius="none"
|
|
||||||
color={filters.type === 'researchable' ? undefined : 'gray'}
|
|
||||||
highContrast
|
|
||||||
onClick={() =>
|
|
||||||
mutateTankopediaFilters((draft) => {
|
|
||||||
draft.type =
|
|
||||||
draft.type === 'researchable' ? undefined : 'researchable';
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ResearchedIcon style={{ width: '1em', height: '1em' }} />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
variant={filters.type === 'premium' ? 'solid' : 'soft'}
|
|
||||||
radius="none"
|
|
||||||
color={filters.type === 'premium' ? 'amber' : 'gray'}
|
|
||||||
highContrast
|
|
||||||
onClick={() =>
|
|
||||||
mutateTankopediaFilters((draft) => {
|
|
||||||
draft.type = draft.type === 'premium' ? undefined : 'premium';
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
color={filters.type === 'premium' ? undefined : 'amber'}
|
|
||||||
style={{ display: 'flex', justifyContent: 'center' }}
|
|
||||||
>
|
|
||||||
<ResearchedIcon style={{ width: '1em', height: '1em' }} />
|
|
||||||
</Text>
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
variant={filters.type === 'collector' ? 'solid' : 'soft'}
|
|
||||||
radius="none"
|
|
||||||
color={filters.type === 'collector' ? 'blue' : 'gray'}
|
|
||||||
highContrast
|
|
||||||
onClick={() =>
|
|
||||||
mutateTankopediaFilters((draft) => {
|
|
||||||
draft.type =
|
|
||||||
draft.type === 'collector' ? undefined : 'collector';
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
color={filters.type === 'premium' ? undefined : 'blue'}
|
|
||||||
style={{ display: 'flex', justifyContent: 'center' }}
|
|
||||||
>
|
|
||||||
<ResearchedIcon style={{ width: '1em', height: '1em' }} />
|
|
||||||
</Text>
|
|
||||||
</IconButton>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Flex
|
|
||||||
overflow="hidden"
|
|
||||||
style={{
|
|
||||||
borderRadius: 'var(--radius-full)',
|
|
||||||
}}
|
|
||||||
direction={{
|
|
||||||
sm: 'row',
|
|
||||||
initial: 'column',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
variant={filters.testing === 'include' ? 'solid' : 'soft'}
|
|
||||||
radius="none"
|
|
||||||
color={filters.testing === 'include' ? undefined : 'gray'}
|
|
||||||
highContrast
|
|
||||||
onClick={() =>
|
|
||||||
mutateTankopediaFilters((draft) => {
|
|
||||||
draft.testing = 'include';
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ExperimentIcon
|
|
||||||
style={{ width: '1em', height: '1em', color: 'currentColor' }}
|
|
||||||
/>
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
variant={filters.testing === 'exclude' ? 'solid' : 'soft'}
|
|
||||||
radius="none"
|
|
||||||
color={filters.testing === 'exclude' ? undefined : 'gray'}
|
|
||||||
highContrast
|
|
||||||
onClick={() =>
|
|
||||||
mutateTankopediaFilters((draft) => {
|
|
||||||
draft.testing = 'exclude';
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ScienceOffIcon
|
|
||||||
style={{ width: '1em', height: '1em', color: 'currentColor' }}
|
|
||||||
/>
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
variant={filters.testing === 'only' ? 'solid' : 'soft'}
|
|
||||||
radius="none"
|
|
||||||
color={filters.testing === 'only' ? undefined : 'gray'}
|
|
||||||
highContrast
|
|
||||||
onClick={() =>
|
|
||||||
mutateTankopediaFilters((draft) => {
|
|
||||||
draft.testing = 'only';
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ScienceIcon
|
|
||||||
style={{ width: '1em', height: '1em', color: 'currentColor' }}
|
|
||||||
/>
|
|
||||||
</IconButton>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Box display={{ initial: 'block', sm: 'none' }}>
|
|
||||||
<IconButton
|
|
||||||
color="red"
|
|
||||||
ml="2"
|
|
||||||
onClick={() =>
|
|
||||||
useTankopediaFilters.setState(
|
|
||||||
useTankopediaFilters.getInitialState(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<TrashIcon />
|
|
||||||
</IconButton>
|
|
||||||
</Box>
|
|
||||||
<Box display={{ initial: 'none', sm: 'block' }}>
|
|
||||||
<Button
|
|
||||||
color="red"
|
|
||||||
mt="2"
|
|
||||||
onClick={() =>
|
|
||||||
useTankopediaFilters.setState(
|
|
||||||
useTankopediaFilters.getInitialState(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<TrashIcon /> Clear
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Results />
|
<Results />
|
||||||
</Flex>
|
</Flex>
|
||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export interface TankopediaFilters {
|
|||||||
class?: TankClass;
|
class?: TankClass;
|
||||||
type?: TreeType;
|
type?: TreeType;
|
||||||
testing: 'include' | 'exclude' | 'only';
|
testing: 'include' | 'exclude' | 'only';
|
||||||
|
search?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useTankopediaFilters = create<TankopediaFilters>()(() => ({
|
export const useTankopediaFilters = create<TankopediaFilters>()(() => ({
|
||||||
|
|||||||
Reference in new issue
Block a user