mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 11:39:50 +02:00
some cleanup
This commit is contained in:
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
@@ -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>
|
||||
Reference in new issue
Block a user