reactbits-mcp-server
Version:
MCP Server for React Bits - Access 99+ React components with animations, backgrounds, and UI elements
355 lines (305 loc) • 10.7 kB
JSX
import React, { useEffect, useRef, useCallback, useMemo } from "react";
import "./ProfileCard.css";
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,
DEVICE_BETA_OFFSET: 20,
};
const clamp = (value, min = 0, max = 100) =>
Math.min(Math.max(value, min), max);
const round = (value, precision = 3) =>
parseFloat(value.toFixed(precision));
const adjust = (
value,
fromMin,
fromMax,
toMin,
toMax
) =>
round(toMin + ((toMax - toMin) * (value - fromMin)) / (fromMax - fromMin));
const easeInOutCubic = (x) =>
x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2;
const ProfileCardComponent = ({
avatarUrl = "<Placeholder for avatar URL>",
iconUrl = "<Placeholder for icon URL>",
grainUrl = "<Placeholder for grain URL>",
behindGradient,
innerGradient,
showBehindGradient = true,
className = "",
enableTilt = true,
enableMobileTilt = false,
mobileTiltSensitivity = 5,
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 = null;
const updateCardTransform = (
offsetX,
offsetY,
card,
wrap
) => {
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,
startX,
startY,
card,
wrap
) => {
const startTime = performance.now();
const targetX = wrap.clientWidth / 2;
const targetY = wrap.clientHeight / 2;
const animationLoop = (currentTime) => {
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) => {
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) => {
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]
);
const handleDeviceOrientation = useCallback(
(event) => {
const card = cardRef.current;
const wrap = wrapRef.current;
if (!card || !wrap || !animationHandlers) return;
const { beta, gamma } = event;
if (!beta || !gamma) return;
animationHandlers.updateCardTransform(
card.clientHeight / 2 + gamma * mobileTiltSensitivity,
card.clientWidth / 2 + (beta - ANIMATION_CONFIG.DEVICE_BETA_OFFSET) * mobileTiltSensitivity,
card,
wrap
);
},
[animationHandlers, mobileTiltSensitivity]
);
useEffect(() => {
if (!enableTilt || !animationHandlers) return;
const card = cardRef.current;
const wrap = wrapRef.current;
if (!card || !wrap) return;
const pointerMoveHandler = handlePointerMove;
const pointerEnterHandler = handlePointerEnter;
const pointerLeaveHandler = handlePointerLeave;
const deviceOrientationHandler = handleDeviceOrientation;
const handleClick = () => {
if (!enableMobileTilt || location.protocol !== 'https:') return;
if (typeof window.DeviceMotionEvent.requestPermission === 'function') {
window.DeviceMotionEvent
.requestPermission()
.then(state => {
if (state === 'granted') {
window.addEventListener('deviceorientation', deviceOrientationHandler);
}
})
.catch(err => console.error(err));
} else {
window.addEventListener('deviceorientation', deviceOrientationHandler);
}
};
card.addEventListener("pointerenter", pointerEnterHandler);
card.addEventListener("pointermove", pointerMoveHandler);
card.addEventListener("pointerleave", pointerLeaveHandler);
card.addEventListener("click", handleClick);
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);
card.removeEventListener("click", handleClick);
window.removeEventListener('deviceorientation', deviceOrientationHandler);
animationHandlers.cancelAnimation();
};
}, [
enableTilt,
enableMobileTilt,
animationHandlers,
handlePointerMove,
handlePointerEnter,
handlePointerLeave,
handleDeviceOrientation,
]);
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,
}),
[iconUrl, grainUrl, showBehindGradient, behindGradient, innerGradient]
);
const handleContactClick = useCallback(() => {
onContactClick?.();
}, [onContactClick]);
return (
<div
ref={wrapRef}
className={`pc-card-wrapper ${className}`.trim()}
style={cardStyle}
>
<section ref={cardRef} className="pc-card">
<div className="pc-inside">
<div className="pc-shine" />
<div className="pc-glare" />
<div className="pc-content pc-avatar-content">
<img
className="avatar"
src={avatarUrl}
alt={`${name || "User"} avatar`}
loading="lazy"
onError={(e) => {
const target = e.target;
target.style.display = "none";
}}
/>
{showUserInfo && (
<div className="pc-user-info">
<div className="pc-user-details">
<div className="pc-mini-avatar">
<img
src={miniAvatarUrl || avatarUrl}
alt={`${name || "User"} mini avatar`}
loading="lazy"
onError={(e) => {
const target = e.target;
target.style.opacity = "0.5";
target.src = avatarUrl;
}}
/>
</div>
<div className="pc-user-text">
<div className="pc-handle">@{handle}</div>
<div className="pc-status">{status}</div>
</div>
</div>
<button
className="pc-contact-btn"
onClick={handleContactClick}
style={{ pointerEvents: "auto" }}
type="button"
aria-label={`Contact ${name || "user"}`}
>
{contactText}
</button>
</div>
)}
</div>
<div className="pc-content">
<div className="pc-details">
<h3>{name}</h3>
<p>{title}</p>
</div>
</div>
</div>
</section>
</div>
);
};
const ProfileCard = React.memo(ProfileCardComponent);
export default ProfileCard;