From c0c6a21e43f253d951a3b8dbdb835a8d63ab72c3 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 13:58:53 -0600 Subject: [PATCH] feat: groups --- .../src/components/AvatarCard/index.css | 15 ++++-- .../src/components/AvatarCard/index.tsx | 51 ++++++++++++++----- .../components/IncrementalLoader/index.tsx | 24 +++++---- .../src/pages/[...locale]/avatars/_index.tsx | 43 +++++++++++----- 4 files changed, 91 insertions(+), 42 deletions(-) diff --git a/packages/website-ue/src/components/AvatarCard/index.css b/packages/website-ue/src/components/AvatarCard/index.css index 2b21c4f4b..6ce90f706 100644 --- a/packages/website-ue/src/components/AvatarCard/index.css +++ b/packages/website-ue/src/components/AvatarCard/index.css @@ -4,15 +4,20 @@ align-items: center; gap: var(--space-4); - .avatar-image { + .avatar-series { + position: relative; width: 100%; aspect-ratio: 7 / 8; - border-radius: var(--space-3); - background-size: cover; - background-position: center; + .avatar-entry { + position: absolute; - box-shadow: var(--shadow-interactive), var(--shadow-aggressive); + border-radius: var(--space-3); + box-shadow: var(--shadow-interactive), var(--shadow-aggressive); + + background-size: cover; + background-position: center; + } } .avatar-label { diff --git a/packages/website-ue/src/components/AvatarCard/index.tsx b/packages/website-ue/src/components/AvatarCard/index.tsx index b0cd9ba76..78a8dc99c 100644 --- a/packages/website-ue/src/components/AvatarCard/index.tsx +++ b/packages/website-ue/src/components/AvatarCard/index.tsx @@ -1,27 +1,50 @@ -import { useGameStrings } from "../../hooks/useGameStrings"; import type { Avatar } from "../../protos/avatar"; import type { SkeletonProps } from "../../types/SkeletonProps"; import { Text } from "../Text"; import "./index.css"; -export function AvatarCard(props: SkeletonProps) { - const profileAvatarEntityStrings = useGameStrings("ProfileAvatarEntity"); +interface Props { + name: string; + avatars: Avatar[]; +} +const MAX_CARDS = 3; +const CARD_TRIM = "var(--space-3)"; + +export function AvatarCard(props: SkeletonProps) { return (
-
+ {!props.skeleton && } + - {props.skeleton - ? "LOADING" - : profileAvatarEntityStrings[props.stuff_ui!.display_name]} + {props.skeleton ? "LOADING" : props.name}
); } + +interface SeriesProps { + avatars: Avatar[]; +} + +function Series({ avatars }: SeriesProps) { + const count = Math.min(avatars.length, MAX_CARDS); + + return ( +
+ {avatars.slice(-MAX_CARDS).map((avatar, index) => ( +
+ ))} +
+ ); +} diff --git a/packages/website-ue/src/components/IncrementalLoader/index.tsx b/packages/website-ue/src/components/IncrementalLoader/index.tsx index 0efe7e8c7..6a203ed97 100644 --- a/packages/website-ue/src/components/IncrementalLoader/index.tsx +++ b/packages/website-ue/src/components/IncrementalLoader/index.tsx @@ -5,9 +5,10 @@ import "./index.css"; export interface IncrementalLoaderProps { initial: number; - skeleton: number; + intermediate: number; rate?: number; + skeleton?: boolean; data: Props[]; Component: (props: SkeletonProps) => ReactNode; } @@ -17,6 +18,7 @@ export function IncrementalLoader({ rate = 2, data, skeleton, + intermediate, Component, }: IncrementalLoaderProps) { const [count, setCount] = useState(initial); @@ -24,15 +26,19 @@ export function IncrementalLoader({ return ( <> - {sliced.map((item, index) => ( - - ))} + {!skeleton && + sliced.map((item, index) => )} - {times(Math.min(skeleton, data.length - count), (index) => ( - - - - ))} + {times( + skeleton + ? initial + intermediate + : Math.min(intermediate, data.length - count), + (index) => ( + + + + ) + )} ); } diff --git a/packages/website-ue/src/pages/[...locale]/avatars/_index.tsx b/packages/website-ue/src/pages/[...locale]/avatars/_index.tsx index b5f1dbab2..6db84ad04 100644 --- a/packages/website-ue/src/pages/[...locale]/avatars/_index.tsx +++ b/packages/website-ue/src/pages/[...locale]/avatars/_index.tsx @@ -5,6 +5,7 @@ import { api } from "../../../core/api/dynamic"; import { useAwait } from "../../../hooks/useAwait"; import { useGameStrings } from "../../../hooks/useGameStrings"; import { LocaleProvider, useLocale } from "../../../hooks/useLocale"; +import type { Avatar } from "../../../protos/avatar"; interface PageProps extends ContentProps { locale: string; @@ -26,26 +27,40 @@ function Content({ skeleton }: ContentProps) { const locale = useLocale(); const { avatars } = useAwait(() => api.avatars(), "avatars"); const profileAvatarEntityStrings = useGameStrings("ProfileAvatarEntity"); - const ordered = useMemo( - () => - avatars.sort((a, b) => { - const nameA = - profileAvatarEntityStrings[a.stuff_ui!.display_name] ?? - a.stuff_ui!.display_name; - const nameB = - profileAvatarEntityStrings[b.stuff_ui!.display_name] ?? - b.stuff_ui!.display_name; + const groups = useMemo(() => { + const groups = new Map(); - return nameA.localeCompare(nameB, locale); - }), - [] - ); + for (const avatar of avatars) { + const name = + profileAvatarEntityStrings[avatar.stuff_ui!.display_name] ?? + avatar.stuff_ui!.display_name; + + if (groups.has(name)) { + groups.get(name)!.push(avatar); + } else { + groups.set(name, [avatar]); + } + } + + return groups; + }, []); + const ordered = useMemo(() => { + const array = Array.from(groups.entries()); + return array + .sort((a, b) => a[0].localeCompare(b[0], locale)) + .map(([name, avatars]) => ({ + name, + avatars: avatars.sort((a, b) => { + return a.stuff_ui!.grade - b.stuff_ui!.grade; + }), + })); + }, []); return (