0c29506402
- Extract utility functions from component files into dedicated modules to fix react-refresh/only-export-components warnings: - parseBadgeDefinition -> src/lib/parseBadgeDefinition.ts - parseProfileBadges -> src/lib/parseProfileBadges.ts - getColors, paletteToTheme -> src/lib/colorMomentUtils.ts - parseDimToAspectRatio, eventToMediaItem -> src/lib/mediaUtils.ts - isAudioUrl, isImageUrl, isVideoUrl -> src/lib/mediaTypeDetection.ts - buildKindOptions, parseSelectedKinds -> src/lib/feedFilterUtils.ts - useVideoThumbnail -> src/hooks/useVideoThumbnail.ts - useEnvelopeDimensions -> src/hooks/useEnvelopeDimensions.ts - usePortalContainer -> src/hooks/usePortalContainer.ts - useAudioPlayer -> src/contexts/audioPlayerContextDef.ts - SubHeaderBar context/hooks -> src/components/SubHeaderBarContext.ts - EmotionDev hooks -> src/blobbi/dev/useEmotionDev.ts - BlobbiActions context def -> BlobbiActionsContextDef.ts - Remove export from internal-only functions (useEventComments, parseEmojiPack, useScrollCarets, formatEffectSummary, getSortedEffectEntries) - Fix react-hooks/exhaustive-deps warnings by adding missing dependencies to useEffect/useCallback/useMemo hooks across 14 files - Fix logical expression dependency warnings by wrapping conditional values (tasks, pubkeys, authorPubkeys) in useMemo - Move module-level constants (CORE_TAB_IDS, CORE_TAB_LABELS, DEFAULT_TAB_LABELS) out of ProfilePage component body - Reorder usePushNotifications hooks so syncPreferences is defined before enable to fix block-scoped variable error
101 lines
2.9 KiB
TypeScript
101 lines
2.9 KiB
TypeScript
import { useCallback } from 'react';
|
|
import { Award } from 'lucide-react';
|
|
|
|
import type { BadgeData } from '@/lib/parseBadgeDefinition';
|
|
import { useCardTilt } from '@/hooks/useCardTilt';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
interface BadgeThumbnailProps {
|
|
badge: BadgeData;
|
|
/** Pixel size for both width and height. Default: 48 */
|
|
size?: number;
|
|
className?: string;
|
|
}
|
|
|
|
/**
|
|
* Renders a badge thumbnail with appropriate image resolution for the given size.
|
|
* Falls back to an Award icon when no image is available.
|
|
*
|
|
* Includes a mouse-only 3D tilt effect. Touch events are ignored so tapping
|
|
* through to badge detail views and normal scrolling are not disrupted.
|
|
*/
|
|
export function BadgeThumbnail({ badge, size = 48, className }: BadgeThumbnailProps) {
|
|
const thumbUrl = pickThumb(badge, size);
|
|
// Tight perspective relative to the small thumbnail makes the 3D rotation
|
|
// clearly visible even on 28-48px elements.
|
|
const tilt = useCardTilt(35, 1.15, size * 3);
|
|
|
|
const handlePointerMove = useCallback(
|
|
(e: React.PointerEvent<HTMLDivElement>) => {
|
|
if (e.pointerType === 'touch') return;
|
|
tilt.onPointerMove(e);
|
|
},
|
|
[tilt],
|
|
);
|
|
|
|
const handlePointerLeave = useCallback(
|
|
(e: React.PointerEvent<HTMLDivElement>) => {
|
|
if (e.pointerType === 'touch') return;
|
|
tilt.onPointerLeave(e);
|
|
},
|
|
[tilt],
|
|
);
|
|
|
|
const style: React.CSSProperties = {
|
|
...tilt.style,
|
|
touchAction: 'auto',
|
|
};
|
|
|
|
return (
|
|
<div
|
|
ref={tilt.ref}
|
|
style={style}
|
|
onPointerMove={handlePointerMove}
|
|
onPointerLeave={handlePointerLeave}
|
|
>
|
|
{thumbUrl ? (
|
|
<img
|
|
src={thumbUrl}
|
|
alt={badge.name}
|
|
className={cn('rounded-lg object-cover', className)}
|
|
style={{ width: size, height: size }}
|
|
loading="lazy"
|
|
decoding="async"
|
|
/>
|
|
) : (
|
|
<div
|
|
className={cn(
|
|
'rounded-lg border border-border bg-gradient-to-br from-primary/10 via-primary/5 to-transparent flex items-center justify-center',
|
|
className,
|
|
)}
|
|
style={{ width: size, height: size }}
|
|
>
|
|
<Award className="text-primary/30" style={{ width: size * 0.5, height: size * 0.5 }} />
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/** Pick the best thumbnail or image for a target pixel size. */
|
|
function pickThumb(badge: BadgeData, targetSize: number): string | undefined {
|
|
const sorted = [...badge.thumbs].sort((a, b) => {
|
|
const aSize = parseDimension(a.dimensions);
|
|
const bSize = parseDimension(b.dimensions);
|
|
return aSize - bSize;
|
|
});
|
|
|
|
for (const thumb of sorted) {
|
|
const dim = parseDimension(thumb.dimensions);
|
|
if (dim >= targetSize) return thumb.url;
|
|
}
|
|
|
|
return sorted[sorted.length - 1]?.url ?? badge.image;
|
|
}
|
|
|
|
function parseDimension(dim?: string): number {
|
|
if (!dim) return 0;
|
|
const match = dim.match(/^(\d+)/);
|
|
return match ? parseInt(match[1], 10) : 0;
|
|
}
|