Files
eranos/src/components/BadgeThumbnail.tsx
T
Chad Curtis 0c29506402 Fix all 50 ESLint warnings by extracting non-component exports and adding missing deps
- 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
2026-04-05 11:57:31 -05:00

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;
}