feat: groups

This commit is contained in:
TrèsAbhi committed 2026-01-14 13:58:53 -06:00
1 parent a7f93698a6
commit c0c6a21e43
4 files changed
+91 -42

No files matched your search

@@ -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 {
@@ -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<Avatar>) {
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<Props>) {
return (
<div className="avatar-card">
<div
className="avatar-image"
style={{
backgroundImage: props.skeleton
? undefined
: `url(/api/avatars/${props.name}.webp)`,
}}
/>
{!props.skeleton && <Series avatars={props.avatars} />}
<Text align="center" className="avatar-label">
{props.skeleton
? "LOADING"
: profileAvatarEntityStrings[props.stuff_ui!.display_name]}
{props.skeleton ? "LOADING" : props.name}
</Text>
</div>
);
}
interface SeriesProps {
avatars: Avatar[];
}
function Series({ avatars }: SeriesProps) {
const count = Math.min(avatars.length, MAX_CARDS);
return (
<div className="avatar-series">
{avatars.slice(-MAX_CARDS).map((avatar, index) => (
<div
className="avatar-entry"
style={{
filter: `brightness(${(index + 1) / count})`,
top: `calc(${index} * ${CARD_TRIM})`,
left: `calc(${index} * ${CARD_TRIM})`,
width: `calc(100% - ${CARD_TRIM} * ${count - 1})`,
height: `calc(100% - ${CARD_TRIM} * ${count - 1})`,
backgroundImage: `url(/api/avatars/${avatar.name}.webp)`,
}}
/>
))}
</div>
);
}
@@ -5,9 +5,10 @@ import "./index.css";
export interface IncrementalLoaderProps<Props> {
initial: number;
skeleton: number;
intermediate: number;
rate?: number;
skeleton?: boolean;
data: Props[];
Component: (props: SkeletonProps<Props>) => ReactNode;
}
@@ -17,6 +18,7 @@ export function IncrementalLoader<Props>({
rate = 2,
data,
skeleton,
intermediate,
Component,
}: IncrementalLoaderProps<Props>) {
const [count, setCount] = useState(initial);
@@ -24,15 +26,19 @@ export function IncrementalLoader<Props>({
return (
<>
{sliced.map((item, index) => (
<Component key={index} {...item} />
))}
{!skeleton &&
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>
))}
{times(
skeleton
? initial + intermediate
: Math.min(intermediate, data.length - count),
(index) => (
<Item key={index} rate={rate} setCount={setCount}>
<Component skeleton />
</Item>
)
)}
</>
);
}
@@ -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<string, Avatar[]>();
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 (
<div className="avatars">
<IncrementalLoader
initial={40}
skeleton={10}
intermediate={10}
data={ordered}
Component={AvatarCard}
/>