feat: incremtal loading

This commit is contained in:
TrèsAbhi committed 2026-01-14 12:34:28 -06:00
1 parent 11e87bd519
commit de6137d1a5
5 files changed
+98 -10

No files matched your search

@@ -1,23 +1,26 @@
import { useGameStrings } from "../../hooks/useGameStrings";
import type { Avatar } from "../../protos/avatar";
import type { SkeletonProps } from "../../types/SkeletonProps";
import { Text } from "../Text";
import "./index.css";
interface Props {
avatar: Avatar;
}
export function AvatarCard({ avatar }: Props) {
export function AvatarCard(props: SkeletonProps<Avatar>) {
const profileAvatarEntityStrings = useGameStrings("ProfileAvatarEntity");
return (
<div className="avatar-card">
<div
className="avatar-image"
style={{ backgroundImage: `url(/api/avatars/${avatar.name}.webp)` }}
style={{
backgroundImage: props.skeleton
? undefined
: `url(/api/avatars/${props.name}.webp)`,
}}
/>
<Text align="center" className="avatar-label">
{profileAvatarEntityStrings[avatar.stuff_ui!.display_name]}
{props.skeleton
? "LOADING"
: profileAvatarEntityStrings[props.stuff_ui!.display_name]}
</Text>
</div>
);
@@ -0,0 +1,2 @@
.incremental-loader {
}
@@ -0,0 +1,76 @@
import { times } from "lodash-es";
import { useEffect, useRef, useState, type ReactNode } from "react";
import type { SkeletonProps } from "../../types/SkeletonProps";
import "./index.css";
export interface IncrementalLoaderProps<Props> {
initial: number;
skeleton: number;
rate?: number;
data: Props[];
Component: (props: SkeletonProps<Props>) => ReactNode;
}
export function IncrementalLoader<Props>({
initial,
rate = 2,
data,
skeleton,
Component,
}: IncrementalLoaderProps<Props>) {
const [count, setCount] = useState(initial);
console.log(count);
const sliced = data.slice(0, count);
return (
<>
{sliced.map((item, index) => (
<Component key={index} {...item} />
))}
{times(Math.min(skeleton, 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,5 +1,6 @@
import { useMemo } from "react";
import { AvatarCard } from "../../../components/AvatarCard";
import { IncrementalLoader } from "../../../components/IncrementalLoader";
import { api } from "../../../core/api/dynamic";
import { useAwait } from "../../../hooks/useAwait";
import { useGameStrings } from "../../../hooks/useGameStrings";
@@ -41,9 +42,12 @@ function Content({ skeleton }: ContentProps) {
return (
<div className="avatars">
{ordered.slice(0, 12).map((avatar) => (
<AvatarCard key={avatar.name} avatar={avatar} />
))}
<IncrementalLoader
initial={20}
skeleton={6}
data={ordered}
Component={AvatarCard}
/>
</div>
);
}
@@ -0,0 +1,3 @@
export type SkeletonProps<Props> =
| { skeleton: true }
| (Props & { skeleton?: false });