mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-24 03:59:50 +02:00
feat: clean up tiers ui
This commit is contained in:
1 parent
5a0b309b0b
commit
7997f490fc
4 files changed
+147
-133
No files matched your search
@@ -930,7 +930,10 @@
|
|||||||
"description": "Reveals the tank without showing the name. This round will be considered incorrect and break your streak!",
|
"description": "Reveals the tank without showing the name. This round will be considered incorrect and break your streak!",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"reveal": "Reveal"
|
"reveal": "Reveal"
|
||||||
}
|
},
|
||||||
|
"clear": "Clear",
|
||||||
|
"select_all": "Select all",
|
||||||
|
"tier": "Tier {tier}"
|
||||||
},
|
},
|
||||||
|
|
||||||
"tankopedia": {
|
"tankopedia": {
|
||||||
|
|||||||
@@ -9,13 +9,17 @@ import {
|
|||||||
ArrowRightIcon,
|
ArrowRightIcon,
|
||||||
EyeOpenIcon,
|
EyeOpenIcon,
|
||||||
MagnifyingGlassIcon,
|
MagnifyingGlassIcon,
|
||||||
|
MixerVerticalIcon,
|
||||||
PaperPlaneIcon,
|
PaperPlaneIcon,
|
||||||
|
StarFilledIcon,
|
||||||
|
TrashIcon,
|
||||||
} from "@radix-ui/react-icons";
|
} from "@radix-ui/react-icons";
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
|
DropdownMenu,
|
||||||
Flex,
|
Flex,
|
||||||
IconButton,
|
IconButton,
|
||||||
Spinner,
|
Spinner,
|
||||||
@@ -23,7 +27,7 @@ import {
|
|||||||
TextField,
|
TextField,
|
||||||
} from "@radix-ui/themes";
|
} from "@radix-ui/themes";
|
||||||
import fuzzysort from "fuzzysort";
|
import fuzzysort from "fuzzysort";
|
||||||
import { debounce } from "lodash-es";
|
import { debounce, times } from "lodash-es";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { awaitableTankDefinitions } from "../core/awaitables/tankDefinitions";
|
import { awaitableTankDefinitions } from "../core/awaitables/tankDefinitions";
|
||||||
import { awaitableTankNames } from "../core/awaitables/tankNames";
|
import { awaitableTankNames } from "../core/awaitables/tankNames";
|
||||||
@@ -41,11 +45,7 @@ const [tankNames, tankDefinitions] = await Promise.all([
|
|||||||
|
|
||||||
const ids = Object.keys(tankDefinitions.tanks);
|
const ids = Object.keys(tankDefinitions.tanks);
|
||||||
|
|
||||||
interface GuesserProps {
|
export function Guesser() {
|
||||||
selectedTiers: number[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Guesser({ selectedTiers }: GuesserProps) {
|
|
||||||
const tank = Guess.use((state) => state.tank);
|
const tank = Guess.use((state) => state.tank);
|
||||||
const guessState = Guess.use((state) => state.guessState);
|
const guessState = Guess.use((state) => state.guessState);
|
||||||
const correctGuesses = Guess.use((state) => state.correctGuesses);
|
const correctGuesses = Guess.use((state) => state.correctGuesses);
|
||||||
@@ -57,6 +57,7 @@ export function Guesser({ selectedTiers }: GuesserProps) {
|
|||||||
const [searching, setSearching] = useState(false);
|
const [searching, setSearching] = useState(false);
|
||||||
const [results, setResults] = useState<TankDefinition[] | null>(null);
|
const [results, setResults] = useState<TankDefinition[] | null>(null);
|
||||||
const [selected, setSelected] = useState<TankDefinition | null>(null);
|
const [selected, setSelected] = useState<TankDefinition | null>(null);
|
||||||
|
const tiers = Guess.use((state) => state.tiers);
|
||||||
|
|
||||||
const requestSearch = useCallback(() => {
|
const requestSearch = useCallback(() => {
|
||||||
setSelected(null);
|
setSelected(null);
|
||||||
@@ -101,60 +102,60 @@ export function Guesser({ selectedTiers }: GuesserProps) {
|
|||||||
width="100%"
|
width="100%"
|
||||||
p="4"
|
p="4"
|
||||||
maxWidth="25rem"
|
maxWidth="25rem"
|
||||||
gap="4"
|
gap="3"
|
||||||
>
|
>
|
||||||
<Box position="relative">
|
<Box position="relative">
|
||||||
|
{results !== null && (
|
||||||
|
<Card variant="classic">
|
||||||
|
<Box py="2" px="3">
|
||||||
|
{results.length === 0 && (
|
||||||
|
<Flex justify="center">
|
||||||
|
<Text color="gray">
|
||||||
|
{strings.website.tools.guess.search.no_results}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
|
||||||
{results !== null && (
|
<SearchResults.Root>
|
||||||
<Card variant="classic">
|
{results.map((result) => {
|
||||||
<Box py="2" px="3">
|
const Icon = classIcons[result.class];
|
||||||
{results.length === 0 && (
|
|
||||||
<Flex justify="center">
|
|
||||||
<Text color="gray">
|
|
||||||
{strings.website.tools.guess.search.no_results}
|
|
||||||
</Text>
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<SearchResults.Root>
|
return (
|
||||||
{results.map((result) => {
|
<SearchResults.Item
|
||||||
const Icon = classIcons[result.class];
|
key={result.id}
|
||||||
|
onClick={() => {
|
||||||
|
if (!input.current) return;
|
||||||
|
|
||||||
return (
|
input.current.value = unwrap(result.name);
|
||||||
<SearchResults.Item
|
setSelected(result);
|
||||||
key={result.id}
|
setResults(null);
|
||||||
onClick={() => {
|
}}
|
||||||
if (!input.current) return;
|
text={unwrap(result.name)}
|
||||||
|
prefix={
|
||||||
input.current.value = unwrap(result.name);
|
<img
|
||||||
setSelected(result);
|
style={{ width: "1em", height: "1em" }}
|
||||||
setResults(null);
|
src={asset(`flags/circle/${result.nation}.webp`)}
|
||||||
}}
|
/>
|
||||||
text={unwrap(result.name)}
|
}
|
||||||
prefix={
|
discriminator={
|
||||||
<img
|
<Flex
|
||||||
style={{ width: "1em", height: "1em" }}
|
align="center"
|
||||||
src={asset(`flags/circle/${result.nation}.webp`)}
|
gap="1"
|
||||||
/>
|
width="38px"
|
||||||
}
|
justify="center"
|
||||||
discriminator={
|
>
|
||||||
<Flex
|
<Icon width="1em" height="1em" />
|
||||||
align="center"
|
{TIER_ROMAN_NUMERALS[result.tier]}
|
||||||
gap="1"
|
</Flex>
|
||||||
width="38px"
|
}
|
||||||
justify="center"
|
/>
|
||||||
>
|
);
|
||||||
<Icon width="1em" height="1em" />
|
})}
|
||||||
{TIER_ROMAN_NUMERALS[result.tier]}
|
</SearchResults.Root>
|
||||||
</Flex>
|
</Box>
|
||||||
}
|
</Card>
|
||||||
/>
|
)}
|
||||||
);
|
</Box>
|
||||||
})}
|
|
||||||
</SearchResults.Root>
|
|
||||||
</Box>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Flex justify="center" style={{ userSelect: "none" }}>
|
<Flex justify="center" style={{ userSelect: "none" }}>
|
||||||
<Text>
|
<Text>
|
||||||
@@ -167,11 +168,75 @@ export function Guesser({ selectedTiers }: GuesserProps) {
|
|||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Flex gap="3">
|
<Flex gap="3">
|
||||||
|
<DropdownMenu.Root modal={false}>
|
||||||
|
<DropdownMenu.Trigger>
|
||||||
|
<IconButton size="3" variant="surface" color="gray">
|
||||||
|
<MixerVerticalIcon />
|
||||||
|
</IconButton>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
|
||||||
|
<DropdownMenu.Content>
|
||||||
|
{times(10, (index) => {
|
||||||
|
const tier = 10 - index;
|
||||||
|
const isSelected = tiers.includes(tier);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu.CheckboxItem
|
||||||
|
checked={isSelected}
|
||||||
|
key={tier}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
Guess.mutate((draft) => {
|
||||||
|
if (isSelected) {
|
||||||
|
draft.tiers = draft.tiers.filter((t) => t !== tier);
|
||||||
|
} else {
|
||||||
|
draft.tiers = [...draft.tiers, tier];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{literals(strings.website.tools.guess.tier, {
|
||||||
|
tier: TIER_ROMAN_NUMERALS[tier],
|
||||||
|
})}
|
||||||
|
</DropdownMenu.CheckboxItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
|
||||||
|
<DropdownMenu.Item
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
Guess.mutate((draft) => {
|
||||||
|
draft.tiers = Guess.initial.tiers;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StarFilledIcon /> {strings.website.tools.guess.select_all}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
|
||||||
|
<DropdownMenu.Item
|
||||||
|
color="red"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
Guess.mutate((draft) => {
|
||||||
|
draft.tiers = [];
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TrashIcon /> {strings.website.tools.guess.clear}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
|
||||||
<TextField.Root
|
<TextField.Root
|
||||||
disabled={guessState !== GuessState.NotGuessed}
|
disabled={guessState !== GuessState.NotGuessed}
|
||||||
ref={input}
|
ref={input}
|
||||||
onChange={requestSearch}
|
onChange={requestSearch}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1, minWidth: "14rem" }}
|
||||||
placeholder={strings.website.tools.guess.search.placeholder}
|
placeholder={strings.website.tools.guess.search.placeholder}
|
||||||
size="3"
|
size="3"
|
||||||
variant="classic"
|
variant="classic"
|
||||||
@@ -225,7 +290,7 @@ export function Guesser({ selectedTiers }: GuesserProps) {
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
size="3"
|
size="3"
|
||||||
disabled={selectedTiers.length === 0}
|
disabled={tiers.length === 0}
|
||||||
color={
|
color={
|
||||||
guessState === GuessState.NotGuessed && selected === null
|
guessState === GuessState.NotGuessed && selected === null
|
||||||
? "red"
|
? "red"
|
||||||
@@ -247,7 +312,7 @@ export function Guesser({ selectedTiers }: GuesserProps) {
|
|||||||
} else {
|
} else {
|
||||||
const filteredIds = ids.filter((id) => {
|
const filteredIds = ids.filter((id) => {
|
||||||
const tank = tankDefinitions.tanks[Number(id)];
|
const tank = tankDefinitions.tanks[Number(id)];
|
||||||
return selectedTiers.includes(tank.tier);
|
return tiers.includes(tank.tier);
|
||||||
});
|
});
|
||||||
const id = Number(
|
const id = Number(
|
||||||
filteredIds[Math.floor(Math.random() * filteredIds.length)]
|
filteredIds[Math.floor(Math.random() * filteredIds.length)]
|
||||||
@@ -283,7 +348,6 @@ export function Guesser({ selectedTiers }: GuesserProps) {
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
import { Box, DropdownMenu, Flex, Heading, IconButton, Text } from "@radix-ui/themes";
|
import { Box, Flex, Heading, Text } from "@radix-ui/themes";
|
||||||
import { Suspense, useEffect, useState } from "react";
|
import { Suspense, useEffect } from "react";
|
||||||
import { MixerVerticalIcon } from "@radix-ui/react-icons";
|
|
||||||
import { times } from "lodash-es";
|
|
||||||
import { TIER_ROMAN_NUMERALS } from "@blitzkit/core";
|
|
||||||
import { GuessBackground } from "../../../components/GuessBackground";
|
import { GuessBackground } from "../../../components/GuessBackground";
|
||||||
import { Guesser } from "../../../components/Guesser";
|
import { Guesser } from "../../../components/Guesser";
|
||||||
import { GuessRenderer } from "../../../components/GuessRenderer";
|
import { GuessRenderer } from "../../../components/GuessRenderer";
|
||||||
@@ -58,26 +55,28 @@ function Container({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Content({ skeleton }: MaybeSkeletonComponentProps) {
|
function Content({ skeleton }: MaybeSkeletonComponentProps) {
|
||||||
const { unwrap, strings } = useLocale();
|
const { unwrap } = useLocale();
|
||||||
const tank = Guess.use((state) => state.tank);
|
const tank = Guess.use((state) => state.tank);
|
||||||
const guessState = Guess.use((state) => state.guessState);
|
const guessState = Guess.use((state) => state.guessState);
|
||||||
const isRevealed = guessState !== GuessState.NotGuessed;
|
const isRevealed = guessState !== GuessState.NotGuessed;
|
||||||
const name = unwrap(tank.name);
|
const name = unwrap(tank.name);
|
||||||
const fontSize = `min(48vh, ${55 / name.length}vw)`;
|
const fontSize = `min(48vh, ${55 / name.length}vw)`;
|
||||||
const transitionDuration = isRevealed ? "2s" : undefined;
|
const transitionDuration = isRevealed ? "2s" : undefined;
|
||||||
const [selectedTiers, setSelectedTiers] = useState<number[]>([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
|
const tiers = Guess.use((state) => state.tiers);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedTiers.length === 0) return;
|
if (tiers.length === 0) return;
|
||||||
|
|
||||||
const filteredIds = ids.filter((id) => {
|
const filteredIds = ids.filter((id) => {
|
||||||
const tank = tankDefinitions.tanks[Number(id)];
|
const tank = tankDefinitions.tanks[Number(id)];
|
||||||
return selectedTiers.includes(tank.tier);
|
return tiers.includes(tank.tier);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (filteredIds.length === 0) return;
|
if (filteredIds.length === 0) return;
|
||||||
|
|
||||||
const id = Number(filteredIds[Math.floor(Math.random() * filteredIds.length)]);
|
const id = Number(
|
||||||
|
filteredIds[Math.floor(Math.random() * filteredIds.length)]
|
||||||
|
);
|
||||||
const newTank = tankDefinitions.tanks[id];
|
const newTank = tankDefinitions.tanks[id];
|
||||||
|
|
||||||
Guess.mutate((draft) => {
|
Guess.mutate((draft) => {
|
||||||
@@ -85,72 +84,13 @@ function Content({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
draft.guessState = GuessState.NotGuessed;
|
draft.guessState = GuessState.NotGuessed;
|
||||||
draft.helpingReveal = false;
|
draft.helpingReveal = false;
|
||||||
});
|
});
|
||||||
}, [selectedTiers]);
|
}, [tiers]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex flexGrow="1" position="relative" overflow="hidden">
|
<Flex flexGrow="1" position="relative" overflow="hidden">
|
||||||
<GuessBackground />
|
<GuessBackground />
|
||||||
|
|
||||||
<DropdownMenu.Root modal={false}>
|
{tiers.length === 0 && (
|
||||||
<DropdownMenu.Trigger>
|
|
||||||
<IconButton
|
|
||||||
size="3"
|
|
||||||
variant="surface"
|
|
||||||
color="gray"
|
|
||||||
style={{
|
|
||||||
position: "fixed",
|
|
||||||
top: "80px",
|
|
||||||
right: "16px",
|
|
||||||
zIndex: 1000,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MixerVerticalIcon />
|
|
||||||
</IconButton>
|
|
||||||
</DropdownMenu.Trigger>
|
|
||||||
|
|
||||||
<DropdownMenu.Content>
|
|
||||||
{times(10, (index) => {
|
|
||||||
const tier = 10 - index;
|
|
||||||
const isSelected = selectedTiers.includes(tier);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownMenu.Item
|
|
||||||
key={tier}
|
|
||||||
color={isSelected ? "blue" : undefined}
|
|
||||||
onClick={(event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
setSelectedTiers((prev) => {
|
|
||||||
if (prev.includes(tier)) {
|
|
||||||
return prev.filter((t) => t !== tier);
|
|
||||||
} else {
|
|
||||||
return [...prev, tier];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{TIER_ROMAN_NUMERALS[tier]}
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
<DropdownMenu.Separator />
|
|
||||||
|
|
||||||
<DropdownMenu.Item
|
|
||||||
onClick={() => setSelectedTiers([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])}
|
|
||||||
>
|
|
||||||
Select All
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
|
|
||||||
<DropdownMenu.Item
|
|
||||||
color="red"
|
|
||||||
onClick={() => setSelectedTiers([])}
|
|
||||||
>
|
|
||||||
Clear All
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
</DropdownMenu.Content>
|
|
||||||
</DropdownMenu.Root>
|
|
||||||
|
|
||||||
{selectedTiers.length === 0 && (
|
|
||||||
<Box
|
<Box
|
||||||
position="absolute"
|
position="absolute"
|
||||||
top="50%"
|
top="50%"
|
||||||
@@ -161,7 +101,11 @@ function Content({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text size="8" weight="bold" style={{ color: "white", textShadow: "0 2px 8px rgba(0,0,0,0.5)" }}>
|
<Text
|
||||||
|
size="8"
|
||||||
|
weight="bold"
|
||||||
|
style={{ color: "white", textShadow: "0 2px 8px rgba(0,0,0,0.5)" }}
|
||||||
|
>
|
||||||
Select tier(s) to start playing
|
Select tier(s) to start playing
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -216,7 +160,7 @@ function Content({ skeleton }: MaybeSkeletonComponentProps) {
|
|||||||
{name}
|
{name}
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
<Guesser selectedTiers={selectedTiers} />
|
<Guesser />
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { TankDefinition } from "@blitzkit/core";
|
import type { TankDefinition } from "@blitzkit/core";
|
||||||
|
import { times } from "lodash-es";
|
||||||
import { Varuna } from "varuna";
|
import { Varuna } from "varuna";
|
||||||
|
|
||||||
export enum GuessState {
|
export enum GuessState {
|
||||||
@@ -14,6 +15,7 @@ export interface Guess {
|
|||||||
correctGuesses: number;
|
correctGuesses: number;
|
||||||
streak: number;
|
streak: number;
|
||||||
helpingReveal: boolean;
|
helpingReveal: boolean;
|
||||||
|
tiers: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Guess = new Varuna<Guess, TankDefinition>((tank) => ({
|
export const Guess = new Varuna<Guess, TankDefinition>((tank) => ({
|
||||||
@@ -23,4 +25,5 @@ export const Guess = new Varuna<Guess, TankDefinition>((tank) => ({
|
|||||||
correctGuesses: 0,
|
correctGuesses: 0,
|
||||||
streak: 0,
|
streak: 0,
|
||||||
helpingReveal: false,
|
helpingReveal: false,
|
||||||
|
tiers: times(10, (index) => index + 1),
|
||||||
}));
|
}));
|
||||||
Reference in new issue
Block a user