Refactor website to use shadcn components
This commit is contained in:
136
frontend/components/PulsatingButton.tsx
Normal file
136
frontend/components/PulsatingButton.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
'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;
|
||||
Reference in New Issue
Block a user