feat: hotspots with animated line+arrow+card, always visible on last frame

This commit is contained in:
2026-07-30 13:20:40 +03:00
parent 5aabd1eedc
commit bc234b4400
+66 -70
View File
@@ -242,36 +242,18 @@ export default function CloudRevealSection() {
// ─── Hotspot bileşeni ──────────────────────────────────────────────────────── // ─── Hotspot bileşeni ────────────────────────────────────────────────────────
const HOTSPOTS = [ const HOTSPOTS: {
{ id: string
id: 'blok-a', label: string
label: '2+1 Daire', sub: string
sub: 'Blok A · 89 m²', x: number // dot pozisyonu — ekranın % yatay
// Ekranın yüzde konumu (sol-üst köşeden) y: number // dot pozisyonu — ekranın % dikey
x: 34, dir: 'left' | 'right' // kartın hangi tarafa uzayacağı
y: 22, }[] = [
}, { id: 'blok-a', label: '2+1 Daire', sub: 'Blok A · 89 m²', x: 34, y: 22, dir: 'left' },
{ { id: 'blok-b', label: '2+1 Daire', sub: 'Blok B · 84 m²', x: 70, y: 19, dir: 'right' },
id: 'blok-b', { id: 'blok-c', label: '1+1 Daire', sub: 'Blok C · 60 m²', x: 50, y: 55, dir: 'right' },
label: '2+1 Daire', { id: 'blok-d', label: '1+1 Daire', sub: 'Blok D · 47 m²', x: 20, y: 65, dir: 'left' },
sub: 'Blok B · 84 m²',
x: 70,
y: 19,
},
{
id: 'blok-c',
label: '1+1 Daire',
sub: 'Blok C · 60 m²',
x: 50,
y: 55,
},
{
id: 'blok-d',
label: '1+1 Daire',
sub: 'Blok D · 47 m²',
x: 20,
y: 65,
},
] ]
function UnitHotspots({ function UnitHotspots({
@@ -279,58 +261,72 @@ function UnitHotspots({
}: { }: {
scrollYProgress: ReturnType<typeof useScroll>['scrollYProgress'] scrollYProgress: ReturnType<typeof useScroll>['scrollYProgress']
}) { }) {
const opacity = useTransform(scrollYProgress, [0.92, 0.98], [0, 1]) const containerOpacity = useTransform(scrollYProgress, [0.92, 0.98], [0, 1])
return ( return (
<motion.div <motion.div
style={{ opacity }} style={{ opacity: containerOpacity }}
className="absolute inset-0 z-20 pointer-events-none" className="absolute inset-0 z-20 pointer-events-none"
> >
{HOTSPOTS.map((spot, i) => ( {HOTSPOTS.map((spot, i) => (
<motion.button <div
key={spot.id} key={spot.id}
initial={{ scale: 0.8, opacity: 0 }} className="absolute"
animate={{ scale: 1, opacity: 1 }}
transition={{ delay: i * 0.15, duration: 0.4, ease: 'easeOut' }}
className="absolute pointer-events-auto group"
style={{ left: `${spot.x}%`, top: `${spot.y}%` }} style={{ left: `${spot.x}%`, top: `${spot.y}%` }}
onClick={() => {
// İleride: daire içi frame animasyonunu tetikle
console.log('clicked:', spot.id)
}}
> >
{/* Pulsing dot */} {/* Dot + çizgi + kart — yatay flex */}
<span className="relative flex h-4 w-4 -translate-x-1/2 -translate-y-1/2"> <div className={`flex items-center gap-0 ${spot.dir === 'left' ? 'flex-row-reverse' : 'flex-row'}`}>
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-white opacity-60" />
<span className="relative inline-flex rounded-full h-4 w-4 bg-white shadow-lg" />
</span>
{/* Label kartı — hover'da görünür, mobilde her zaman */} {/* Pulsing dot */}
<div className="absolute left-5 -top-8 min-w-max bg-black/70 backdrop-blur-sm border border-white/20 rounded-xl px-3 py-2 text-left opacity-0 group-hover:opacity-100 transition-opacity duration-200 md:opacity-0 max-md:opacity-100 pointer-events-none"> <span className="relative flex h-3 w-3 shrink-0">
{/* Animated ok çizgisi */} <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-white opacity-70" />
<svg <span className="relative inline-flex rounded-full h-3 w-3 bg-white" />
className="absolute -left-5 top-1/2 -translate-y-1/2" </span>
width="20" height="2"
viewBox="0 0 20 2" {/* Animated çizgi */}
<motion.div
className="h-px bg-white/60 shrink-0"
initial={{ width: 0 }}
animate={{ width: 48 }}
transition={{ delay: i * 0.2 + 0.1, duration: 0.4, ease: 'easeOut' }}
/>
{/* Ok ucu */}
<motion.div
initial={{ opacity: 0, x: spot.dir === 'right' ? -4 : 4 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: i * 0.2 + 0.4, duration: 0.2 }}
className="shrink-0 text-white/60"
> >
<motion.line {spot.dir === 'right'
x1="0" y1="1" x2="20" y2="1" ? <svg width="6" height="8" viewBox="0 0 6 8" fill="none"><path d="M1 1l4 3-4 3" stroke="white" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round"/></svg>
stroke="white" strokeWidth="1" strokeOpacity="0.6" : <svg width="6" height="8" viewBox="0 0 6 8" fill="none"><path d="M5 1L1 4l4 3" stroke="white" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round"/></svg>
initial={{ pathLength: 0 }} }
animate={{ pathLength: 1 }} </motion.div>
transition={{ delay: i * 0.15 + 0.3, duration: 0.4 }}
/> {/* Kart */}
</svg> <motion.button
<p className="text-white text-xs font-medium tracking-wide">{spot.label}</p> initial={{ opacity: 0, x: spot.dir === 'right' ? -8 : 8 }}
<p className="text-white/50 text-[10px] mt-0.5">{spot.sub}</p> animate={{ opacity: 1, x: 0 }}
<div className="mt-1.5 flex items-center gap-1 text-white/60 text-[10px] tracking-widest uppercase"> transition={{ delay: i * 0.2 + 0.45, duration: 0.3, ease: 'easeOut' }}
<span>Detaylar</span> className="pointer-events-auto bg-black/60 backdrop-blur-md border border-white/20 rounded-lg px-3 py-2 text-left hover:bg-black/80 hover:border-white/40 transition-all duration-200 active:scale-95 cursor-pointer"
<svg width="8" height="8" viewBox="0 0 8 8" fill="none"> onClick={() => {
<path d="M1 4h6M4 1l3 3-3 3" stroke="currentColor" strokeWidth="1" strokeLinecap="round"/> // İleride: daire içi frame sekansını başlat
</svg> console.log('unit clicked:', spot.id)
</div> }}
>
<p className="text-white text-xs font-medium tracking-wide whitespace-nowrap">{spot.label}</p>
<p className="text-white/50 text-[10px] mt-0.5 whitespace-nowrap">{spot.sub}</p>
<div className="mt-1.5 flex items-center gap-1 text-white/40 text-[10px] tracking-widest uppercase">
<span>İncele</span>
<svg width="8" height="8" viewBox="0 0 8 8" fill="none">
<path d="M1 4h6M4 1l3 3-3 3" stroke="currentColor" strokeWidth="1" strokeLinecap="round"/>
</svg>
</div>
</motion.button>
</div> </div>
</motion.button> </div>
))} ))}
</motion.div> </motion.div>
) )