2026-06-14 11:24:04 -04:00
|
|
|
|
import React from 'react';
|
|
|
|
|
|
import { useAvatarDecoration } from '../../hooks/useAvatarDecoration';
|
|
|
|
|
|
import { decorationUrl } from '../../features/lotus/avatarDecorations';
|
|
|
|
|
|
|
|
|
|
|
|
// How far the decoration image extends beyond the avatar on each side (px).
|
|
|
|
|
|
// The APNG files are 256×256 with a transparent center. At this extension
|
|
|
|
|
|
// the center hole lines up naturally with the avatar beneath it.
|
|
|
|
|
|
const INSET = 8;
|
|
|
|
|
|
|
|
|
|
|
|
type AvatarDecorationProps = {
|
|
|
|
|
|
userId: string;
|
|
|
|
|
|
children: React.ReactNode;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export function AvatarDecoration({ userId, children }: AvatarDecorationProps) {
|
|
|
|
|
|
const slug = useAvatarDecoration(userId);
|
|
|
|
|
|
|
|
|
|
|
|
if (!slug) {
|
|
|
|
|
|
return <>{children}</>;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: 'relative',
|
|
|
|
|
|
display: 'inline-flex',
|
|
|
|
|
|
flexShrink: 0,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{children}
|
|
|
|
|
|
<img
|
|
|
|
|
|
src={decorationUrl(slug)}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
top: -INSET,
|
|
|
|
|
|
left: -INSET,
|
|
|
|
|
|
right: -INSET,
|
|
|
|
|
|
bottom: -INSET,
|
|
|
|
|
|
width: `calc(100% + ${INSET * 2}px)`,
|
|
|
|
|
|
height: `calc(100% + ${INSET * 2}px)`,
|
|
|
|
|
|
pointerEvents: 'none',
|
|
|
|
|
|
zIndex: 10,
|
|
|
|
|
|
objectFit: 'contain',
|
|
|
|
|
|
}}
|
|
|
|
|
|
alt=""
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
loading="lazy"
|
|
|
|
|
|
decoding="async"
|
2026-06-14 12:02:50 -04:00
|
|
|
|
onError={(e) => {
|
|
|
|
|
|
(e.currentTarget as HTMLImageElement).style.display = 'none';
|
|
|
|
|
|
}}
|
2026-06-14 11:24:04 -04:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|