diff --git a/src/components/BadgeDetailContent.tsx b/src/components/BadgeDetailContent.tsx index 4d4ee1ab..223ec24b 100644 --- a/src/components/BadgeDetailContent.tsx +++ b/src/components/BadgeDetailContent.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState, useCallback } from 'react'; +import { useEffect, useMemo, useState, useCallback, useRef } from 'react'; import { Link } from 'react-router-dom'; import { Award, Copy, Check, Users, Gift, Loader2, MessageCircle, Newspaper, MoreHorizontal } from 'lucide-react'; import { nip19 } from 'nostr-tools'; @@ -33,6 +33,7 @@ import { genUserName } from '@/lib/genUserName'; import { formatNumber } from '@/lib/formatNumber'; import { VerifiedNip05Text } from '@/components/Nip05Badge'; import { parseBadgeDefinition } from '@/components/BadgeContent'; +import { useCardTilt } from '@/hooks/useCardTilt'; import { useProfileUrl } from '@/hooks/useProfileUrl'; import { AwardBadgeDialog } from '@/components/AwardBadgeDialog'; import { NoteMoreMenu } from '@/components/NoteMoreMenu'; @@ -146,40 +147,9 @@ export function BadgeDetailContent({ event }: { event: NostrEvent }) { return (
- {/* Hero badge image */} + {/* Hero badge image with 3D tilt */} {heroImage ? ( -
- {/* Rotating light rays */} - + ) : (
@@ -575,6 +545,104 @@ function AwardeeCardSkeleton() { ); } +/** + * Badge hero with interactive 3D tilt. Hovering moves the badge in + * perspective space while a specular glare overlay tracks the pointer, + * making the badge feel like a tangible, glossy object. + */ +function BadgeHero({ heroImage, badgeName }: { heroImage: string; badgeName: string }) { + const tilt = useCardTilt(18, 1.06); + const glareRef = useRef(null); + + const handlePointerMove = useCallback( + (e: React.PointerEvent) => { + tilt.onPointerMove(e); + + // Move the specular glare to follow the cursor + const el = tilt.ref.current; + const glare = glareRef.current; + if (!el || !glare) return; + const rect = el.getBoundingClientRect(); + const x = ((e.clientX - rect.left) / rect.width) * 100; + const y = ((e.clientY - rect.top) / rect.height) * 100; + glare.style.background = `radial-gradient(circle at ${x}% ${y}%, rgba(255,255,255,0.35) 0%, rgba(255,255,255,0.08) 35%, transparent 65%)`; + glare.style.opacity = '1'; + }, + [tilt], + ); + + const handlePointerLeave = useCallback( + () => { + tilt.onPointerLeave(); + const glare = glareRef.current; + if (glare) glare.style.opacity = '0'; + }, + [tilt], + ); + + return ( +
+ {/* Rotating light rays (behind tilt container) */} +