Files
game-marathon/frontend/src/components/ui/NeonButton.tsx

167 lines
5.6 KiB
TypeScript
Raw Normal View History

2025-12-17 02:03:33 +07:00
import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from 'react'
import { clsx } from 'clsx'
import { Loader2 } from 'lucide-react'
interface NeonButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger'
size?: 'sm' | 'md' | 'lg'
color?: 'neon' | 'purple' | 'pink'
isLoading?: boolean
icon?: ReactNode
iconPosition?: 'left' | 'right'
glow?: boolean
pulse?: boolean
}
export const NeonButton = forwardRef<HTMLButtonElement, NeonButtonProps>(
(
{
className,
variant = 'primary',
size = 'md',
color = 'neon',
isLoading,
icon,
iconPosition = 'left',
glow = true,
pulse = false,
children,
disabled,
...props
},
ref
) => {
const colorMap = {
neon: {
primary: 'bg-neon-500 hover:bg-neon-400 text-dark-900',
secondary: 'bg-dark-600 hover:bg-dark-500 text-neon-400 border border-neon-500/30',
outline: 'bg-transparent border-2 border-neon-500 text-neon-500 hover:bg-neon-500 hover:text-dark-900',
ghost: 'bg-transparent hover:bg-neon-500/10 text-neon-400',
danger: 'bg-red-600 hover:bg-red-700 text-white',
glow: '0 0 20px rgba(0, 240, 255, 0.5)',
glowHover: '0 0 30px rgba(0, 240, 255, 0.7)',
},
purple: {
primary: 'bg-accent-500 hover:bg-accent-400 text-white',
secondary: 'bg-dark-600 hover:bg-dark-500 text-accent-400 border border-accent-500/30',
outline: 'bg-transparent border-2 border-accent-500 text-accent-500 hover:bg-accent-500 hover:text-white',
ghost: 'bg-transparent hover:bg-accent-500/10 text-accent-400',
danger: 'bg-red-600 hover:bg-red-700 text-white',
glow: '0 0 20px rgba(168, 85, 247, 0.5)',
glowHover: '0 0 30px rgba(168, 85, 247, 0.7)',
},
pink: {
primary: 'bg-pink-500 hover:bg-pink-400 text-white',
secondary: 'bg-dark-600 hover:bg-dark-500 text-pink-400 border border-pink-500/30',
outline: 'bg-transparent border-2 border-pink-500 text-pink-500 hover:bg-pink-500 hover:text-white',
ghost: 'bg-transparent hover:bg-pink-500/10 text-pink-400',
danger: 'bg-red-600 hover:bg-red-700 text-white',
glow: '0 0 20px rgba(236, 72, 153, 0.5)',
glowHover: '0 0 30px rgba(236, 72, 153, 0.7)',
},
}
const sizeClasses = {
sm: 'px-3 py-1.5 text-sm gap-1.5',
md: 'px-4 py-2.5 text-base gap-2',
lg: 'px-6 py-3 text-lg gap-2.5',
}
const iconSizes = {
sm: 'w-4 h-4',
md: 'w-5 h-5',
lg: 'w-6 h-6',
}
const colors = colorMap[color]
return (
<button
ref={ref}
disabled={disabled || isLoading}
className={clsx(
'inline-flex items-center justify-center font-semibold rounded-lg',
'transition-all duration-300 ease-out',
'disabled:opacity-50 disabled:cursor-not-allowed disabled:shadow-none',
'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-dark-900',
color === 'neon' && 'focus:ring-neon-500',
color === 'purple' && 'focus:ring-accent-500',
color === 'pink' && 'focus:ring-pink-500',
colors[variant],
sizeClasses[size],
pulse && 'neon-glow-pulse',
className
)}
style={{
boxShadow: glow && !disabled && variant !== 'ghost' ? colors.glow : undefined,
}}
onMouseEnter={(e) => {
if (glow && !disabled && variant !== 'ghost') {
e.currentTarget.style.boxShadow = colors.glowHover
}
props.onMouseEnter?.(e)
}}
onMouseLeave={(e) => {
if (glow && !disabled && variant !== 'ghost') {
e.currentTarget.style.boxShadow = colors.glow
}
props.onMouseLeave?.(e)
}}
{...props}
>
{isLoading && <Loader2 className={clsx(iconSizes[size], 'animate-spin')} />}
{!isLoading && icon && iconPosition === 'left' && (
<span className={iconSizes[size]}>{icon}</span>
)}
{children}
{!isLoading && icon && iconPosition === 'right' && (
<span className={iconSizes[size]}>{icon}</span>
)}
</button>
)
}
)
NeonButton.displayName = 'NeonButton'
// Gradient button variant
interface GradientButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
size?: 'sm' | 'md' | 'lg'
isLoading?: boolean
icon?: ReactNode
}
export const GradientButton = forwardRef<HTMLButtonElement, GradientButtonProps>(
({ className, size = 'md', isLoading, icon, children, disabled, ...props }, ref) => {
const sizeClasses = {
sm: 'px-3 py-1.5 text-sm gap-1.5',
md: 'px-4 py-2.5 text-base gap-2',
lg: 'px-6 py-3 text-lg gap-2.5',
}
return (
<button
ref={ref}
disabled={disabled || isLoading}
className={clsx(
'relative inline-flex items-center justify-center font-semibold rounded-lg',
'bg-gradient-to-r from-neon-500 via-accent-500 to-pink-500',
'text-white transition-all duration-300',
'hover:shadow-[0_0_30px_rgba(168,85,247,0.5)]',
'disabled:opacity-50 disabled:cursor-not-allowed',
'focus:outline-none focus:ring-2 focus:ring-accent-500 focus:ring-offset-2 focus:ring-offset-dark-900',
sizeClasses[size],
className
)}
{...props}
>
{isLoading && <Loader2 className="w-5 h-5 animate-spin" />}
{!isLoading && icon && <span className="w-5 h-5">{icon}</span>}
{children}
</button>
)
}
)
GradientButton.displayName = 'GradientButton'