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`, )})`, }} > {name} @@ -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 ( + + + + ); +}