From de6137d1a52ecaa708a26634af20fcb12197a1ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Wed, 14 Jan 2026 12:34:28 -0600 Subject: [PATCH] feat: incremtal loading --- .../src/components/AvatarCard/index.tsx | 17 +++-- .../components/IncrementalLoader/index.css | 2 + .../components/IncrementalLoader/index.tsx | 76 +++++++++++++++++++ .../src/pages/[...locale]/avatars/_index.tsx | 10 ++- .../website-ue/src/types/SkeletonProps.ts | 3 + 5 files changed, 98 insertions(+), 10 deletions(-) create mode 100644 packages/website-ue/src/components/IncrementalLoader/index.css create mode 100644 packages/website-ue/src/components/IncrementalLoader/index.tsx create mode 100644 packages/website-ue/src/types/SkeletonProps.ts diff --git a/packages/website-ue/src/components/AvatarCard/index.tsx b/packages/website-ue/src/components/AvatarCard/index.tsx index 11e9d712a..b0cd9ba76 100644 --- a/packages/website-ue/src/components/AvatarCard/index.tsx +++ b/packages/website-ue/src/components/AvatarCard/index.tsx @@ -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) { const profileAvatarEntityStrings = useGameStrings("ProfileAvatarEntity"); return (
- {profileAvatarEntityStrings[avatar.stuff_ui!.display_name]} + {props.skeleton + ? "LOADING" + : profileAvatarEntityStrings[props.stuff_ui!.display_name]}
); diff --git a/packages/website-ue/src/components/IncrementalLoader/index.css b/packages/website-ue/src/components/IncrementalLoader/index.css new file mode 100644 index 000000000..4d77989f3 --- /dev/null +++ b/packages/website-ue/src/components/IncrementalLoader/index.css @@ -0,0 +1,2 @@ +.incremental-loader { +} diff --git a/packages/website-ue/src/components/IncrementalLoader/index.tsx b/packages/website-ue/src/components/IncrementalLoader/index.tsx new file mode 100644 index 000000000..8a8e2a79e --- /dev/null +++ b/packages/website-ue/src/components/IncrementalLoader/index.tsx @@ -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 { + initial: number; + skeleton: number; + rate?: number; + + data: Props[]; + Component: (props: SkeletonProps) => ReactNode; +} + +export function IncrementalLoader({ + initial, + rate = 2, + data, + skeleton, + Component, +}: IncrementalLoaderProps) { + const [count, setCount] = useState(initial); + console.log(count); + const sliced = data.slice(0, count); + + return ( + <> + {sliced.map((item, index) => ( + + ))} + + {times(Math.min(skeleton, 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-ue/src/pages/[...locale]/avatars/_index.tsx b/packages/website-ue/src/pages/[...locale]/avatars/_index.tsx index 7e3d69485..fa8612c51 100644 --- a/packages/website-ue/src/pages/[...locale]/avatars/_index.tsx +++ b/packages/website-ue/src/pages/[...locale]/avatars/_index.tsx @@ -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 (
- {ordered.slice(0, 12).map((avatar) => ( - - ))} +
); } diff --git a/packages/website-ue/src/types/SkeletonProps.ts b/packages/website-ue/src/types/SkeletonProps.ts new file mode 100644 index 000000000..466e9ce50 --- /dev/null +++ b/packages/website-ue/src/types/SkeletonProps.ts @@ -0,0 +1,3 @@ +export type SkeletonProps = + | { skeleton: true } + | (Props & { skeleton?: false });