feat: incremental loader

Co-authored-by: Copilot <[email protected]>
This commit is contained in:
TrèsAbhiandCopilot committed 2026-04-28 15:31:21 -05:00
1 parent ee1ecb83b7
commit 7b650bf39b
5 files changed
+62 -36

No files matched your search

@@ -1,12 +1,12 @@
@keyframes skeleton-animation {
0% {
background-color: var(--black-a8);
background-color: var(--black-a6);
}
50% {
background-color: var(--black-a5);
}
100% {
background-color: var(--black-a8);
background-color: var(--black-a6);
}
}
@@ -31,19 +31,17 @@
background-size: 140%;
background-position: 40% center;
.cover {
width: 100%;
height: 100%;
background: linear-gradient(transparent 75%, var(--gray-1));
}
}
}
.name {
white-space: nowrap;
overflow: hidden;
max-width: 100%;
width: 100%;
text-overflow: ellipsis;
.skeleton {
width: 100%;
}
}
}
@@ -2,7 +2,10 @@ import { api } from "../../core/api/dynamic";
import { useAwait } from "../../hooks/useAwait";
import { useGameStrings } from "../../hooks/useGameStrings";
import { useLocale } from "../../hooks/useLocale";
import type { PropsWithSkeleton } from "../../types/propsWithSkeleton";
import { InlineSkeleton } from "../InlineSkeleton";
import { LinkI18n } from "../LinkI18n";
import { Skeleton } from "../Skeleton";
import { Text } from "../Text";
import styles from "./index.module.css";
@@ -11,37 +14,46 @@ interface TankCardProps {
subtitle?: string;
}
export function TankCard({ id }: TankCardProps) {
const tank = useAwait(() => api.tank(id), `tank-${id}`);
const gameStrings = useGameStrings("TankEntity");
export function TankCard(props: PropsWithSkeleton<TankCardProps>) {
const tank = props.skeleton
? undefined
: useAwait(() => api.tank(props.id), `tank-${props.id}`);
const gameStrings = props.skeleton ? undefined : useGameStrings("TankEntity");
const locale = useLocale();
return (
<LinkI18n
href={`/tanks/${tank.slug}`}
href={props.skeleton ? undefined : `/tanks/${tank!.slug}`}
underline="never"
locale={locale}
className={styles["tank-card"]}
>
<div className={styles["icon-wrapper"]}>
<div
className={styles.flag}
style={{
backgroundImage: `url(/api/nations/${tank.tank!.nation}/card.webp)`,
}}
/>
{!props.skeleton && (
<div
className={styles.flag}
style={{
backgroundImage: `url(/api/nations/${tank!.tank!.nation}/card.webp)`,
}}
/>
)}
<div
className={styles.icon}
style={{
backgroundImage: `url(/api/tanks/${id}.webp)`,
}}
>
<div className={styles.cover} />
</div>
{!props.skeleton && (
<div
className={styles.icon}
style={{
backgroundImage: `url(/api/tanks/${props.id}.webp)`,
}}
/>
)}
{props.skeleton && <Skeleton className={styles.icon} />}
</div>
<Text className={styles.name}>{gameStrings[tank.tank!.name!.value]}</Text>
<Text className={styles.name}>
{props.skeleton && <InlineSkeleton className={styles.skeleton} />}
{!props.skeleton && gameStrings![tank!.tank!.name!.value]}
</Text>
</LinkI18n>
);
}
@@ -1,5 +1,6 @@
import fuzzysort from "fuzzysort";
import { useMemo } from "react";
import { IncrementalLoader } from "../../../components/IncrementalLoader";
import { TankCard } from "../../../components/TankCard";
import { TankCards } from "../../../components/TankCards";
import { TankSearch } from "../../../components/TankSearch";
@@ -41,7 +42,7 @@ function Content() {
compareTiers(a.tank!.tier_catalog_id, b.tank!.tier_catalog_id),
);
return list;
return list.map(({ id }) => id);
}, []);
const tanksFiltered = useMemo(() => {
if (!search) return tanksOrdered;
@@ -50,17 +51,32 @@ function Content() {
keys: ["id", "name", "deburred"],
});
return results.map(({ obj }) => obj);
return results.map(({ obj }) => obj.id);
}, [search]);
return (
<>
<TankSearch />
<TankCards>
{tanksFiltered.slice(0, 32).map(({ id }) => (
<TankCard key={id} id={id} />
))}
</TankCards>
<List tanks={tanksFiltered} />
</>
);
}
interface ListProps {
tanks: string[];
}
function List({ tanks }: ListProps) {
const data = useMemo(() => tanks.map((id) => ({ key: id, id })), [tanks]);
return (
<TankCards>
<IncrementalLoader
initial={7 * 5}
intermediate={7 * 3}
data={data}
Component={TankCard}
/>
</TankCards>
);
}
@@ -1,3 +1,3 @@
export type PropsWithSkeleton<Props> =
| { skeleton: true }
| { skeleton: true; onIntersection?: () => void }
| (Props & { skeleton?: false });