minecraft-website/src/components/player-tooltip.tsx
2025-11-09 19:56:28 -07:00

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