diff --git a/packages/website/src/components/Back/index.tsx b/packages/website/src/components/Back/index.tsx
index 8d7f985ac..4397dcfe0 100644
--- a/packages/website/src/components/Back/index.tsx
+++ b/packages/website/src/components/Back/index.tsx
@@ -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 (
& {
+ 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);
}
}
diff --git a/packages/website/src/components/Button/index.tsx b/packages/website/src/components/Button/index.tsx
index fd14bec73..5a87e60bd 100644
--- a/packages/website/src/components/Button/index.tsx
+++ b/packages/website/src/components/Button/index.tsx
@@ -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}
/>
);
diff --git a/packages/website/src/components/IncrementalLoader.tsx b/packages/website/src/components/IncrementalLoader.tsx
new file mode 100644
index 000000000..64ebafa18
--- /dev/null
+++ b/packages/website/src/components/IncrementalLoader.tsx
@@ -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
{
+ initial: number;
+ intermediate: number;
+ rate?: number;
+
+ skeleton?: boolean;
+ data: Props[];
+ Component: (
+ props: MaybeSkeletonComponentProps>,
+ ) => ReactNode;
+}
+
+export function IncrementalLoader({
+ initial,
+ rate = 2,
+ data,
+ skeleton,
+ intermediate,
+ Component,
+}: IncrementalLoaderProps) {
+ const [count, setCount] = useState(initial);
+ const sliced = data.slice(0, count);
+
+ useEffect(() => setCount(initial), [data, initial]);
+
+ return (
+ <>
+ {!skeleton &&
+ sliced.map(({ key, ...props }) => )}
+
+ {times(
+ skeleton
+ ? initial + intermediate
+ : Math.min(intermediate, data.length - count),
+ (index) => (
+ -
+
+
+ ),
+ )}
+ >
+ );
+}
+
+interface ItemProps {
+ children: ReactNode;
+ rate: number;
+ setCount: (setter: (state: number) => number) => void;
+}
+
+function Item({ setCount, rate, children }: ItemProps) {
+ const wrapper = useRef(null);
+ const observer = useRef(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 (
+
+ {children}
+
+ );
+}
diff --git a/packages/website/src/components/InlineSkeleton/index.css b/packages/website/src/components/InlineSkeleton/index.module.css
similarity index 75%
rename from packages/website/src/components/InlineSkeleton/index.css
rename to packages/website/src/components/InlineSkeleton/index.module.css
index 593636058..1f30aeffb 100644
--- a/packages/website/src/components/InlineSkeleton/index.css
+++ b/packages/website/src/components/InlineSkeleton/index.module.css
@@ -1,3 +1,4 @@
.inline-skeleton {
display: inline-block;
+ height: 1em;
}
diff --git a/packages/website/src/components/InlineSkeleton/index.tsx b/packages/website/src/components/InlineSkeleton/index.tsx
index d825cff16..57f65e4c1 100644
--- a/packages/website/src/components/InlineSkeleton/index.tsx
+++ b/packages/website/src/components/InlineSkeleton/index.tsx
@@ -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 (
);
diff --git a/packages/website/src/components/TankCard/index.tsx b/packages/website/src/components/TankCard/index.tsx
index 5c84977fd..f47d0e420 100644
--- a/packages/website/src/components/TankCard/index.tsx
+++ b/packages/website/src/components/TankCard/index.tsx
@@ -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) => {
+ if (props.skeleton) {
+ return ;
+ }
+
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 = (
{
- 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`,
)})`,
}}
>
@@ -79,9 +79,9 @@ export const TankCard = ({
{name}
- {discriminator && (
+ {props.discriminator && (
- {discriminator}
+ {props.discriminator}
)}
@@ -89,7 +89,7 @@ export const TankCard = ({
if (provideLink) {
return (
-
+
{content}
);
diff --git a/packages/website/src/components/TankCardSkeleton/index.module.css b/packages/website/src/components/TankCardSkeleton/index.module.css
index 44510e73e..bfac648ec 100644
--- a/packages/website/src/components/TankCardSkeleton/index.module.css
+++ b/packages/website/src/components/TankCardSkeleton/index.module.css
@@ -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;
+ }
+ }
}
diff --git a/packages/website/src/components/TankCardSkeleton/index.tsx b/packages/website/src/components/TankCardSkeleton/index.tsx
index 76559276a..1aa329156 100644
--- a/packages/website/src/components/TankCardSkeleton/index.tsx
+++ b/packages/website/src/components/TankCardSkeleton/index.tsx
@@ -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(null!);
-
- useIntersection(() => onIntersection?.(), card, {
- disabled: onIntersection === undefined,
- });
-
- return (
-
-
-
-
- );
- },
- () => true,
-);
+export const TankCardSkeleton = ({ compact }: TankCardProps) => {
+ return (
+
+
+
+
+ );
+};
diff --git a/packages/website/src/components/TankSearch/index.tsx b/packages/website/src/components/TankSearch/index.tsx
index e31bfa224..ab8360eeb 100644
--- a/packages/website/src/components/TankSearch/index.tsx
+++ b/packages/website/src/components/TankSearch/index.tsx
@@ -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(
}
}, [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 (
@@ -439,7 +435,7 @@ export const TankSearch = memo(
{!skeleton && !compact && }
- {tanks.length > 0 && (
+ {sorted.length > 0 && (
@@ -497,38 +493,18 @@ export const TankSearch = memo(
)}
- {!skeleton && (
- <>
- {tanks.length > 0 && (
-
- {tanks.map((tank) => (
-
- ))}
+
+
+
- {times(
- Math.min(PREVIEW_COUNT, sorted.length - loadedCards),
- (index) => {
- return (
- {
- setLoadedCards((state) =>
- Math.min(state + 2, sorted.length),
- );
- }}
- />
- );
- },
- )}
-
- )}
-
- {tanks.length === 0 && }
- >
+ {!skeleton && sorted.length === 0 && (
+
)}
);
diff --git a/packages/website/src/components/TankSearchBar/index.tsx b/packages/website/src/components/TankSearchBar/index.tsx
index a96cb3145..5a4811c7c 100644
--- a/packages/website/src/components/TankSearchBar/index.tsx
+++ b/packages/website/src/components/TankSearchBar/index.tsx
@@ -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(null);
+
+ useEffect(() => {
+ if (search !== null) return;
+ input.current!.value = "";
+ }, [search]);
+
const handleChange = useCallback(() => {
if (!input.current) return;
diff --git a/packages/website/src/components/TankSearchFilters/index.module.css b/packages/website/src/components/TankSearchFilters/index.module.css
index 19d1b8d9d..c0249ae3b 100644
--- a/packages/website/src/components/TankSearchFilters/index.module.css
+++ b/packages/website/src/components/TankSearchFilters/index.module.css
@@ -23,3 +23,7 @@
.overflow-plus {
/* ml="1" */
}
+
+.shell-icon {
+ display: contents;
+}
diff --git a/packages/website/src/components/TankSearchFilters/index.tsx b/packages/website/src/components/TankSearchFilters/index.tsx
index bf4b9a7cc..978ca803f 100644
--- a/packages/website/src/components/TankSearchFilters/index.tsx
+++ b/packages/website/src/components/TankSearchFilters/index.tsx
@@ -613,7 +613,7 @@ function TypeFilter() {
function ShellFilter() {
return (
-
+
@@ -635,19 +635,14 @@ function IndividualShellFilter({
{shells[index] === null && (
-
+
)}
diff --git a/packages/website/src/components/ToolBanner.astro b/packages/website/src/components/ToolBanner.astro
index 8d7dacd83..2e7b3cded 100644
--- a/packages/website/src/components/ToolBanner.astro
+++ b/packages/website/src/components/ToolBanner.astro
@@ -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}
+
+
diff --git a/packages/website/src/icons/Logo.tsx b/packages/website/src/icons/Logo.tsx
new file mode 100644
index 000000000..f99be60e5
--- /dev/null
+++ b/packages/website/src/icons/Logo.tsx
@@ -0,0 +1,19 @@
+import type { ComponentProps } from "react";
+
+export function Logo(props: ComponentProps<"svg">) {
+ return (
+