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>