some cleanup

This commit is contained in:
tresabhi committed 2026-08-29 13:13:44 -05:00
1 parent 8d7575815b
commit ec161addab
15 files changed
+254 -134

No files matched your search

@@ -5,8 +5,6 @@ import styles from "./index.module.css";
interface Props extends ComponentProps<"div"> {}
export function Back({ className, onClick, ...props }: Props) {
console.log("init");
return (
<div
className={classNames(styles.back, className)}
@@ -21,27 +21,19 @@
padding: var(--space-2) var(--space-3);
}
&[data-array="true"] {
&:first-child {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
&:last-child {
&[data-list-child="true"] {
&:not(:first-child) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
&[data-parent-array="true"] {
*:first-child > & {
border-left-width: var(--space-0);
}
&:not(:last-child) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
*:last-child > & {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-right-width: var(--space-0);
}
}
@@ -82,6 +74,6 @@
&[data-variant="surface"],
&[data-variant="outline"] {
outline: var(--space-1) solid var(--accent-6);
border: var(--space-1) solid var(--accent-6);
}
}
@@ -8,8 +8,7 @@ export interface ButtonProps extends ComponentProps<"button"> {
radius?: `${1 | 2 | 3}` | "max";
variant?: "solid" | "surface" | "soft" | "inverted" | "ghost" | "outline";
size?: "regular" | "minor";
array?: boolean;
parentArray?: boolean;
listChild?: boolean;
highContrast?: boolean; // TODO: add high contrast
}
@@ -35,8 +34,7 @@ export function Button({
style,
radius = "max",
size = "regular",
array = false,
parentArray = false,
listChild,
...props
}: ButtonProps) {
return (
@@ -47,8 +45,7 @@ export function Button({
data-light-text={variant === "solid"}
data-radius={radius}
data-size={size}
data-array={array}
data-parent-array={parentArray}
data-list-child={listChild}
{...props}
/>
);
@@ -0,0 +1,84 @@
import { times } from "lodash-es";
import { useEffect, useRef, useState, type ReactNode } from "react";
import type { MaybeSkeletonComponentProps } from "../types/maybeSkeletonComponentProps";
export interface IncrementalLoaderProps<Props extends { key: string }> {
initial: number;
intermediate: number;
rate?: number;
skeleton?: boolean;
data: Props[];
Component: (
props: MaybeSkeletonComponentProps<Omit<Props, "key">>,
) => ReactNode;
}
export function IncrementalLoader<Props extends { key: string }>({
initial,
rate = 2,
data,
skeleton,
intermediate,
Component,
}: IncrementalLoaderProps<Props>) {
const [count, setCount] = useState(initial);
const sliced = data.slice(0, count);
useEffect(() => setCount(initial), [data, initial]);
return (
<>
{!skeleton &&
sliced.map(({ key, ...props }) => <Component key={key} {...props} />)}
{times(
skeleton
? initial + intermediate
: Math.min(intermediate, data.length - count),
(index) => (
<Item key={index} rate={rate} setCount={setCount}>
<Component skeleton />
</Item>
),
)}
</>
);
}
interface ItemProps {
children: ReactNode;
rate: number;
setCount: (setter: (state: number) => number) => void;
}
function Item({ setCount, rate, children }: ItemProps) {
const wrapper = useRef<HTMLDivElement>(null);
const observer = useRef<IntersectionObserver | null>(null);
useEffect(() => {
function dispose() {
observer.current?.disconnect();
observer.current = null;
}
if (!wrapper.current) return;
if (observer.current === null) {
observer.current = new IntersectionObserver(([entry]) => {
if (!entry.isIntersecting) return;
setCount((state) => state + rate);
});
observer.current.observe(wrapper.current);
}
return dispose;
}, []);
return (
<div ref={wrapper} className="incremental-loader">
{children}
</div>
);
}
@@ -1,3 +1,4 @@
.inline-skeleton {
display: inline-block;
height: 1em;
}
@@ -1,11 +1,11 @@
import { Skeleton, type SkeletonProps } from '@radix-ui/themes';
import './index.css';
import { classNames } from "../../ui/classNames";
import { Skeleton, type SkeletonProps } from "../Skeleton";
import styles from "./index.module.css";
export function InlineSkeleton({ className, ...props }: SkeletonProps) {
return (
<Skeleton
className={`inline-skeleton ${className}`}
height="1em"
className={classNames(styles["inline-skeleton"], className)}
{...props}
/>
);
@@ -4,9 +4,11 @@ import { type ReactNode } from "react";
import { api } from "../../core/blitzkit/api";
import { useLocale } from "../../hooks/useLocale";
import { TankopediaPersistent } from "../../stores/tankopediaPersistent";
import type { MaybeSkeletonComponentProps } from "../../types/maybeSkeletonComponentProps";
import { classIcons } from "../ClassIcon";
import { Flex } from "../Flex";
import { LinkI18nWrapper } from "../LinkI18nWrapper";
import { TankCardSkeleton } from "../TankCardSkeleton";
import { MAX_RECENTLY_VIEWED } from "../TankSearch/constants";
import { Text, type TextProps } from "../Text";
import styles from "./index.module.css";
@@ -21,38 +23,36 @@ type TankCardProps = {
const tankDefinitions = await api.tankDefinitions();
export const TankCard = ({
tank,
discriminator,
onTankSelect: onSelect,
noLink,
compact,
}: TankCardProps) => {
export const TankCard = (props: MaybeSkeletonComponentProps<TankCardProps>) => {
if (props.skeleton) {
return <TankCardSkeleton />;
}
const { unwrap, locale } = useLocale();
const provideLink = !noLink && onSelect === undefined;
const name = unwrap(tank.name!);
const provideLink = !props.noLink && props.onTankSelect === undefined;
const name = unwrap(props.tank.name!);
const color: TextProps["color"] =
tank.type === TankType.TANK_TYPE_COLLECTOR
props.tank.type === TankType.TANK_TYPE_COLLECTOR
? "blue"
: tank.type === TankType.TANK_TYPE_PREMIUM
: props.tank.type === TankType.TANK_TYPE_PREMIUM
? "amber"
: "gray";
const lowContrast = tank.type !== TankType.TANK_TYPE_RESEARCHABLE;
const lowContrast = props.tank.type !== TankType.TANK_TYPE_RESEARCHABLE;
const Icon = classIcons[tank.class];
const Icon = classIcons[props.tank.class];
const content = (
<Flex
className={styles.wrapper}
data-compact={compact}
data-compact={props.compact}
column
gap="3"
onClick={() => {
onSelect?.(tank);
props.onTankSelect?.(props.tank);
TankopediaPersistent.mutate((draft) => {
draft.recentlyViewed = uniq([tank.id, ...draft.recentlyViewed])
draft.recentlyViewed = uniq([props.tank.id, ...draft.recentlyViewed])
.filter((id) => id in tankDefinitions.tanks)
.slice(0, MAX_RECENTLY_VIEWED);
});
@@ -64,13 +64,13 @@ export const TankCard = ({
style={{
backgroundImage: `url(${alias(
"api",
`/flags/scratched/${tank.nation}.webp`,
`/flags/scratched/${props.tank.nation}.webp`,
)})`,
}}
>
<img
alt={name}
src={alias("api", `/tanks/${tank.id}/icons/big.webp`)}
src={alias("api", `/tanks/${props.tank.id}/icons/big.webp`)}
/>
</Flex>
@@ -79,9 +79,9 @@ export const TankCard = ({
<span className={styles.text}>{name}</span>
</Text>
{discriminator && (
{props.discriminator && (
<Text lowContrast align="center">
{discriminator}
{props.discriminator}
</Text>
)}
</Flex>
@@ -89,7 +89,7 @@ export const TankCard = ({
if (provideLink) {
return (
<LinkI18nWrapper locale={locale} href={`/tanks/${tank.slug}`}>
<LinkI18nWrapper locale={locale} href={`/tanks/${props.tank.slug}`}>
{content}
</LinkI18nWrapper>
);
@@ -1,7 +1,43 @@
.icon {
/* height="5rem" */
}
.wrapper {
--image-height: 6rem;
.name {
/* height="1em" */
&[data-compact="true"] {
--image-height: 4rem;
.image-wrapper {
background-size: auto 75%;
}
}
.image-wrapper {
overflow: hidden;
box-sizing: border-box;
background-size: auto 100%;
background-repeat: no-repeat;
height: var(--image-height);
}
.name {
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
.icon {
width: 1em;
height: 1em;
min-width: 1em;
min-height: 1em;
}
.text {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
}
}
@@ -1,27 +1,17 @@
import { memo, useRef } from "react";
import { useIntersection } from "../../hooks/useIntersection";
import { Flex } from "../Flex";
import { InlineSkeleton } from "../InlineSkeleton";
import { Skeleton } from "../Skeleton";
import styles from "./index.module.css";
interface SkeletonTankCardProps {
onIntersection?: () => void;
}
type TankCardProps = {
compact?: boolean;
};
export const TankCardSkeleton = memo(
({ onIntersection }: SkeletonTankCardProps) => {
const card = useRef<HTMLDivElement>(null!);
useIntersection(() => onIntersection?.(), card, {
disabled: onIntersection === undefined,
});
return (
<Flex column gap="2" ref={card}>
<Skeleton className={styles.icon} />
<Skeleton className={styles.name} />
</Flex>
);
},
() => true,
);
export const TankCardSkeleton = ({ compact }: TankCardProps) => {
return (
<Flex className={styles.wrapper} data-compact={compact} column gap="3">
<Skeleton className={styles["image-wrapper"]} />
<InlineSkeleton className={styles.name} />
</Flex>
);
};
@@ -8,8 +8,8 @@ import {
} from "@blitzkit/core";
import { literals } from "@blitzkit/i18n/src/literals";
import fuzzysort from "fuzzysort";
import { times, uniq } from "lodash-es";
import { memo, useEffect, useMemo, useState } from "react";
import { uniq } from "lodash-es";
import { memo, useMemo } from "react";
import usePromise from "react-promise-suspense";
import { awaitableTankNames } from "../../core/awaitables/tankNames";
import { api } from "../../core/blitzkit/api";
@@ -25,15 +25,15 @@ import type { MaybeSkeletonComponentProps } from "../../types/maybeSkeletonCompo
import { Callout } from "../Callout";
import { ExperimentIcon } from "../ExperimentIcon";
import { Flex } from "../Flex";
import { IncrementalLoader } from "../IncrementalLoader";
import { Link } from "../Link";
import { RecentlyViewedTanks } from "../RecentlyViewedTanks";
import { TankCardSkeleton } from "../TankCardSkeleton";
import { TankCard } from "../TankCard";
import { TankCardWrapper } from "../TankCardWrapper";
import { TankSearchBar } from "../TankSearchBar";
import { TankSearchFilters } from "../TankSearchFilters";
import { TankSearchNoResults } from "../TankSearchNoResults";
import { Text } from "../Text";
import { TankSearchCard } from "./components/Card";
import { MAX_RECENTLY_VIEWED } from "./constants";
import styles from "./index.module.css";
@@ -43,9 +43,6 @@ export type TankSearchProps = MaybeSkeletonComponentProps & {
onSelectAll?: (tanks: TankDefinition[]) => void;
};
const PREVIEW_COUNT = 20;
const DEFAULT_LOADED_CARDS = 64;
const [gameDefinitions, modelDefinitions, tankDefinitions, tankNames] =
await Promise.all([
api.gameDefinitions(),
@@ -420,18 +417,17 @@ export const TankSearch = memo<TankSearchProps>(
}
}, [tankFilters, tankopediaSort]);
const [loadedCards, setLoadedCards] = useState(DEFAULT_LOADED_CARDS);
const tanks = sorted.slice(0, loadedCards);
useEffect(() => {
setLoadedCards(DEFAULT_LOADED_CARDS);
}, [tankFilters, tankopediaSort]);
const data = sorted.map((tank) => ({
key: `${tank.id}`,
tank,
onTankSelect: onSelect,
}));
return (
<Flex className={styles.container} column gap="4">
<TankSearchBar
skeleton={skeleton}
topResult={tanks?.[0]}
topResult={sorted[0]}
onSelect={onSelect}
/>
@@ -439,7 +435,7 @@ export const TankSearch = memo<TankSearchProps>(
{!skeleton && !compact && <RecentlyViewedTanks />}
{tanks.length > 0 && (
{sorted.length > 0 && (
<Flex justify="center" className={styles.label}>
<div className={styles.count}>
<Text lowContrast>
@@ -497,38 +493,18 @@ export const TankSearch = memo<TankSearchProps>(
</div>
)}
{!skeleton && (
<>
{tanks.length > 0 && (
<TankCardWrapper>
{tanks.map((tank) => (
<TankSearchCard
tank={tank}
key={tank.id}
onSelect={onSelect}
/>
))}
<TankCardWrapper>
<IncrementalLoader
skeleton={skeleton}
initial={5 * 5}
intermediate={5 * 2}
data={data}
Component={TankCard}
/>
</TankCardWrapper>
{times(
Math.min(PREVIEW_COUNT, sorted.length - loadedCards),
(index) => {
return (
<TankCardSkeleton
key={index}
onIntersection={() => {
setLoadedCards((state) =>
Math.min(state + 2, sorted.length),
);
}}
/>
);
},
)}
</TankCardWrapper>
)}
{tanks.length === 0 && <TankSearchNoResults type="search" />}
</>
{!skeleton && sorted.length === 0 && (
<TankSearchNoResults type="search" />
)}
</Flex>
);
@@ -1,6 +1,11 @@
import type { TankDefinition } from "@blitzkit/core";
import { MagnifyingGlassIcon } from "@radix-ui/react-icons";
import { type KeyboardEventHandler, useCallback, useRef } from "react";
import {
type KeyboardEventHandler,
useCallback,
useEffect,
useRef,
} from "react";
import { useLocale } from "../../hooks/useLocale";
import { TankFilters } from "../../stores/tankFilters";
import type { MaybeSkeletonComponentProps } from "../../types/maybeSkeletonComponentProps";
@@ -19,8 +24,16 @@ export function TankSearchBar({
onSelect,
}: SearchBarProps) {
const { strings } = useLocale();
const search = TankFilters.use((state) => state.search);
const input = useRef<HTMLInputElement>(null);
useEffect(() => {
if (search !== null) return;
input.current!.value = "";
}, [search]);
const handleChange = useCallback(() => {
if (!input.current) return;
@@ -23,3 +23,7 @@
.overflow-plus {
/* ml="1" */
}
.shell-icon {
display: contents;
}
@@ -613,7 +613,7 @@ function TypeFilter() {
function ShellFilter() {
return (
<Flex style={{}}>
<Flex>
<IndividualShellFilter index={0} />
<IndividualShellFilter index={1} premium />
<IndividualShellFilter index={2} />
@@ -635,19 +635,14 @@ function IndividualShellFilter({
<DropdownMenu.Root modal={false}>
<DropdownMenu.Trigger asChild>
<IconButton
listChild
size="minor"
variant="surface"
style={{
borderTopLeftRadius: index === 0 ? undefined : 0,
borderBottomLeftRadius: index === 0 ? undefined : 0,
borderTopRightRadius: index === 2 ? undefined : 0,
borderBottomRightRadius: index === 2 ? undefined : 0,
}}
color="gray"
highContrast
>
{shells[index] === null && (
<Text color="gray" style={{ display: "contents" }}>
<Text lowContrast className={styles["shell-icon"]}>
<MissingShellIcon width="1em" height="1em" />
</Text>
)}
@@ -2,6 +2,7 @@
import { ArrowLeftIcon } from "@radix-ui/react-icons";
import type { Tool } from "../constants/tools";
import { getStrings } from "../core/i18n/getStrings";
import { Logo } from "../icons/Logo";
import { Back } from "./Back";
import { Flex } from "./Flex";
import { Heading } from "./Heading";
@@ -37,6 +38,8 @@ const toolStrings =
backdrop-filter: var(--blur-tint);
overflow: hidden;
:root.light & {
backdrop-filter: var(--blur-tint) invert(1) hue-rotate(180deg);
}
@@ -51,13 +54,23 @@ const toolStrings =
flex: 1;
max-width: var(--page-width);
display: flex;
flex-direction: column;
.return {
width: 0.75em;
height: 0.75em;
}
.logo {
position: absolute;
pointer-events: none;
right: 0;
top: 50%;
height: 100%;
transform: translateY(-50%) scale(10);
color: var(--black-a4);
}
}
.pattern {
@@ -110,6 +123,8 @@ const toolStrings =
{toolStrings.name}
</Flex>
</Heading>
<Logo className="logo" />
</div>
</div>
</div>
+19
View File
@@ -0,0 +1,19 @@
import type { ComponentProps } from "react";
export function Logo(props: ComponentProps<"svg">) {
return (
<svg
width="128"
height="128"
viewBox="0 0 128 128"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M48.4082 19.8652C56.6277 19.8652 62.3045 21.5665 65.4375 24.9688C67.7005 27.3762 68.8875 30.4035 69 34.0498L52.1045 58.1982L61.252 68.375L30.8174 120.845L32.3818 93.4385H0L12.9863 19.8652H48.4082ZM83.584 58.1318L103.038 35.7969H128L99.2139 65.7285L117.691 109.37H90.4053L81.2812 84.373L78.4297 87.3389L74.5391 109.37H50.0312L52.4111 95.8789L71.4209 68.375L62.2734 58.1982L92.6064 7L83.584 58.1318ZM26.5791 77.875H32.9961C35.3204 77.875 37.1735 77.1677 38.5547 75.7529C39.9358 74.3381 40.8292 72.485 41.2334 70.1943C41.6376 67.9037 41.4012 66.0505 40.5254 64.6357C39.6495 63.1876 38.0496 62.463 35.7256 62.4629H29.3076L26.5791 77.875ZM31.4297 50.4365H37.1904C39.3126 50.4365 40.9635 49.8137 42.1426 48.5674C43.3553 47.2873 44.1641 45.5016 44.5684 43.2109C44.9726 40.954 44.8036 39.2186 44.0625 38.0059C43.3213 36.7598 41.8725 36.1367 39.7168 36.1367H33.957L31.4297 50.4365Z"
fill="currentColor"
/>
</svg>
);
}