import React, { useEffect, useRef, useCallback, useMemo } from "react"; import "@/styles/profile-card.css"; interface ProfileCardProps { avatarUrl: string; iconUrl?: string; grainUrl?: string; behindGradient?: string; innerGradient?: string; showBehindGradient?: boolean; className?: string; enableTilt?: boolean; miniAvatarUrl?: string; name?: string; title?: string; handle?: string; status?: string; contactText?: string; showUserInfo?: boolean; onContactClick?: () => void; } const DEFAULT_BEHIND_GRADIENT = "radial-gradient(farthest-side circle at var(--pointer-x) var(--pointer-y),hsla(266,100%,90%,var(--card-opacity)) 4%,hsla(266,50%,80%,calc(var(--card-opacity)*0.75)) 10%,hsla(266,25%,70%,calc(var(--card-opacity)*0.5)) 50%,hsla(266,0%,60%,0) 100%),radial-gradient(35% 52% at 55% 20%,#00ffaac4 0%,#073aff00 100%),radial-gradient(100% 100% at 50% 50%,#00c1ffff 1%,#073aff00 76%),conic-gradient(from 124deg at 50% 50%,#c137ffff 0%,#07c6ffff 40%,#07c6ffff 60%,#c137ffff 100%)"; const DEFAULT_INNER_GRADIENT = "linear-gradient(145deg,#60496e8c 0%,#71C4FF44 100%)"; const ANIMATION_CONFIG = { SMOOTH_DURATION: 600, INITIAL_DURATION: 1500, INITIAL_X_OFFSET: 70, INITIAL_Y_OFFSET: 60, } as const; const clamp = (value: number, min = 0, max = 100): number => Math.min(Math.max(value, min), max); const round = (value: number, precision = 3): number => parseFloat(value.toFixed(precision)); const adjust = ( value: number, fromMin: number, fromMax: number, toMin: number, toMax: number ): number => round(toMin + ((toMax - toMin) * (value - fromMin)) / (fromMax - fromMin)); const easeInOutCubic = (x: number): number => x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2; const ProfileCardComponent: React.FC = ({ avatarUrl = "", iconUrl = "", grainUrl = "", behindGradient, innerGradient, showBehindGradient = true, className = "", enableTilt = true, miniAvatarUrl, name = "Javi A. Torres", title = "Software Engineer", handle = "javicodes", status = "Online", contactText = "Contact", showUserInfo = true, onContactClick, }) => { const wrapRef = useRef(null); const cardRef = useRef(null); const animationHandlers = useMemo(() => { if (!enableTilt) return null; let rafId: number | null = null; const updateCardTransform = ( offsetX: number, offsetY: number, card: HTMLElement, wrap: HTMLElement ) => { const width = card.clientWidth; const height = card.clientHeight; const percentX = clamp((100 / width) * offsetX); const percentY = clamp((100 / height) * offsetY); const centerX = percentX - 50; const centerY = percentY - 50; const properties = { "--pointer-x": `${percentX}%`, "--pointer-y": `${percentY}%`, "--background-x": `${adjust(percentX, 0, 100, 35, 65)}%`, "--background-y": `${adjust(percentY, 0, 100, 35, 65)}%`, "--pointer-from-center": `${clamp( Math.hypot(percentY - 50, percentX - 50) / 50, 0, 1 )}`, "--pointer-from-top": `${percentY / 100}`, "--pointer-from-left": `${percentX / 100}`, "--rotate-x": `${round(-(centerX / 5))}deg`, "--rotate-y": `${round(centerY / 4)}deg`, }; Object.entries(properties).forEach(([property, value]) => { wrap.style.setProperty(property, value); }); }; const createSmoothAnimation = ( duration: number, startX: number, startY: number, card: HTMLElement, wrap: HTMLElement ) => { const startTime = performance.now(); const targetX = wrap.clientWidth / 2; const targetY = wrap.clientHeight / 2; const animationLoop = (currentTime: number) => { const elapsed = currentTime - startTime; const progress = clamp(elapsed / duration); const easedProgress = easeInOutCubic(progress); const currentX = adjust(easedProgress, 0, 1, startX, targetX); const currentY = adjust(easedProgress, 0, 1, startY, targetY); updateCardTransform(currentX, currentY, card, wrap); if (progress < 1) { rafId = requestAnimationFrame(animationLoop); } }; rafId = requestAnimationFrame(animationLoop); }; return { updateCardTransform, createSmoothAnimation, cancelAnimation: () => { if (rafId) { cancelAnimationFrame(rafId); rafId = null; } }, }; }, [enableTilt]); const handlePointerMove = useCallback( (event: PointerEvent) => { const card = cardRef.current; const wrap = wrapRef.current; if (!card || !wrap || !animationHandlers) return; const rect = card.getBoundingClientRect(); animationHandlers.updateCardTransform( event.clientX - rect.left, event.clientY - rect.top, card, wrap ); }, [animationHandlers] ); const handlePointerEnter = useCallback(() => { const card = cardRef.current; const wrap = wrapRef.current; if (!card || !wrap || !animationHandlers) return; animationHandlers.cancelAnimation(); wrap.classList.add("active"); card.classList.add("active"); }, [animationHandlers]); const handlePointerLeave = useCallback( (event: PointerEvent) => { const card = cardRef.current; const wrap = wrapRef.current; if (!card || !wrap || !animationHandlers) return; animationHandlers.createSmoothAnimation( ANIMATION_CONFIG.SMOOTH_DURATION, event.offsetX, event.offsetY, card, wrap ); wrap.classList.remove("active"); card.classList.remove("active"); }, [animationHandlers] ); useEffect(() => { if (!enableTilt || !animationHandlers) return; const card = cardRef.current; const wrap = wrapRef.current; if (!card || !wrap) return; const pointerMoveHandler = handlePointerMove as EventListener; const pointerEnterHandler = handlePointerEnter as EventListener; const pointerLeaveHandler = handlePointerLeave as EventListener; card.addEventListener("pointerenter", pointerEnterHandler); card.addEventListener("pointermove", pointerMoveHandler); card.addEventListener("pointerleave", pointerLeaveHandler); const initialX = wrap.clientWidth - ANIMATION_CONFIG.INITIAL_X_OFFSET; const initialY = ANIMATION_CONFIG.INITIAL_Y_OFFSET; animationHandlers.updateCardTransform(initialX, initialY, card, wrap); animationHandlers.createSmoothAnimation( ANIMATION_CONFIG.INITIAL_DURATION, initialX, initialY, card, wrap ); return () => { card.removeEventListener("pointerenter", pointerEnterHandler); card.removeEventListener("pointermove", pointerMoveHandler); card.removeEventListener("pointerleave", pointerLeaveHandler); animationHandlers.cancelAnimation(); }; }, [ enableTilt, animationHandlers, handlePointerMove, handlePointerEnter, handlePointerLeave, ]); const cardStyle = useMemo( () => ({ "--icon": iconUrl ? `url(${iconUrl})` : "none", "--grain": grainUrl ? `url(${grainUrl})` : "none", "--behind-gradient": showBehindGradient ? behindGradient ?? DEFAULT_BEHIND_GRADIENT : "none", "--inner-gradient": innerGradient ?? DEFAULT_INNER_GRADIENT, } as React.CSSProperties), [iconUrl, grainUrl, showBehindGradient, behindGradient, innerGradient] ); const handleContactClick = useCallback(() => { onContactClick?.(); }, [onContactClick]); return (
{`${name { const target = e.target as HTMLImageElement; target.style.display = "none"; }} /> {showUserInfo && (
{`${name { const target = e.target as HTMLImageElement; target.style.opacity = "0.5"; target.src = avatarUrl; }} />
@{handle}
{status}
)}

{name}

{title}

); }; const ProfileCard = React.memo(ProfileCardComponent); export default ProfileCard;