112 lines
3.5 KiB
TypeScript
112 lines
3.5 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
import { motion, AnimatePresence } from 'motion/react';
|
|
import { useEffect, useRef } from 'react';
|
|
|
|
interface PlayerTooltipProps {
|
|
playerName: string;
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
// Get Minecraft skin URL (using Minotar API)
|
|
const getMinecraftSkinUrl = (username: string) => {
|
|
// Minotar provides avatar images - use /avatar/ for head only or /helm/avatar/ for head with helm
|
|
return `https://minotar.net/avatar/${encodeURIComponent(username)}/64`;
|
|
};
|
|
|
|
export function PlayerTooltip({ playerName, children }: PlayerTooltipProps) {
|
|
const [isHovered, setIsHovered] = useState(false);
|
|
const [position, setPosition] = useState({ top: 0, left: 0 });
|
|
const triggerRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
const updatePosition = () => {
|
|
if (triggerRef.current) {
|
|
// Find the actual child element (the <p> tag) to get its position
|
|
const childElement = triggerRef.current.firstElementChild as HTMLElement;
|
|
if (childElement) {
|
|
const rect = childElement.getBoundingClientRect();
|
|
setPosition({
|
|
top: rect.top - 90, // Position above the element (accounting for tooltip height)
|
|
left: rect.left + rect.width / 2,
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
if (isHovered) {
|
|
// Small delay to ensure DOM is ready
|
|
const timeoutId = setTimeout(updatePosition, 0);
|
|
window.addEventListener('scroll', updatePosition, true);
|
|
window.addEventListener('resize', updatePosition);
|
|
|
|
return () => {
|
|
clearTimeout(timeoutId);
|
|
window.removeEventListener('scroll', updatePosition, true);
|
|
window.removeEventListener('resize', updatePosition);
|
|
};
|
|
}
|
|
}, [isHovered]);
|
|
|
|
const tooltipContent = (
|
|
<AnimatePresence>
|
|
{isHovered && typeof window !== 'undefined' && (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 10, scale: 0.9 }}
|
|
animate={{
|
|
opacity: 1,
|
|
y: 0,
|
|
scale: 1,
|
|
transition: {
|
|
type: 'spring',
|
|
stiffness: 260,
|
|
damping: 20,
|
|
},
|
|
}}
|
|
exit={{ opacity: 0, y: 10, scale: 0.9 }}
|
|
className="fixed z-[100] flex -translate-x-1/2 flex-col items-center justify-center rounded-lg bg-card border border-border shadow-xl p-3"
|
|
style={{
|
|
pointerEvents: 'none',
|
|
willChange: 'transform',
|
|
top: `${position.top}px`,
|
|
left: `${position.left}px`,
|
|
}}
|
|
>
|
|
<div className="relative">
|
|
<img
|
|
src={getMinecraftSkinUrl(playerName)}
|
|
alt={`${playerName}'s Minecraft skin`}
|
|
width={64}
|
|
height={64}
|
|
className="rounded-md border-2 border-border"
|
|
loading="lazy"
|
|
/>
|
|
</div>
|
|
<div className="mt-2 text-xs font-semibold text-foreground">
|
|
{playerName}
|
|
</div>
|
|
<div className="absolute -bottom-1 left-1/2 -translate-x-1/2 w-2 h-2 rotate-45 bg-card border-r border-b border-border" />
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
);
|
|
|
|
return (
|
|
<>
|
|
<div
|
|
ref={triggerRef}
|
|
className="relative inline"
|
|
onMouseEnter={() => setIsHovered(true)}
|
|
onMouseLeave={() => setIsHovered(false)}
|
|
style={{ display: 'inline' }}
|
|
>
|
|
{children}
|
|
</div>
|
|
{typeof window !== 'undefined' && createPortal(tooltipContent, document.body)}
|
|
</>
|
|
);
|
|
}
|
|
|