165 lines
4.0 KiB
TypeScript
165 lines
4.0 KiB
TypeScript
import React, { useEffect, useState, useRef } from 'react';
|
|
import { SxProps } from '@mui/material';
|
|
import Box from '@mui/material/Box';
|
|
|
|
interface PulseProps {
|
|
timestamp: number | string;
|
|
sx?: SxProps;
|
|
}
|
|
|
|
const Pulse: React.FC<PulseProps> = ({ timestamp, sx }) => {
|
|
const [isAnimating, setIsAnimating] = useState(false);
|
|
const [animationKey, setAnimationKey] = useState(0);
|
|
const previousTimestamp = useRef<number | string | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (timestamp && timestamp !== previousTimestamp.current) {
|
|
previousTimestamp.current = timestamp;
|
|
setAnimationKey(prev => prev + 1);
|
|
setIsAnimating(true);
|
|
|
|
// Reset animation state after animation completes
|
|
const timer = setTimeout(() => {
|
|
setIsAnimating(false);
|
|
}, 1000);
|
|
|
|
return () => clearTimeout(timer);
|
|
}
|
|
}, [timestamp]);
|
|
|
|
const containerStyle: React.CSSProperties = {
|
|
position: 'relative',
|
|
width: 80,
|
|
height: 80,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
};
|
|
|
|
const baseCoreStyle: React.CSSProperties = {
|
|
width: 0,
|
|
height: 0,
|
|
borderRadius: '50%',
|
|
backgroundColor: '#2196f3',
|
|
position: 'relative',
|
|
zIndex: 3,
|
|
};
|
|
|
|
const coreStyle: React.CSSProperties = {
|
|
...baseCoreStyle,
|
|
animation: isAnimating ? 'pulse-glow 1s ease-out' : 'none',
|
|
};
|
|
|
|
const pulseRing1Style: React.CSSProperties = {
|
|
position: 'absolute',
|
|
width: 24,
|
|
height: 24,
|
|
borderRadius: '50%',
|
|
backgroundColor: '#2196f3',
|
|
zIndex: 2,
|
|
animation: 'pulse-expand 1s ease-out forwards',
|
|
};
|
|
|
|
const pulseRing2Style: React.CSSProperties = {
|
|
position: 'absolute',
|
|
width: 24,
|
|
height: 24,
|
|
borderRadius: '50%',
|
|
backgroundColor: '#64b5f6',
|
|
zIndex: 1,
|
|
animation: 'pulse-expand 1s ease-out 0.2s forwards',
|
|
};
|
|
|
|
const rippleStyle: React.CSSProperties = {
|
|
position: 'absolute',
|
|
width: 32,
|
|
height: 32,
|
|
borderRadius: '50%',
|
|
border: '2px solid #2196f3',
|
|
backgroundColor: 'transparent',
|
|
zIndex: 0,
|
|
animation: 'ripple-expand 1s ease-out forwards',
|
|
};
|
|
|
|
const outerRippleStyle: React.CSSProperties = {
|
|
position: 'absolute',
|
|
width: 40,
|
|
height: 40,
|
|
borderRadius: '50%',
|
|
border: '1px solid #90caf9',
|
|
backgroundColor: 'transparent',
|
|
zIndex: 0,
|
|
animation: 'ripple-expand 1s ease-out 0.3s forwards',
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<style>
|
|
{`
|
|
@keyframes pulse-expand {
|
|
0% {
|
|
transform: scale(1);
|
|
opacity: 1;
|
|
}
|
|
50% {
|
|
transform: scale(1.3);
|
|
opacity: 0.7;
|
|
}
|
|
100% {
|
|
transform: scale(1.6);
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
@keyframes ripple-expand {
|
|
0% {
|
|
transform: scale(0.8);
|
|
opacity: 0.8;
|
|
}
|
|
100% {
|
|
transform: scale(2);
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
@keyframes pulse-glow {
|
|
0% {
|
|
box-shadow: 0 0 5px #2196f3, 0 0 10px #2196f3, 0 0 15px #2196f3;
|
|
}
|
|
50% {
|
|
box-shadow: 0 0 10px #2196f3, 0 0 20px #2196f3, 0 0 30px #2196f3;
|
|
}
|
|
100% {
|
|
box-shadow: 0 0 5px #2196f3, 0 0 10px #2196f3, 0 0 15px #2196f3;
|
|
}
|
|
}
|
|
`}
|
|
</style>
|
|
|
|
<Box sx={{ ...containerStyle, ...sx }}>
|
|
{/* Base circle */}
|
|
<div style={coreStyle} />
|
|
|
|
{/* Pulse rings */}
|
|
{isAnimating && (
|
|
<>
|
|
{/* Primary pulse ring */}
|
|
<div key={`pulse-1-${animationKey}`} style={pulseRing1Style} />
|
|
|
|
{/* Secondary pulse ring with delay */}
|
|
<div key={`pulse-2-${animationKey}`} style={pulseRing2Style} />
|
|
|
|
{/* Ripple effect */}
|
|
<div key={`ripple-${animationKey}`} style={rippleStyle} />
|
|
|
|
{/* Outer ripple */}
|
|
<div key={`ripple-outer-${animationKey}`} style={outerRippleStyle} />
|
|
</>
|
|
)}
|
|
</Box>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export { Pulse };
|