Redesign p1
This commit is contained in:
@@ -15,13 +15,13 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
ref={ref}
|
||||
disabled={disabled || isLoading}
|
||||
className={clsx(
|
||||
'inline-flex items-center justify-center font-medium rounded-lg transition-colors',
|
||||
'inline-flex items-center justify-center font-medium rounded-lg transition-all duration-200',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed',
|
||||
{
|
||||
'bg-primary-600 hover:bg-primary-700 text-white': variant === 'primary',
|
||||
'bg-gray-700 hover:bg-gray-600 text-white': variant === 'secondary',
|
||||
'bg-neon-500 hover:bg-neon-400 text-dark-900 font-semibold shadow-[0_0_10px_rgba(0,240,255,0.3)] hover:shadow-[0_0_20px_rgba(0,240,255,0.5)]': variant === 'primary',
|
||||
'bg-dark-600 hover:bg-dark-500 text-white border border-dark-500': variant === 'secondary',
|
||||
'bg-red-600 hover:bg-red-700 text-white': variant === 'danger',
|
||||
'bg-transparent hover:bg-gray-800 text-gray-300': variant === 'ghost',
|
||||
'bg-transparent hover:bg-dark-700 text-gray-300 hover:text-white': variant === 'ghost',
|
||||
'px-3 py-1.5 text-sm': size === 'sm',
|
||||
'px-4 py-2 text-base': size === 'md',
|
||||
'px-6 py-3 text-lg': size === 'lg',
|
||||
|
||||
@@ -4,11 +4,18 @@ import { clsx } from 'clsx'
|
||||
interface CardProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
hover?: boolean
|
||||
}
|
||||
|
||||
export function Card({ children, className }: CardProps) {
|
||||
export function Card({ children, className, hover = false }: CardProps) {
|
||||
return (
|
||||
<div className={clsx('bg-gray-800 rounded-xl p-6 shadow-lg', className)}>
|
||||
<div
|
||||
className={clsx(
|
||||
'bg-dark-800 rounded-xl p-6 border border-dark-600',
|
||||
hover && 'transition-all duration-300 hover:-translate-y-1 hover:border-neon-500/30 hover:shadow-[0_10px_40px_rgba(0,240,255,0.1)]',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
211
frontend/src/components/ui/GlassCard.tsx
Normal file
211
frontend/src/components/ui/GlassCard.tsx
Normal file
@@ -0,0 +1,211 @@
|
||||
import { type ReactNode, type HTMLAttributes } from 'react'
|
||||
import { clsx } from 'clsx'
|
||||
|
||||
interface GlassCardProps extends HTMLAttributes<HTMLDivElement> {
|
||||
children: ReactNode
|
||||
variant?: 'default' | 'dark' | 'neon' | 'gradient'
|
||||
hover?: boolean
|
||||
glow?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function GlassCard({
|
||||
children,
|
||||
variant = 'default',
|
||||
hover = false,
|
||||
glow = false,
|
||||
className,
|
||||
...props
|
||||
}: GlassCardProps) {
|
||||
const variantClasses = {
|
||||
default: 'glass',
|
||||
dark: 'glass-dark',
|
||||
neon: 'glass-neon',
|
||||
gradient: 'gradient-border',
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'rounded-xl p-6',
|
||||
variantClasses[variant],
|
||||
hover && 'card-hover cursor-pointer',
|
||||
glow && 'neon-glow-pulse',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Stats card variant
|
||||
interface StatsCardProps {
|
||||
label: string
|
||||
value: string | number
|
||||
icon?: ReactNode
|
||||
trend?: {
|
||||
value: number
|
||||
isPositive: boolean
|
||||
}
|
||||
color?: 'neon' | 'purple' | 'pink' | 'default'
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function StatsCard({
|
||||
label,
|
||||
value,
|
||||
icon,
|
||||
trend,
|
||||
color = 'default',
|
||||
className,
|
||||
}: StatsCardProps) {
|
||||
const colorClasses = {
|
||||
neon: 'border-neon-500/30 hover:border-neon-500/50',
|
||||
purple: 'border-accent-500/30 hover:border-accent-500/50',
|
||||
pink: 'border-pink-500/30 hover:border-pink-500/50',
|
||||
default: 'border-dark-600 hover:border-dark-500',
|
||||
}
|
||||
|
||||
const iconColorClasses = {
|
||||
neon: 'text-neon-500 bg-neon-500/10',
|
||||
purple: 'text-accent-500 bg-accent-500/10',
|
||||
pink: 'text-pink-500 bg-pink-500/10',
|
||||
default: 'text-gray-400 bg-dark-700',
|
||||
}
|
||||
|
||||
const valueColorClasses = {
|
||||
neon: 'text-neon-400',
|
||||
purple: 'text-accent-400',
|
||||
pink: 'text-pink-400',
|
||||
default: 'text-white',
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'glass rounded-xl p-4 border transition-all duration-300',
|
||||
colorClasses[color],
|
||||
'hover:-translate-y-0.5',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-400 mb-1">{label}</p>
|
||||
<p className={clsx('text-2xl font-bold', valueColorClasses[color])}>
|
||||
{value}
|
||||
</p>
|
||||
{trend && (
|
||||
<p
|
||||
className={clsx(
|
||||
'text-xs mt-1',
|
||||
trend.isPositive ? 'text-green-400' : 'text-red-400'
|
||||
)}
|
||||
>
|
||||
{trend.isPositive ? '+' : ''}{trend.value}%
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{icon && (
|
||||
<div
|
||||
className={clsx(
|
||||
'w-12 h-12 rounded-lg flex items-center justify-center',
|
||||
iconColorClasses[color]
|
||||
)}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Feature card variant
|
||||
interface FeatureCardProps {
|
||||
title: string
|
||||
description: string
|
||||
icon: ReactNode
|
||||
color?: 'neon' | 'purple' | 'pink'
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function FeatureCard({
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
color = 'neon',
|
||||
className,
|
||||
}: FeatureCardProps) {
|
||||
const colorClasses = {
|
||||
neon: {
|
||||
icon: 'text-neon-500 bg-neon-500/10 group-hover:bg-neon-500/20',
|
||||
border: 'group-hover:border-neon-500/50',
|
||||
glow: 'group-hover:shadow-[0_0_30px_rgba(0,240,255,0.15)]',
|
||||
},
|
||||
purple: {
|
||||
icon: 'text-accent-500 bg-accent-500/10 group-hover:bg-accent-500/20',
|
||||
border: 'group-hover:border-accent-500/50',
|
||||
glow: 'group-hover:shadow-[0_0_30px_rgba(168,85,247,0.15)]',
|
||||
},
|
||||
pink: {
|
||||
icon: 'text-pink-500 bg-pink-500/10 group-hover:bg-pink-500/20',
|
||||
border: 'group-hover:border-pink-500/50',
|
||||
glow: 'group-hover:shadow-[0_0_30px_rgba(236,72,153,0.15)]',
|
||||
},
|
||||
}
|
||||
|
||||
const colors = colorClasses[color]
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'group glass rounded-xl p-6 border border-dark-600 transition-all duration-300',
|
||||
'hover:-translate-y-1',
|
||||
colors.border,
|
||||
colors.glow,
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'w-14 h-14 rounded-xl flex items-center justify-center mb-4 transition-colors',
|
||||
colors.icon
|
||||
)}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-white mb-2">{title}</h3>
|
||||
<p className="text-gray-400 text-sm">{description}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Interactive card with animated border
|
||||
interface AnimatedBorderCardProps extends HTMLAttributes<HTMLDivElement> {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function AnimatedBorderCard({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: AnimatedBorderCardProps) {
|
||||
return (
|
||||
<div className={clsx('relative group', className)} {...props}>
|
||||
{/* Animated gradient border */}
|
||||
<div
|
||||
className="absolute -inset-0.5 bg-gradient-to-r from-neon-500 via-accent-500 to-pink-500 rounded-xl opacity-30 group-hover:opacity-60 blur transition-opacity duration-300"
|
||||
style={{
|
||||
backgroundSize: '200% 200%',
|
||||
animation: 'gradient-flow 3s linear infinite',
|
||||
}}
|
||||
/>
|
||||
{/* Card content */}
|
||||
<div className="relative glass-dark rounded-xl p-6">{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
116
frontend/src/components/ui/GlitchText.tsx
Normal file
116
frontend/src/components/ui/GlitchText.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import { type ReactNode, type HTMLAttributes } from 'react'
|
||||
import { clsx } from 'clsx'
|
||||
|
||||
interface GlitchTextProps extends HTMLAttributes<HTMLSpanElement> {
|
||||
children: ReactNode
|
||||
as?: 'span' | 'h1' | 'h2' | 'h3' | 'h4' | 'p'
|
||||
intensity?: 'low' | 'medium' | 'high'
|
||||
color?: 'neon' | 'purple' | 'pink' | 'white'
|
||||
hover?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function GlitchText({
|
||||
children,
|
||||
as: Component = 'span',
|
||||
intensity = 'medium',
|
||||
color = 'neon',
|
||||
hover = false,
|
||||
className,
|
||||
...props
|
||||
}: GlitchTextProps) {
|
||||
const text = typeof children === 'string' ? children : ''
|
||||
|
||||
const colorClasses = {
|
||||
neon: 'text-neon-500',
|
||||
purple: 'text-accent-500',
|
||||
pink: 'text-pink-500',
|
||||
white: 'text-white',
|
||||
}
|
||||
|
||||
const glowClasses = {
|
||||
neon: 'neon-text',
|
||||
purple: 'neon-text-purple',
|
||||
pink: '[text-shadow:0_0_10px_#ec4899,0_0_20px_#ec4899,0_0_30px_#ec4899]',
|
||||
white: '[text-shadow:0_0_10px_rgba(255,255,255,0.5),0_0_20px_rgba(255,255,255,0.3)]',
|
||||
}
|
||||
|
||||
const intensityClasses = {
|
||||
low: 'animate-[glitch-skew_2s_infinite_linear_alternate-reverse]',
|
||||
medium: '',
|
||||
high: 'animate-glitch',
|
||||
}
|
||||
|
||||
if (hover) {
|
||||
return (
|
||||
<Component
|
||||
className={clsx(
|
||||
colorClasses[color],
|
||||
'relative inline-block cursor-pointer transition-all duration-300',
|
||||
'hover:' + glowClasses[color],
|
||||
'glitch-hover',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Component>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Component
|
||||
className={clsx(
|
||||
'glitch relative inline-block',
|
||||
colorClasses[color],
|
||||
glowClasses[color],
|
||||
intensityClasses[intensity],
|
||||
className
|
||||
)}
|
||||
data-text={text}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Component>
|
||||
)
|
||||
}
|
||||
|
||||
// Simpler glitch effect for headings
|
||||
interface GlitchHeadingProps {
|
||||
children: ReactNode
|
||||
level?: 1 | 2 | 3 | 4
|
||||
className?: string
|
||||
gradient?: boolean
|
||||
}
|
||||
|
||||
export function GlitchHeading({
|
||||
children,
|
||||
level = 1,
|
||||
className,
|
||||
gradient = false,
|
||||
}: GlitchHeadingProps) {
|
||||
const text = typeof children === 'string' ? children : ''
|
||||
|
||||
const sizeClasses = {
|
||||
1: 'text-4xl md:text-5xl lg:text-6xl font-bold',
|
||||
2: 'text-3xl md:text-4xl font-bold',
|
||||
3: 'text-2xl md:text-3xl font-semibold',
|
||||
4: 'text-xl md:text-2xl font-semibold',
|
||||
}
|
||||
|
||||
const Component = `h${level}` as keyof JSX.IntrinsicElements
|
||||
|
||||
return (
|
||||
<Component
|
||||
className={clsx(
|
||||
'glitch relative inline-block',
|
||||
sizeClasses[level],
|
||||
gradient ? 'gradient-neon-text' : 'text-white neon-text',
|
||||
className
|
||||
)}
|
||||
data-text={text}
|
||||
>
|
||||
{children}
|
||||
</Component>
|
||||
)
|
||||
}
|
||||
@@ -11,7 +11,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
return (
|
||||
<div className="w-full">
|
||||
{label && (
|
||||
<label htmlFor={id} className="block text-sm font-medium text-gray-300 mb-1">
|
||||
<label htmlFor={id} className="block text-sm font-medium text-gray-300 mb-1.5">
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
@@ -19,15 +19,16 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
ref={ref}
|
||||
id={id}
|
||||
className={clsx(
|
||||
'w-full px-4 py-2 bg-gray-800 border rounded-lg text-white placeholder-gray-500',
|
||||
'focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent',
|
||||
'transition-colors',
|
||||
error ? 'border-red-500' : 'border-gray-700',
|
||||
'w-full px-4 py-3 bg-dark-800 border rounded-lg text-white placeholder-gray-500',
|
||||
'focus:outline-none focus:border-neon-500',
|
||||
'focus:shadow-[0_0_0_3px_rgba(0,240,255,0.1),0_0_10px_rgba(0,240,255,0.2)]',
|
||||
'transition-all duration-200',
|
||||
error ? 'border-red-500' : 'border-dark-600',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
{error && <p className="mt-1 text-sm text-red-500">{error}</p>}
|
||||
{error && <p className="mt-1.5 text-sm text-red-400">{error}</p>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
166
frontend/src/components/ui/NeonButton.tsx
Normal file
166
frontend/src/components/ui/NeonButton.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
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'
|
||||
@@ -4,3 +4,8 @@ export { Card, CardHeader, CardTitle, CardContent } from './Card'
|
||||
export { ToastContainer } from './Toast'
|
||||
export { ConfirmModal } from './ConfirmModal'
|
||||
export { UserAvatar, clearAvatarCache } from './UserAvatar'
|
||||
|
||||
// New design system components
|
||||
export { GlitchText, GlitchHeading } from './GlitchText'
|
||||
export { NeonButton, GradientButton } from './NeonButton'
|
||||
export { GlassCard, StatsCard, FeatureCard, AnimatedBorderCard } from './GlassCard'
|
||||
|
||||
Reference in New Issue
Block a user