137 lines
3.6 KiB
TypeScript
137 lines
3.6 KiB
TypeScript
'use client';
|
|
|
|
import React from 'react';
|
|
import Link from 'next/link';
|
|
import styled, {keyframes} from 'styled-components';
|
|
|
|
type PulsatingButtonProps = {
|
|
label: string;
|
|
href: string;
|
|
color?: string;
|
|
textColor?: string;
|
|
width?: number;
|
|
height?: number;
|
|
pulse?: boolean;
|
|
};
|
|
|
|
const MAX_LAYERS = 2;
|
|
|
|
const pulse = keyframes`
|
|
0%, 10% {
|
|
opacity: 0;
|
|
transform: scale(1.1);
|
|
}
|
|
80% {
|
|
opacity: 0.7;
|
|
transform: scale(1.15, 1.4);
|
|
}
|
|
81%, 100% {
|
|
opacity: 0;
|
|
transform: scale(1);
|
|
}
|
|
`;
|
|
|
|
const scale = keyframes`
|
|
0% {
|
|
transform: scale(1);
|
|
}
|
|
35%, 80% {
|
|
transform: scale(1.1, 1.35);
|
|
}
|
|
100% {
|
|
transform: scale(1);
|
|
}
|
|
`;
|
|
|
|
const Wrapper = styled.div`
|
|
position: relative;
|
|
display: inline-flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
`;
|
|
|
|
const Layer = styled.div<{
|
|
color: string;
|
|
$width: number;
|
|
$height: number;
|
|
$layer: number;
|
|
}>`
|
|
position: absolute;
|
|
background: transparent;
|
|
border: 1px solid ${({color, $layer}) =>
|
|
$layer === 0 ? `${color}AA` : `${color}66`}; // L0: ~67%, L1: ~40%
|
|
border-radius: 9999px;
|
|
animation: ${({$layer}) => ($layer ? pulse : scale)} 1.5s infinite;
|
|
width: ${({$width, $layer}) => $width + $layer * 8}px;
|
|
height: ${({$height, $layer}) => $height + $layer * 8}px;
|
|
z-index: ${({$layer}) => MAX_LAYERS - $layer};
|
|
`;
|
|
|
|
// Use $-prefix to avoid prop leaking
|
|
const StyledButton = styled.a.withConfig({
|
|
shouldForwardProp: (prop) =>
|
|
!['$bgColor', '$textColor', '$width', '$height'].includes(prop),
|
|
})<{
|
|
$bgColor: string;
|
|
$textColor: string;
|
|
$width: number;
|
|
$height: number;
|
|
}>`
|
|
position: relative;
|
|
z-index: ${MAX_LAYERS + 1};
|
|
display: inline-flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
background-color: ${({$bgColor}) => $bgColor};
|
|
color: ${({$textColor}) => $textColor};
|
|
border-radius: 9999px;
|
|
padding: 0.75rem 1.5rem;
|
|
font-weight: 600;
|
|
text-decoration: none;
|
|
transition: background-color 0.3s ease;
|
|
width: ${({$width}) => $width}px;
|
|
height: ${({$height}) => $height}px;
|
|
|
|
&:hover {
|
|
opacity: 0.9;
|
|
}
|
|
`;
|
|
|
|
const PulsatingButton: React.FC<PulsatingButtonProps> = ({
|
|
label,
|
|
href,
|
|
color = '#3B82F6', // Tailwind blue-500
|
|
textColor = '#ffffff',
|
|
width = 160,
|
|
height = 48,
|
|
pulse = true,
|
|
}) => {
|
|
return (
|
|
<Wrapper>
|
|
{pulse &&
|
|
Array.from({length: MAX_LAYERS}).map((_, index) => (
|
|
<Layer
|
|
key={index}
|
|
color={color}
|
|
$width={width}
|
|
$height={height}
|
|
$layer={index}
|
|
/>
|
|
))}
|
|
|
|
<Link href={href} passHref legacyBehavior>
|
|
<StyledButton
|
|
$bgColor={color}
|
|
$textColor={textColor}
|
|
$width={width}
|
|
$height={height}
|
|
>
|
|
{label}
|
|
</StyledButton>
|
|
</Link>
|
|
</Wrapper>
|
|
);
|
|
};
|
|
|
|
export default PulsatingButton;
|