Files
blitzkit/packages/website-ue/src/components/AvatarCard/index.tsx
T

153 lines
4.3 KiB
TypeScript
Raw Normal View History

2026-01-15 12:55:18 -06:00
import { literals } from "@blitzkit/i18n";
2026-01-25 15:55:30 -06:00
import { Grade } from "@protos/blitz_static_standard_grades_enum";
2026-01-18 14:12:20 -06:00
import { DownloadIcon, StarFilledIcon } from "@radix-ui/react-icons";
2026-01-17 13:50:53 -06:00
import type { Color } from "../../core/ui/color";
2026-01-17 13:36:17 -06:00
import { useGameStrings } from "../../hooks/useGameStrings";
2026-01-15 12:55:18 -06:00
import { useStrings } from "../../hooks/useStrings";
2026-01-12 14:13:21 -06:00
import type { Avatar } from "../../protos/avatar";
2026-01-16 20:37:11 -06:00
import type { PropsWithSkeleton } from "../../types/propsWithSkeleton";
2026-01-15 12:55:18 -06:00
import { Badge } from "../Badge";
2026-01-18 14:12:20 -06:00
import { Button } from "../Button";
2026-01-17 13:36:17 -06:00
import { Dialog } from "../Dialog";
2026-01-16 20:37:11 -06:00
import { InlineSkeleton } from "../InlineSkeleton";
2026-01-18 14:12:20 -06:00
import { Link } from "../Link";
2026-01-16 20:37:11 -06:00
import { Skeleton } from "../Skeleton";
2026-01-12 14:13:21 -06:00
import { Text } from "../Text";
2026-01-17 13:36:17 -06:00
import styles from "./index.module.css";
2026-01-12 14:13:21 -06:00
2026-01-14 13:58:53 -06:00
interface Props {
name: string;
avatars: Avatar[];
}
2026-01-13 13:30:09 -06:00
2026-01-14 13:58:53 -06:00
const MAX_CARDS = 3;
const CARD_TRIM = "var(--space-3)";
2026-01-16 20:37:11 -06:00
export function AvatarCard(props: PropsWithSkeleton<Props>) {
2026-01-17 13:36:17 -06:00
const card = (
<div className={styles.card}>
2026-01-16 20:37:11 -06:00
<Series skeleton={props.skeleton} {...props} />
2026-01-14 13:58:53 -06:00
2026-01-17 13:36:17 -06:00
<Text align="center" className={styles.label}>
2026-01-16 20:37:11 -06:00
{props.skeleton ? <InlineSkeleton /> : props.name}
2026-01-13 13:30:09 -06:00
</Text>
2026-01-12 14:13:21 -06:00
</div>
);
2026-01-17 13:36:17 -06:00
if (props.skeleton) return card;
return (
<Dialog.Root>
<Dialog.Trigger>{card}</Dialog.Trigger>
<Dialog.Content>
<Text size="major" weight="bold">
{props.name}
</Text>
2026-01-17 13:50:53 -06:00
<Details avatars={props.avatars} />
2026-01-17 13:36:17 -06:00
</Dialog.Content>
</Dialog.Root>
);
2026-01-12 14:13:21 -06:00
}
2026-01-14 13:58:53 -06:00
2026-01-17 13:50:53 -06:00
interface DetailsProps {
avatars: Avatar[];
}
const gradeColors: Record<Grade, Color> = {
2026-01-25 15:55:30 -06:00
[Grade.GRADE_UNSPECIFIED]: "gray",
[Grade.GRADE_COMMON]: "gray",
[Grade.GRADE_RARE]: "blue",
[Grade.GRADE_EPIC]: "purple",
[Grade.GRADE_LEGENDARY]: "amber",
2026-01-17 13:50:53 -06:00
};
function Details({ avatars }: DetailsProps) {
const gameStrings = useGameStrings("ProfileAvatarEntity");
const strings = useStrings();
return [...avatars].reverse().map((avatar) => {
const description = gameStrings[avatar.stuff_ui!.description];
const obtaining = gameStrings[avatar.stuff_ui!.obtaining_methods];
const hasDescription = description || obtaining;
return (
<div className={styles.details}>
<div className={styles.preview} data-grade={avatar.stuff_ui!.grade}>
<div
className={styles.image}
style={{
backgroundImage: `url(/api/avatars/${avatar.name}.webp)`,
}}
/>
<Badge
color={gradeColors[avatar.stuff_ui!.grade]}
className={styles.grade}
>
<StarFilledIcon />
{strings.grades[avatar.stuff_ui!.grade]}
</Badge>
</div>
<div className={styles.description}>
<Text>
{hasDescription ? (
<>
{description} {obtaining}
</>
) : (
strings.avatars.no_description
)}
</Text>
2026-01-18 14:12:20 -06:00
<Link
underline="never"
href={`/api/avatars/${avatar.name}.webp`}
download
>
<Button>
<DownloadIcon /> {strings.avatars.download}
</Button>
</Link>
2026-01-17 13:50:53 -06:00
</div>
</div>
);
});
}
2026-01-14 13:58:53 -06:00
interface SeriesProps {
avatars: Avatar[];
}
2026-01-16 20:37:11 -06:00
function Series(props: PropsWithSkeleton<SeriesProps>) {
const count = Math.min(props.skeleton ? 0 : props.avatars.length, MAX_CARDS);
2026-01-15 12:55:18 -06:00
const strings = useStrings();
2026-01-14 13:58:53 -06:00
return (
2026-01-17 13:36:17 -06:00
<div className={styles.series}>
2026-01-16 20:37:11 -06:00
{!props.skeleton &&
props.avatars.slice(-MAX_CARDS).map((avatar, index) => (
<div
key={avatar.name}
2026-01-17 13:36:17 -06:00
className={styles.entry}
2026-01-16 20:37:11 -06:00
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)`,
}}
/>
))}
2026-01-15 12:55:18 -06:00
2026-01-16 20:37:11 -06:00
{!props.skeleton && props.avatars.length > 1 && (
2026-01-17 13:36:17 -06:00
<Badge color="gray" highContrast className={styles.count}>
2026-01-16 20:37:11 -06:00
{literals(strings.units.x, { value: props.avatars.length })}
2026-01-15 12:55:18 -06:00
</Badge>
)}
2026-01-16 20:37:11 -06:00
2026-01-17 13:36:17 -06:00
{props.skeleton && <Skeleton className={styles.entry} />}
2026-01-14 13:58:53 -06:00
</div>
);
}