Update
This commit is contained in:
81
resources/js/components/reactbits/CountUp/CountUp.jsx
Normal file
81
resources/js/components/reactbits/CountUp/CountUp.jsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { useInView, useMotionValue, useSpring } from 'framer-motion';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
export default function CountUp({
|
||||
to,
|
||||
from = 0,
|
||||
direction = 'up',
|
||||
delay = 0,
|
||||
duration = 2,
|
||||
className = '',
|
||||
postFix = '',
|
||||
startWhen = true,
|
||||
separator = '',
|
||||
onStart,
|
||||
onEnd,
|
||||
}) {
|
||||
const ref = useRef(null);
|
||||
const motionValue = useMotionValue(direction === 'down' ? to : from);
|
||||
|
||||
const damping = 20 + 40 * (1 / duration);
|
||||
const stiffness = 100 * (1 / duration);
|
||||
|
||||
const springValue = useSpring(motionValue, {
|
||||
damping,
|
||||
stiffness,
|
||||
});
|
||||
|
||||
const isInView = useInView(ref, { once: true, margin: '0px' });
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
ref.current.textContent = String(direction === 'down' ? to : from);
|
||||
}
|
||||
}, [from, to, direction]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInView && startWhen) {
|
||||
if (typeof onStart === 'function') {
|
||||
onStart();
|
||||
}
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
motionValue.set(direction === 'down' ? from : to);
|
||||
}, delay * 1000);
|
||||
|
||||
const durationTimeoutId = setTimeout(
|
||||
() => {
|
||||
if (typeof onEnd === 'function') {
|
||||
onEnd();
|
||||
}
|
||||
},
|
||||
delay * 1000 + duration * 1000,
|
||||
);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
clearTimeout(durationTimeoutId);
|
||||
};
|
||||
}
|
||||
}, [isInView, startWhen, motionValue, direction, from, to, delay, onStart, onEnd, duration]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = springValue.on('change', (latest) => {
|
||||
if (ref.current) {
|
||||
const options = {
|
||||
useGrouping: !!separator,
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0,
|
||||
};
|
||||
|
||||
const formattedNumber = Intl.NumberFormat('en-US', options).format(latest.toFixed(0));
|
||||
|
||||
ref.current.textContent = (separator ? formattedNumber.replace(/,/g, separator) : formattedNumber) + postFix;
|
||||
}
|
||||
});
|
||||
|
||||
return () => unsubscribe();
|
||||
}, [springValue, separator]);
|
||||
|
||||
return <span className={`${className}`} ref={ref} />;
|
||||
}
|
||||
29
resources/js/components/reactbits/ShinyText/ShinyText.jsx
Normal file
29
resources/js/components/reactbits/ShinyText/ShinyText.jsx
Normal file
@@ -0,0 +1,29 @@
|
||||
const ShinyText = ({ text, disabled = false, speed = 5, className = '' }) => {
|
||||
const animationDuration = `${speed}s`;
|
||||
|
||||
return (
|
||||
<div className={`shiny-text ${disabled ? 'disabled' : ''} ${className}`} style={{ animationDuration }}>
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShinyText;
|
||||
|
||||
// tailwind.config.js
|
||||
// module.exports = {
|
||||
// theme: {
|
||||
// extend: {
|
||||
// keyframes: {
|
||||
// shine: {
|
||||
// '0%': { 'background-position': '100%' },
|
||||
// '100%': { 'background-position': '-100%' },
|
||||
// },
|
||||
// },
|
||||
// animation: {
|
||||
// shine: 'shine 5s linear infinite',
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// plugins: [],
|
||||
// };
|
||||
132
resources/js/components/reactbits/TiltedCard/TiltedCard.jsx
Normal file
132
resources/js/components/reactbits/TiltedCard/TiltedCard.jsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import { motion, useMotionValue, useSpring } from 'framer-motion';
|
||||
import { useRef, useState } from 'react';
|
||||
|
||||
const springValues = {
|
||||
damping: 30,
|
||||
stiffness: 100,
|
||||
mass: 2,
|
||||
};
|
||||
|
||||
export default function TiltedCard({
|
||||
imageSrc,
|
||||
altText = 'Tilted card image',
|
||||
captionText = '',
|
||||
containerHeight = '300px',
|
||||
containerWidth = '100%',
|
||||
imageHeight = '300px',
|
||||
imageWidth = '300px',
|
||||
scaleOnHover = 1.1,
|
||||
rotateAmplitude = 14,
|
||||
showMobileWarning = true,
|
||||
showTooltip = true,
|
||||
overlayContent = null,
|
||||
displayOverlayContent = false,
|
||||
}) {
|
||||
const ref = useRef(null);
|
||||
const x = useMotionValue(0);
|
||||
const y = useMotionValue(0);
|
||||
const rotateX = useSpring(useMotionValue(0), springValues);
|
||||
const rotateY = useSpring(useMotionValue(0), springValues);
|
||||
const scale = useSpring(1, springValues);
|
||||
const opacity = useSpring(0);
|
||||
const rotateFigcaption = useSpring(0, {
|
||||
stiffness: 350,
|
||||
damping: 30,
|
||||
mass: 1,
|
||||
});
|
||||
|
||||
const [lastY, setLastY] = useState(0);
|
||||
|
||||
function handleMouse(e) {
|
||||
if (!ref.current) return;
|
||||
|
||||
const rect = ref.current.getBoundingClientRect();
|
||||
const offsetX = e.clientX - rect.left - rect.width / 2;
|
||||
const offsetY = e.clientY - rect.top - rect.height / 2;
|
||||
|
||||
const rotationX = (offsetY / (rect.height / 2)) * -rotateAmplitude;
|
||||
const rotationY = (offsetX / (rect.width / 2)) * rotateAmplitude;
|
||||
|
||||
rotateX.set(rotationX);
|
||||
rotateY.set(rotationY);
|
||||
|
||||
x.set(e.clientX - rect.left);
|
||||
y.set(e.clientY - rect.top);
|
||||
|
||||
const velocityY = offsetY - lastY;
|
||||
rotateFigcaption.set(-velocityY * 0.6);
|
||||
setLastY(offsetY);
|
||||
}
|
||||
|
||||
function handleMouseEnter() {
|
||||
scale.set(scaleOnHover);
|
||||
opacity.set(1);
|
||||
}
|
||||
|
||||
function handleMouseLeave() {
|
||||
opacity.set(0);
|
||||
scale.set(1);
|
||||
rotateX.set(0);
|
||||
rotateY.set(0);
|
||||
rotateFigcaption.set(0);
|
||||
}
|
||||
|
||||
return (
|
||||
<figure
|
||||
ref={ref}
|
||||
className="relative flex h-full w-full flex-col items-center justify-center [perspective:800px]"
|
||||
style={{
|
||||
height: containerHeight,
|
||||
width: containerWidth,
|
||||
}}
|
||||
onMouseMove={handleMouse}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
{showMobileWarning && (
|
||||
<div className="absolute top-4 block text-center text-sm sm:hidden">This effect is not optimized for mobile. Check on desktop.</div>
|
||||
)}
|
||||
|
||||
<motion.div
|
||||
className="relative [transform-style:preserve-3d]"
|
||||
style={{
|
||||
width: imageWidth,
|
||||
height: imageHeight,
|
||||
rotateX,
|
||||
rotateY,
|
||||
scale,
|
||||
}}
|
||||
>
|
||||
<motion.img
|
||||
src={imageSrc}
|
||||
alt={altText}
|
||||
className="absolute top-0 left-0 [transform:translateZ(0)] rounded-[15px] object-cover will-change-transform"
|
||||
style={{
|
||||
width: imageWidth,
|
||||
height: imageHeight,
|
||||
}}
|
||||
/>
|
||||
|
||||
{displayOverlayContent && overlayContent && (
|
||||
<motion.div className="absolute top-0 left-0 z-[2] [transform:translateZ(30px)] will-change-transform">
|
||||
{overlayContent}
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
{showTooltip && (
|
||||
<motion.figcaption
|
||||
className="pointer-events-none absolute top-0 left-0 z-[3] hidden rounded-[4px] bg-white px-[10px] py-[4px] text-[10px] text-[#2d2d2d] opacity-0 sm:block"
|
||||
style={{
|
||||
x,
|
||||
y,
|
||||
opacity,
|
||||
rotate: rotateFigcaption,
|
||||
}}
|
||||
>
|
||||
{captionText}
|
||||
</motion.figcaption>
|
||||
)}
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user