feat: better layout

This commit is contained in:
TrèsAbhi committed 2024-06-24 13:54:08 -05:00
1 parent 0c74cb90e9
commit fe945cced1
11 files changed
+156 -137

No files matched your search

@@ -1,10 +1,9 @@
import { TrashIcon } from '@radix-ui/react-icons'; import { ReloadIcon } from '@radix-ui/react-icons';
import { Box, Button, Flex, IconButton, Text } from '@radix-ui/themes'; import { Box, Flex, IconButton, Text } from '@radix-ui/themes';
import { times } from 'lodash'; import { times } from 'lodash';
import { use } from 'react'; import { use } from 'react';
import { classIcons } from '../../../../components/ClassIcon'; import { classIcons } from '../../../../components/ClassIcon';
import { ExperimentIcon } from '../../../../components/ExperimentIcon'; import { ExperimentIcon } from '../../../../components/ExperimentIcon';
import { NAVBAR_HEIGHT } from '../../../../components/Navbar';
import { ResearchedIcon } from '../../../../components/ResearchedIcon'; import { ResearchedIcon } from '../../../../components/ResearchedIcon';
import { ScienceIcon } from '../../../../components/ScienceIcon'; import { ScienceIcon } from '../../../../components/ScienceIcon';
import { ScienceOffIcon } from '../../../../components/ScienceOffIcon'; import { ScienceOffIcon } from '../../../../components/ScienceOffIcon';
@@ -23,19 +22,11 @@ export function FilterControl() {
const filters = useTankopediaFilters(); const filters = useTankopediaFilters();
return ( return (
<Flex <Flex height="fit-content" gap="2" align="start" justify="center">
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 <Flex
direction={{ initial: 'row', sm: 'column' }} direction={{ initial: 'row', sm: 'column' }}
overflow="hidden" overflow="hidden"
style={{ borderRadius: 'var(--radius-4)' }} style={{ borderRadius: 'var(--radius-5)' }}
> >
<Flex direction={{ sm: 'row', initial: 'column' }}> <Flex direction={{ sm: 'row', initial: 'column' }}>
{times(5, (index) => { {times(5, (index) => {
@@ -51,7 +42,7 @@ export function FilterControl() {
highContrast highContrast
onClick={() => onClick={() =>
mutateTankopediaFilters((draft) => { mutateTankopediaFilters((draft) => {
draft.tier = draft.tier === tier ? undefined : tier; draft.tier = tier;
}) })
} }
> >
@@ -74,7 +65,7 @@ export function FilterControl() {
highContrast highContrast
onClick={() => onClick={() =>
mutateTankopediaFilters((draft) => { mutateTankopediaFilters((draft) => {
draft.tier = draft.tier === tier ? undefined : tier; draft.tier = tier;
}) })
} }
> >
@@ -88,7 +79,7 @@ export function FilterControl() {
<Flex <Flex
direction={{ initial: 'row', sm: 'column' }} direction={{ initial: 'row', sm: 'column' }}
overflow="hidden" overflow="hidden"
style={{ borderRadius: 'var(--radius-4)' }} style={{ borderRadius: 'var(--radius-5)' }}
> >
<Flex direction={{ sm: 'row', initial: 'column' }}> <Flex direction={{ sm: 'row', initial: 'column' }}>
{awaitedGameDefinitions.nations {awaitedGameDefinitions.nations
@@ -227,7 +218,7 @@ export function FilterControl() {
} }
> >
<Text <Text
color={filters.type === 'premium' ? undefined : 'blue'} color={filters.type === 'collector' ? undefined : 'blue'}
style={{ display: 'flex', justifyContent: 'center' }} style={{ display: 'flex', justifyContent: 'center' }}
> >
<ResearchedIcon style={{ width: '1em', height: '1em' }} /> <ResearchedIcon style={{ width: '1em', height: '1em' }} />
@@ -287,10 +278,9 @@ export function FilterControl() {
</IconButton> </IconButton>
</Flex> </Flex>
<Box display={{ initial: 'block', sm: 'none' }}> <Box>
<IconButton <IconButton
color="red" color="red"
ml="2"
onClick={() => onClick={() =>
useTankopediaFilters.setState( useTankopediaFilters.setState(
useTankopediaFilters.getInitialState(), useTankopediaFilters.getInitialState(),
@@ -298,23 +288,9 @@ export function FilterControl() {
) )
} }
> >
<TrashIcon /> <ReloadIcon />
</IconButton> </IconButton>
</Box> </Box>
<Box display={{ initial: 'none', sm: 'block' }}>
<Button
color="red"
mt="2"
onClick={() =>
useTankopediaFilters.setState(
useTankopediaFilters.getInitialState(),
true,
)
}
>
<TrashIcon /> Clear
</Button>
</Box>
</Flex> </Flex>
); );
} }
+20 -24
View File
@@ -1,8 +1,6 @@
import { Callout, Flex } from '@radix-ui/themes'; import { Callout, Flex } from '@radix-ui/themes';
import { go } from 'fuzzysort'; import { go } from 'fuzzysort';
import { range } from 'lodash';
import { memo, use, useMemo } from 'react'; import { memo, use, useMemo } from 'react';
import { Vector2Tuple } from 'three';
import { ExperimentIcon } from '../../../../components/ExperimentIcon'; import { ExperimentIcon } from '../../../../components/ExperimentIcon';
import { resolveNearPenetration } from '../../../../core/blitz/resolveNearPenetration'; import { resolveNearPenetration } from '../../../../core/blitz/resolveNearPenetration';
import { modelDefinitions } from '../../../../core/blitzkit/modelDefinitions'; import { modelDefinitions } from '../../../../core/blitzkit/modelDefinitions';
@@ -11,13 +9,13 @@ import { resolveDpm } from '../../../../core/blitzkit/resolveDpm';
import { resolveReload } from '../../../../core/blitzkit/resolveReload'; import { resolveReload } from '../../../../core/blitzkit/resolveReload';
import { import {
TankDefinition, TankDefinition,
Tier,
tankDefinitions, tankDefinitions,
tankNames, tankNames,
tanksDefinitionsArray, tanksDefinitionsArray,
} from '../../../../core/blitzkit/tankDefinitions'; } from '../../../../core/blitzkit/tankDefinitions';
import { unionBoundingBox } from '../../../../core/blitzkit/unionBoundingBox'; import { unionBoundingBox } from '../../../../core/blitzkit/unionBoundingBox';
import { useTankopediaFilters } from '../../../../stores/tankopediaFilters'; import { useTankopediaFilters } from '../../../../stores/tankopediaFilters';
import { FilterControl } from './FilterControl';
import { SearchBar } from './SearchBar'; import { SearchBar } from './SearchBar';
import { TankCard } from './TankCard'; import { TankCard } from './TankCard';
import { TankCardWrapper } from './TankCardWrapper'; import { TankCardWrapper } from './TankCardWrapper';
@@ -30,10 +28,14 @@ export const Results = memo(() => {
const awaitedTankNames = use(tankNames); const awaitedTankNames = use(tankNames);
const testing = useTankopediaFilters((state) => state.testing); const testing = useTankopediaFilters((state) => state.testing);
const search = useTankopediaFilters((state) => state.search); const search = useTankopediaFilters((state) => state.search);
const tier = useTankopediaFilters((state) => state.tier);
const sort = useTankopediaFilters((state) => state.sort); const sort = useTankopediaFilters((state) => state.sort);
const searchedTanks = useMemo(() => { const searchedTanks = useMemo(() => {
if (search !== undefined) { if (search !== undefined) {
const searchedRaw = go(search, awaitedTankNames, { key: 'combined' }); const searchedRaw = go(search, awaitedTankNames, {
key: 'combined',
limit: 25,
});
const searchedTanks = searchedRaw.map( const searchedTanks = searchedRaw.map(
(result) => awaitedTankDefinitions[result.obj.id], (result) => awaitedTankDefinitions[result.obj.id],
); );
@@ -45,10 +47,6 @@ export const Results = memo(() => {
let sorted: TankDefinition[]; let sorted: TankDefinition[];
switch (sort.by) { switch (sort.by) {
case 'meta.name':
sorted = filtered.sort((a, b) => a.name?.localeCompare(b.name));
break;
case 'survivability.health': { case 'survivability.health': {
sorted = filtered.sort((a, b) => { sorted = filtered.sort((a, b) => {
const aHealth = a.health + a.turrets.at(-1)!.health; const aHealth = a.health + a.turrets.at(-1)!.health;
@@ -332,25 +330,23 @@ export const Results = memo(() => {
<Flex direction="column" gap="4" flexGrow="1"> <Flex direction="column" gap="4" flexGrow="1">
<SearchBar topResult={searchedTanks?.[0]} /> <SearchBar topResult={searchedTanks?.[0]} />
{!search && <FilterControl />}
{testing === 'only' && ( {testing === 'only' && (
<Callout.Root color="amber"> <Flex justify="center" mt="4">
<Callout.Icon> <Callout.Root color="amber">
<ExperimentIcon style={{ width: '1em', height: '1em' }} /> <Callout.Icon>
</Callout.Icon> <ExperimentIcon style={{ width: '1em', height: '1em' }} />
<Callout.Text> </Callout.Icon>
Tanks in testing are subject to change and many not represent the <Callout.Text>
final product. Tanks in testing are subject to change and many not represent the
</Callout.Text> final product.
</Callout.Root> </Callout.Text>
</Callout.Root>
</Flex>
)} )}
{!search && {!search && sort.by === 'meta.tier' && <TierCard tier={tier} />}
sort.by === 'meta.tier' &&
range(
...(sort.direction === 'descending'
? ([10, 0] as Vector2Tuple)
: ([1, 11] as Vector2Tuple)),
).map((tier) => <TierCard key={tier} tier={tier as Tier} />)}
{(search || sort.by !== 'meta.tier') && ( {(search || sort.by !== 'meta.tier') && (
<TankCardWrapper py="4"> <TankCardWrapper py="4">
@@ -45,31 +45,32 @@ export function SearchBar({ topResult }: SearchBarProps) {
); );
return ( return (
<Flex gap="2"> <Flex justify="center">
<TextField.Root <Flex gap="2" flexGrow="1">
style={{ flex: 1 }} <TextField.Root
ref={input} style={{ flex: 1 }}
placeholder="Search tanks..." ref={input}
size="3" placeholder="Search tanks..."
onChange={handleChange} onChange={handleChange}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
> >
<TextField.Slot>
{searching ? <Spinner /> : <MagnifyingGlassIcon />}
</TextField.Slot>
{topResult && (
<TextField.Slot> <TextField.Slot>
<Link href={`/tools/tankopedia/${topResult.id}`}> {searching ? <Spinner /> : <MagnifyingGlassIcon />}
<Button variant="ghost" size="3">
{topResult.name} <CaretRightIcon />
</Button>
</Link>
</TextField.Slot> </TextField.Slot>
)}
</TextField.Root>
<Sort /> {topResult && (
<TextField.Slot>
<Link href={`/tools/tankopedia/${topResult.id}`}>
<Button variant="ghost">
{topResult.name} <CaretRightIcon />
</Button>
</Link>
</TextField.Slot>
)}
</TextField.Root>
<Sort />
</Flex>
</Flex> </Flex>
); );
} }
+1 -1
View File
@@ -12,7 +12,7 @@ export function Sort() {
return ( return (
<DropdownMenu.Root modal={false}> <DropdownMenu.Root modal={false}>
<DropdownMenu.Trigger> <DropdownMenu.Trigger>
<Button variant="surface" size="3" color="gray"> <Button variant="surface" color="gray">
Sort Sort
<CaretDownIcon /> <CaretDownIcon />
</Button> </Button>
@@ -1,13 +1,10 @@
import { Flex, Text } from '@radix-ui/themes'; import { Flex, Text } from '@radix-ui/themes';
import { Icon } from '@radix-ui/themes/dist/cjs/components/callout'; import { Icon } from '@radix-ui/themes/dist/cjs/components/callout';
import { assignInlineVars } from '@vanilla-extract/dynamic'; import { assignInlineVars } from '@vanilla-extract/dynamic';
import { useEffect, useRef } from 'react';
import { Link } from '../../../../components/Link'; import { Link } from '../../../../components/Link';
import { asset } from '../../../../core/blitzkit/asset'; import { asset } from '../../../../core/blitzkit/asset';
import { TankDefinition } from '../../../../core/blitzkit/tankDefinitions'; import { TankDefinition } from '../../../../core/blitzkit/tankDefinitions';
import { tankIcon } from '../../../../core/blitzkit/tankIcon'; import { tankIcon } from '../../../../core/blitzkit/tankIcon';
import { tankopediaFilterTank } from '../../../../core/blitzkit/tankopediaFilterTank';
import { useTankopediaFilters } from '../../../../stores/tankopediaFilters';
import * as styles from './TankCard.css'; import * as styles from './TankCard.css';
interface TankCardProps { interface TankCardProps {
@@ -17,7 +14,7 @@ interface TankCardProps {
export function TankCard({ tank }: TankCardProps) { export function TankCard({ tank }: TankCardProps) {
// const awaitedModelDefinitions = use(modelDefinitions); // const awaitedModelDefinitions = use(modelDefinitions);
// const Icon = classIcons[tank.class]; // const Icon = classIcons[tank.class];
const link = useRef<HTMLAnchorElement>(null); // const link = useRef<HTMLAnchorElement>(null);
// const sortBy = useTankopediaFilters((state) => state.sort.by); // const sortBy = useTankopediaFilters((state) => state.sort.by);
// const discriminator = useMemo(() => { // const discriminator = useMemo(() => {
// if (sortBy.startsWith('meta')) return undefined; // if (sortBy.startsWith('meta')) return undefined;
@@ -46,20 +43,20 @@ export function TankCard({ tank }: TankCardProps) {
// } // }
// }, [sortBy]); // }, [sortBy]);
useEffect(() => { // useEffect(() => {
const unsubscribe = useTankopediaFilters.subscribe((filters) => { // const unsubscribe = useTankopediaFilters.subscribe((filters) => {
if (!link.current) return; // if (!link.current) return;
const visible = tankopediaFilterTank(filters, tank); // const visible = tankopediaFilterTank(filters, tank);
link.current.style.display = visible ? 'flex' : 'none'; // link.current.style.display = visible ? 'flex' : 'none';
}); // });
return unsubscribe; // return unsubscribe;
}, []); // }, []);
return ( return (
<Link <Link
ref={link} // ref={link}
size="1" size="1"
color={ color={
tank.treeType === 'collector' tank.treeType === 'collector'
@@ -3,7 +3,6 @@ import { Grid, GridProps } from '@radix-ui/themes';
export function TankCardWrapper(props: GridProps) { export function TankCardWrapper(props: GridProps) {
return ( return (
<Grid <Grid
flexGrow="1"
columns="repeat(auto-fill, minmax(100px, 1fr))" columns="repeat(auto-fill, minmax(100px, 1fr))"
gap="2" gap="2"
gapY="6" gapY="6"
@@ -1,12 +1,12 @@
import { Flex, Heading, Separator } from '@radix-ui/themes'; import { Flex, Text } from '@radix-ui/themes';
import { memo, use, useEffect, useMemo, useRef } from 'react'; import { memo, use, useMemo } from 'react';
import { Link } from '../../../../components/Link';
import { TANK_CLASSES } from '../../../../components/Tanks/components/Item/constants'; import { TANK_CLASSES } from '../../../../components/Tanks/components/Item/constants';
import { gameDefinitions } from '../../../../core/blitzkit/gameDefinitions'; import { gameDefinitions } from '../../../../core/blitzkit/gameDefinitions';
import { import {
Tier, Tier,
tanksDefinitionsArray, tanksDefinitionsArray,
} from '../../../../core/blitzkit/tankDefinitions'; } from '../../../../core/blitzkit/tankDefinitions';
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';
@@ -21,11 +21,11 @@ export const TierCard = memo(
({ tier }: TierCardProps) => { ({ tier }: TierCardProps) => {
const awaitedGameDefinitions = use(gameDefinitions); const awaitedGameDefinitions = use(gameDefinitions);
const awaitedTanksDefinitionsArray = use(tanksDefinitionsArray); const awaitedTanksDefinitionsArray = use(tanksDefinitionsArray);
const container = useRef<HTMLDivElement>(null); const filters = useTankopediaFilters();
const tanks = useMemo( const tanks = useMemo(
() => () =>
awaitedTanksDefinitionsArray awaitedTanksDefinitionsArray
.filter((tank) => tank.tier === tier) .filter((tank) => tankopediaFilterTank(filters, tank))
.sort( .sort(
(a, b) => (a, b) =>
treeTypeOrder.indexOf(a.treeType) - treeTypeOrder.indexOf(a.treeType) -
@@ -40,41 +40,38 @@ export const TierCard = memo(
awaitedGameDefinitions.nations.indexOf(a.nation) - awaitedGameDefinitions.nations.indexOf(a.nation) -
awaitedGameDefinitions.nations.indexOf(b.nation), awaitedGameDefinitions.nations.indexOf(b.nation),
), ),
[tier], [tier, filters],
); );
useEffect(() => { if (tanks.length === 0)
const unsubscribe = useTankopediaFilters.subscribe((filters) => { return (
if (!container.current) return; <Flex flexGrow="1" align="center" justify="center">
<Text color="gray">
const visible = tanks.some((tank) => No tanks found.{' '}
tankopediaFilterTank(filters, tank), <Link
); href="#"
container.current.style.display = visible ? 'flex' : 'none'; underline="always"
}); color="red"
onClick={() =>
return unsubscribe; useTankopediaFilters.setState(
}, []); useTankopediaFilters.getInitialState(),
true,
)
}
>
Try clearing filters
</Link>
.
</Text>
</Flex>
);
return ( return (
<Flex <TankCardWrapper mt="6">
ref={container} {tanks.map((tank) => (
direction="column" <TankCard key={tank.id} tank={tank} />
gap="4" ))}
display={tanks.length === 0 ? 'none' : 'flex'} </TankCardWrapper>
py="4"
>
<Flex align="center" mb="4" gap="4">
<Heading weight="regular">Tier {TIER_ROMAN_NUMERALS[tier]}</Heading>
<Separator style={{ flex: 1 }} />
</Flex>
<TankCardWrapper>
{tanks.map((tank) => (
<TankCard key={tank.id} tank={tank} />
))}
</TankCardWrapper>
</Flex>
); );
}, },
(prev, next) => prev.tier === next.tier, (prev, next) => prev.tier === next.tier,
@@ -0,0 +1,49 @@
import { Flex, IconButton, Text } from '@radix-ui/themes';
import { times } from 'lodash';
import { memo } from 'react';
import { Tier } from '../../../../core/blitzkit/tankDefinitions';
import { TIER_ROMAN_NUMERALS } from '../../../../core/blitzkit/tankDefinitions/constants';
import {
mutateTankopediaFilters,
useTankopediaFilters,
} from '../../../../stores/tankopediaFilters';
export const TierFilter = memo(() => {
const tierFilter = useTankopediaFilters((state) => state.tier);
const sort = useTankopediaFilters((state) => state.sort);
const search = useTankopediaFilters((state) => state.search);
if (sort.by !== 'meta.tier' || search) return null;
return (
<Flex justify="center">
<Flex
direction="row"
overflow="hidden"
style={{ borderRadius: 'var(--radius-full)' }}
>
{times(10, (index) => {
const tier = (10 - index) as Tier;
const selected = tierFilter === tier;
return (
<IconButton
key={tier}
variant={selected ? 'solid' : 'soft'}
radius="none"
color={selected ? undefined : 'gray'}
highContrast
onClick={() =>
mutateTankopediaFilters((draft) => {
draft.tier = tier;
})
}
>
<Text size="2">{TIER_ROMAN_NUMERALS[tier]}</Text>
</IconButton>
);
})}
</Flex>
</Flex>
);
});
+8 -4
View File
@@ -2,14 +2,18 @@
import { Flex } from '@radix-ui/themes'; import { Flex } from '@radix-ui/themes';
import PageWrapper from '../../../components/PageWrapper'; import PageWrapper from '../../../components/PageWrapper';
import { FilterControl } from './components/FilterControl';
import { Results } from './components/Results'; import { Results } from './components/Results';
export default function Page() { export default function Page() {
return ( return (
<PageWrapper color="purple" size={1028 + 256}> <PageWrapper color="purple" size={1028}>
<Flex gap="8" direction={{ initial: 'column', sm: 'row' }} pb="6"> <Flex
<FilterControl /> gap="8"
direction={{ initial: 'column', sm: 'row' }}
py="4"
flexGrow="1"
>
{/* <FilterControl /> */}
<Results /> <Results />
</Flex> </Flex>
</PageWrapper> </PageWrapper>
-1
View File
@@ -107,7 +107,6 @@ export type ArmorPiercingLayer =
export const SORT_NAMES = { export const SORT_NAMES = {
'meta.tier': 'tier', 'meta.tier': 'tier',
'meta.name': 'name',
'survivability.health': 'health', 'survivability.health': 'health',
'survivability.viewRange': 'view range', 'survivability.viewRange': 'view range',
'survivability.camouflageStill': 'camouflage still', 'survivability.camouflageStill': 'camouflage still',
+2 -1
View File
@@ -5,7 +5,7 @@ import { Tier } from '../core/blitzkit/tankDefinitions';
import { TankopediaSortBy, TankopediaSortDirection } from './tankopedia'; import { TankopediaSortBy, TankopediaSortDirection } from './tankopedia';
export interface TankopediaFilters { export interface TankopediaFilters {
tier?: Tier; tier: Tier;
nation?: string; nation?: string;
class?: TankClass; class?: TankClass;
type?: TreeType; type?: TreeType;
@@ -23,6 +23,7 @@ export const useTankopediaFilters = create<TankopediaFilters>()(() => ({
by: 'meta.tier', by: 'meta.tier',
direction: 'descending', direction: 'descending',
}, },
tier: 10,
})); }));
export function mutateTankopediaFilters( export function mutateTankopediaFilters(