migrate 3

This commit is contained in:
tresabhi committed 2026-08-18 13:57:01 -05:00
1 parent de8b3959b3
commit 5285d081e4
29 files changed
+740 -83

No files matched your search

+1 -1
View File
@@ -424,7 +424,7 @@
"tier": "Tier {tier}"
},
"tankopedia": {
"tanks": {
"search": "Search a stat...",
"name": "Tankopedia",
+1
View File
@@ -40,6 +40,7 @@
"markdown-to-jsx": "^7.7.13",
"pwa-install-handler": "^2.6.3",
"quicklime": "^1.2.0",
"radix-ui": "^1.6.7",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-promise-suspense": "^0.3.4",
@@ -0,0 +1,86 @@
.button {
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
border-radius: var(--max-size);
padding: var(--space-3) var(--space-4);
border: none;
font-family: var(--font-family-regular);
font-size: 0.75rem;
color: var(--gray-1);
font-weight: var(--weight-medium);
cursor: pointer;
&[data-size="minor"] {
font-size: 0.75rem;
}
&[data-array="true"] {
&:first-child {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
&:last-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
&[data-parent-array="true"] {
*:first-child > & {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
*:last-child > & {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
svg,
img {
width: 1em;
height: 1em;
}
&[data-light-text="true"] {
color: var(--white-a12);
}
&[data-variant="solid"] {
background-color: var(--accent-8);
}
&[data-variant="surface"],
&[data-variant="soft"] {
background-color: var(--accent-3);
color: var(--gray-12);
}
&[data-variant="inverted"] {
background-color: var(--accent-12);
color: var(--accent-1);
}
&[data-variant="ghost"],
&[data-variant="outline"] {
background: none;
color: var(--accent-12);
}
&[data-variant="ghost"] {
padding: 0;
}
&[data-variant="surface"],
&[data-variant="outline"] {
outline: var(--space-1) solid var(--accent-6);
}
}
@@ -0,0 +1,54 @@
import type { ComponentProps } from "react";
import { classNames } from "../../ui/classNames";
import type { Color } from "../../ui/color";
import styles from "./index.module.css";
export interface ButtonProps extends ComponentProps<"button"> {
color?: Color;
radius?: `${1 | 2 | 3}` | "max";
variant?: "solid" | "surface" | "soft" | "inverted" | "ghost" | "outline";
size?: "regular" | "minor";
array?: boolean;
parentArray?: boolean;
}
export const LIGHT_TEXT_COLORS = new Set<Color>([
"gold",
"bronze",
"teal",
"grass",
"amber",
"plum",
"pink",
"brown",
"crimson",
"jade",
"blue",
"tomato",
]);
export function Button({
className,
color,
variant = "solid",
style,
radius = "max",
size = "regular",
array = false,
parentArray = false,
...props
}: ButtonProps) {
return (
<button
className={classNames("composable-radius", styles.button, className)}
data-accent={color}
data-variant={variant}
data-light-text={variant === "solid"}
data-radius={radius}
data-size={size}
data-array={array}
data-parent-array={parentArray}
{...props}
/>
);
}
Whitespace-only changes.
@@ -0,0 +1,10 @@
import type { ReactNode } from "react";
import styles from "./index.module.css";
interface Props {
children: ReactNode;
}
export function Callout({ children }: Props) {
return <div className={styles.callout}>{children}</div>;
}
@@ -0,0 +1,75 @@
import {
Menubar,
MenubarCheckboxItem,
MenubarContent,
MenubarLabel,
MenubarRadioGroup,
MenubarRadioItem,
MenubarSub,
MenubarSubContent,
MenubarSubTrigger,
MenubarTrigger,
type MenubarCheckboxItemProps,
type MenubarContentProps,
type MenubarLabelProps,
type MenubarProps,
type MenubarRadioGroupProps,
type MenubarRadioItemProps,
type MenubarSubContentProps,
type MenubarSubProps,
type MenubarSubTriggerProps,
type MenubarTriggerProps,
} from "radix-ui/menubar";
function Root(props: MenubarProps) {
return <Menubar {...props} />;
}
function Trigger(props: MenubarTriggerProps) {
return <MenubarTrigger {...props} />;
}
function Content(props: MenubarContentProps) {
return <MenubarContent {...props} />;
}
function Sub(props: MenubarSubProps) {
return <MenubarSub {...props} />;
}
function SubTrigger(props: MenubarSubTriggerProps) {
return <MenubarSubTrigger {...props} />;
}
function SubContent(props: MenubarSubContentProps) {
return <MenubarSubContent {...props} />;
}
function Label(props: MenubarLabelProps) {
return <MenubarLabel {...props} />;
}
function CheckboxItem(props: MenubarCheckboxItemProps) {
return <MenubarCheckboxItem {...props} />;
}
function RadioGroup(props: MenubarRadioGroupProps) {
return <MenubarRadioGroup {...props} />;
}
function RadioItem(props: MenubarRadioItemProps) {
return <MenubarRadioItem {...props} />;
}
export const DropdownMenu = {
Root,
Trigger,
Content,
Sub,
SubTrigger,
SubContent,
Label,
CheckboxItem,
RadioGroup,
RadioItem,
};
Whitespace-only changes.
@@ -0,0 +1,3 @@
export function Spinner() {
return null;
}
@@ -1,5 +1,4 @@
import { alias, TankType, type TankDefinition } from "@blitzkit/core";
import { Flex, Text, type TextProps } from "@radix-ui/themes";
import { uniq } from "lodash-es";
import { forwardRef, type ReactNode } from "react";
import { api } from "../../core/blitzkit/api";
@@ -15,16 +15,6 @@ import {
ResetIcon,
TrashIcon,
} from "@radix-ui/react-icons";
import {
Box,
Button,
DropdownMenu,
Flex,
IconButton,
Text,
Tooltip,
type FlexProps,
} from "@radix-ui/themes";
import { times } from "lodash-es";
import { Fragment, type ComponentProps, type ReactNode } from "react";
import { api } from "../../../core/blitzkit/api";
@@ -1,4 +1,3 @@
import { Flex, Link, Text } from "@radix-ui/themes";
import { useLocale } from "../../../hooks/useLocale";
import { TankFilters } from "../../../stores/tankFilters";
@@ -1,6 +1,5 @@
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";
@@ -1,4 +1,3 @@
import { Flex, Separator, Text } from "@radix-ui/themes";
import { isEqual } from "lodash-es";
import { useMemo } from "react";
import { api } from "../../../core/blitzkit/api";
@@ -1,4 +1,3 @@
import { Flex, Skeleton } from "@radix-ui/themes";
import { memo, useRef } from "react";
import { useIntersection } from "../../../hooks/useIntersection";
@@ -21,5 +20,5 @@ export const SkeletonTankCard = memo(
</Flex>
);
},
() => true
() => true,
);
@@ -1,11 +1,12 @@
import { CaretDownIcon } from '@radix-ui/react-icons';
import { Button, DropdownMenu } from '@radix-ui/themes';
import { useLocale } from '../../../hooks/useLocale';
import { CaretDownIcon } from "@radix-ui/react-icons";
import { useLocale } from "../../../hooks/useLocale";
import type {
TankopediaSortBy,
TankopediaSortDirection,
} from '../../../stores/tankopediaPersistent';
import { TankSort } from '../../../stores/tankopediaSort';
} from "../../../stores/tankopediaPersistent";
import { TankSort } from "../../../stores/tankopediaSort";
import { Button } from "../../Button";
import { DropdownMenu } from "../../DropdownMenu";
interface ItemProps {
by: TankopediaSortBy;
@@ -38,7 +39,7 @@ export function Sort() {
const { strings } = useLocale();
return (
<DropdownMenu.Root modal={false}>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Button variant="surface" color="gray">
{strings.website.common.tank_search.sort_dropdown.label}
@@ -1,5 +1,3 @@
import { Grid, type GridProps } from "@radix-ui/themes";
export function TankCardWrapper(props: GridProps) {
return (
<Grid
@@ -0,0 +1,19 @@
.container {
display: flex;
flex-direction: column;
gap: var(--space-2);
flex-grow: 1;
.tanks {
/* mt="2" gap="1" align="center" justify="center" direction="column" */
.count {
/* gap="2" */
}
}
.test-warning {
/* justify="center" mt="4" */
}
}
@@ -7,10 +7,9 @@ import {
type TankDefinition,
} 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 { memo, useEffect, useMemo, useState, type ComponentProps } from "react";
import usePromise from "react-promise-suspense";
import { awaitableTankNames } from "../../core/awaitables/tankNames";
import { api } from "../../core/blitzkit/api";
@@ -23,18 +22,22 @@ import { TankopediaPersistent } from "../../stores/tankopediaPersistent";
import { SORT_UNITS } from "../../stores/tankopediaPersistent/constants";
import { TankSort } from "../../stores/tankopediaSort";
import type { MaybeSkeletonComponentProps } from "../../types/maybeSkeletonComponentProps";
import { Callout } from "../Callout";
import { ExperimentIcon } from "../ExperimentIcon";
import { Link } from "../Link";
import { Text } from "../Text";
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 { TankSearchBar } from "../TankSearchBar";
import { SkeletonTankCard } from "./components/SkeletonTankCard";
import { TankCardWrapper } from "./components/TankCardWrapper";
import { MAX_RECENTLY_VIEWED } from "./constants";
import styles from "./index.module.css";
export type TankSearchProps = MaybeSkeletonComponentProps &
Omit<FlexProps, "onSelect"> & {
ComponentProps<"div"> & {
compact?: boolean;
onSelect?: (tank: TankDefinition) => void;
onSelectAll?: (tanks: TankDefinition[]) => void;
@@ -427,8 +430,8 @@ export const TankSearch = memo<TankSearchProps>(
}, [tankFilters, tankopediaSort]);
return (
<Flex direction="column" gap="2" flexGrow="1" {...props}>
<SearchBar
<div className={styles.container} {...props}>
<TankSearchBar
skeleton={skeleton}
topResult={tanks?.[0]}
onSelect={onSelect}
@@ -438,9 +441,9 @@ export const TankSearch = memo<TankSearchProps>(
{!skeleton && !compact && <RecentlyViewed />}
<Flex mt="2" gap="1" align="center" justify="center" direction="column">
<Flex gap="2">
<Text color="gray">
<div className={styles.tanks}>
<div className={styles.count}>
<Text lowContrast>
{sorted.length === 1
? strings.website.common.tank_search.count_singular
: literals(strings.website.common.tank_search.count_plural, {
@@ -468,7 +471,7 @@ export const TankSearch = memo<TankSearchProps>(
{strings.website.common.tank_search.select_all}
</Link>
)}
</Flex>
</div>
{tankopediaSort.by !== "meta.none" && (
<Text color="gray">
@@ -483,19 +486,15 @@ export const TankSearch = memo<TankSearchProps>(
, {tankopediaSort.direction}
</Text>
)}
</Flex>
</div>
{tankFilters.showTesting && !tankFilters.showNonTesting && (
<Flex justify="center" mt="4">
<Callout.Root color="amber">
<Callout.Icon>
<ExperimentIcon style={{ width: "1em", height: "1em" }} />
</Callout.Icon>
<Callout.Text>
{strings.website.common.warnings.test}
</Callout.Text>
</Callout.Root>
</Flex>
<div className={styles["test-warning"]}>
<Callout color="amber">
<ExperimentIcon style={{ width: "1em", height: "1em" }} />
{strings.website.common.warnings.test}
</Callout>
</div>
)}
{!skeleton && !tankFilters.searching && (
@@ -544,7 +543,7 @@ export const TankSearch = memo<TankSearchProps>(
)}
</TankCardWrapper>
)}
</Flex>
</div>
);
},
);
@@ -0,0 +1,7 @@
.search-bar {
/* justify="center" */
.wrapper {
/* gap="2" flexGrow="1" */
}
}
@@ -1,20 +1,25 @@
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";
import { useLocale } from "../../hooks/useLocale";
import { TankFilters } from "../../stores/tankFilters";
import type { MaybeSkeletonComponentProps } from "../../types/maybeSkeletonComponentProps";
import { Spinner } from "../Spinner";
import { Sort } from "../TankSearch/components/Sort";
import { TextField } from "../TextField";
import styles from "./index.module.css";
type SearchBarProps = MaybeSkeletonComponentProps & {
topResult?: TankDefinition;
onSelect?: (tank: TankDefinition) => void;
};
export function SearchBar({ topResult, skeleton, onSelect }: SearchBarProps) {
export function TankSearchBar({
topResult,
skeleton,
onSelect,
}: SearchBarProps) {
const { strings } = useLocale();
const search = TankFilters.use((state) => state.search);
const searching = TankFilters.use((state) => state.searching);
@@ -33,7 +38,7 @@ export function SearchBar({ topResult, skeleton, onSelect }: SearchBarProps) {
draft.search = sanitized.length === 0 ? null : sanitized;
});
}, 500),
[]
[],
);
const handleChange = useCallback(() => {
if (!searching) {
@@ -56,14 +61,13 @@ export function SearchBar({ topResult, skeleton, onSelect }: SearchBarProps) {
window.location.href = `/tanks/${topResult.slug}`;
}
},
[topResult]
[topResult],
);
return (
<Flex justify="center">
<Flex gap="2" flexGrow="1">
<TextField.Root
variant="classic"
<div className={styles["search-bar"]}>
<div className={styles.wrapper}>
<TextField
disabled={skeleton}
defaultValue={search ?? undefined}
style={{ flex: 1 }}
@@ -72,15 +76,11 @@ export function SearchBar({ topResult, skeleton, onSelect }: SearchBarProps) {
onChange={handleChange}
onKeyDown={handleKeyDown}
>
<TextField.Slot>
{searching ? <Spinner /> : <MagnifyingGlassIcon />}
</TextField.Slot>
<QuickLink topResult={topResult} />
</TextField.Root>
{searching ? <Spinner /> : <MagnifyingGlassIcon />}
</TextField>
<Sort />
</Flex>
</Flex>
</div>
</div>
);
}
@@ -0,0 +1,88 @@
.text-field {
display: flex;
align-items: center;
box-sizing: border-box;
gap: var(--space-3);
cursor: text;
font: inherit;
color: var(--gray-11);
&[data-variant="bubble"] {
border-radius: var(--max-size);
background-color: var(--gray-1);
outline: var(--outline-interactive);
input {
padding: var(--space-3) var(--space-4);
}
}
&[data-variant="form"] {
border-bottom: var(--space-1) solid var(--gray-6);
input {
padding: var(--space-2);
}
}
&[data-has-icon="true"] {
padding-left: var(--space-3);
input {
padding-left: 0;
}
}
&:focus-within {
outline: var(--outline-focus);
box-shadow: var(--shadow-focus);
}
&[data-disabled="true"] {
cursor: not-allowed;
background-color: var(--gray-3);
color: var(--gray-9);
input::placeholder {
color: var(--gray-6);
}
}
svg {
width: 1em;
height: 1em;
}
input {
min-width: 0;
flex: 1;
background: none;
border: none;
cursor: inherit;
font: inherit;
color: var(--gray-12);
&::placeholder {
color: var(--gray-9);
}
&:focus {
outline: none;
}
&[data-align="center"] {
text-align: center;
}
&[data-align="right"] {
text-align: right;
}
}
}
@@ -0,0 +1,75 @@
import {
forwardRef,
useCallback,
useImperativeHandle,
useRef,
type ChangeEvent,
type ComponentProps,
type ReactNode,
} from "react";
import { classNames } from "../../ui/classNames";
import styles from "./index.module.css";
export interface TextFieldProps extends ComponentProps<"input"> {
children?: ReactNode;
align?: "left" | "center" | "right";
autoWidth?: boolean;
variant?: "bubble" | "form";
}
export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
(
{
children,
className,
autoWidth,
placeholder,
align,
disabled,
value,
onChange,
variant = "bubble",
...props
},
ref,
) => {
const input = useRef<HTMLInputElement>(null);
const handleChange = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
if (autoWidth && input.current) {
input.current.style.width = `${event.target.value.length || event.target.placeholder.length}ch`;
}
onChange?.(event);
},
[onChange],
);
useImperativeHandle(ref, () => input.current!, []);
return (
<div
{...props}
className={classNames(styles["text-field"], className)}
data-has-icon={children !== undefined}
data-disabled={disabled}
data-variant={variant}
>
{children}
<input
data-align={align === "left" ? undefined : align}
disabled={disabled}
ref={input}
onChange={handleChange}
placeholder={placeholder}
value={value}
style={{
width: `${value?.toString().length || placeholder?.length || 0}ch`,
}}
/>
</div>
);
},
);
@@ -0,0 +1,94 @@
---
import type { Tool } from "../constants/tools";
import { getStrings } from "../core/i18n/getStrings";
import { Heading } from "./Heading";
interface Props {
locale: string;
tool: Tool;
}
const { tool, locale } = Astro.props;
const strings = getStrings(locale);
const toolStrings =
strings.website.tools[tool.id as keyof typeof strings.website.tools];
---
<style>
.tool-banner {
background-size: cover;
.content {
width: 100%;
height: 100%;
position: relative;
display: flex;
justify-content: center;
gap: var(--space-4);
padding: var(--space-5) var(--space-4);
box-sizing: border-box;
backdrop-filter: var(--blur-tint);
:root.light & {
backdrop-filter: var(--blur-tint) invert(1) hue-rotate(180deg);
}
@media (width > 40rem) {
padding: var(--space-6) var(--space-4);
}
.centered {
flex: 1;
max-width: var(--page-width);
display: flex;
flex-direction: column;
}
.pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
:root.light & {
filter: invert(1) brightness(0);
}
mask-image: linear-gradient(
90deg,
transparent 25%,
var(--black-a1),
transparent
);
mask-repeat: no-repeat;
mask-size: cover;
}
}
}
</style>
<div
class="tool-banner"
style={{
backgroundImage: `url(/media/tools/banners/${tool.id}.webp)`,
backgroundPosition: tool.banner_background_position ?? "top",
}}
>
<div class="content">
<div
class="pattern"
style={{ backgroundImage: `url(/media/patterns/${tool.id}.svg)` }}
>
</div>
<div class="centered">
<Heading weight="bold" size="2">
{toolStrings.name}
</Heading>
</div>
</div>
</div>
+21 -4
View File
@@ -2,58 +2,75 @@ export interface Tool {
id: string;
path?: string;
strings?: string;
href?: string;
banner_background_position: "top" | "center" | "bottom";
}
export const tools: Record<string, Tool> = {
tanks: {
id: "tanks",
strings: "tankopedia",
banner_background_position: "top",
},
players: {
id: "players",
strings: "players",
banner_background_position: "top",
},
compare: {
id: "compare",
banner_background_position: "bottom",
},
performance: {
id: "performance",
banner_background_position: "bottom",
},
playlist: {
id: "playlist",
banner_background_position: "bottom",
},
mixer: {
id: "mixer",
banner_background_position: "bottom",
},
guesser: {
id: "guesser",
banner_background_position: "bottom",
},
avatars: {
id: "avatars",
banner_background_position: "bottom",
},
backgrounds: {
id: "backgrounds",
banner_background_position: "bottom",
},
session: {
id: "session",
banner_background_position: "bottom",
},
tier_list: {
id: "tier_list",
banner_background_position: "bottom",
},
embed: {
id: "embed",
banner_background_position: "bottom",
},
more: {
id: "more",
href: "https://discord.gg/nDt7AjGJQH",
banner_background_position: "bottom",
},
api: {
id: "api",
banner_background_position: "bottom",
},
changelogs: {
id: "changelogs",
banner_background_position: "bottom",
},
settings: {
id: "changelogs",
banner_background_position: "center",
},
};
@@ -1,10 +1,8 @@
import { createDefaultSkills } from "@blitzkit/core";
import { Flex, Heading, Text } from "@radix-ui/themes";
import { useEffect, useMemo, useState } from "react";
import { CompareTable } from "../../../../components/Compare/CompareTable";
import { Controls } from "../../../../components/Compare/Controls";
import { DamageWarning } from "../../../../components/DamageWarning";
import { PageWrapper } from "../../../../components/PageWrapper";
import { api } from "../../../../core/blitzkit/api";
import { tankCharacteristics } from "../../../../core/blitzkit/tankCharacteristics";
import { tankToCompareMember } from "../../../../core/blitzkit/tankToCompareMember";
@@ -118,7 +116,7 @@ function Content() {
}, [members]);
return (
<PageWrapper color="crimson" maxWidth="100%" px="0" pb="0">
<>
<Flex justify="center" gap="4" align="center" direction="column">
<DamageWarning />
<Controls
@@ -148,6 +146,6 @@ function Content() {
</Text>
</Flex>
)}
</PageWrapper>
</>
);
}
@@ -1,4 +1,3 @@
import { PageWrapper } from "../../../../components/PageWrapper";
import { TankSearch } from "../../../../components/TankSearch";
import {
LocaleProvider,
@@ -12,9 +11,7 @@ export function Page({
}: LocaleAcceptorProps & MaybeSkeletonComponentProps) {
return (
<LocaleProvider locale={locale}>
<PageWrapper color="purple" maxWidth="80rem">
<TankSearch skeleton={skeleton} />
</PageWrapper>
<TankSearch skeleton={skeleton} />
</LocaleProvider>
);
}
@@ -1,6 +1,7 @@
---
import ToolBanner from "../../../../components/ToolBanner.astro";
import { tools } from "../../../../constants/tools";
import ToolLayout from "../../../../layouts/ToolLayout.astro";
import BaseLayout from "../../../../layouts/BaseLayout.astro";
import { Page } from "./_index";
export { getStaticPaths } from "../index.astro";
@@ -8,8 +9,10 @@ export { getStaticPaths } from "../index.astro";
const { locale } = Astro.props;
---
<ToolLayout {locale} tool={tools.tanks}>
<BaseLayout {locale} accent="purple">
<ToolBanner {locale} tool={tools.tanks} slot="pre-app" />
<Page {locale} client:only="react">
<Page {locale} skeleton slot="fallback" />
</Page>
</ToolLayout>
</BaseLayout>