Initial commit
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
|
||||
interface AnimatedCounterProps {
|
||||
end: number;
|
||||
duration?: number;
|
||||
prefix?: string;
|
||||
suffix?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const AnimatedCounter: React.FC<AnimatedCounterProps> = ({
|
||||
end,
|
||||
duration = 2000,
|
||||
prefix = '',
|
||||
suffix = '',
|
||||
className = ''
|
||||
}) => {
|
||||
const [count, setCount] = useState(0);
|
||||
const counterRef = useRef<HTMLSpanElement>(null);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setIsVisible(true);
|
||||
}
|
||||
},
|
||||
{ threshold: 0.2 }
|
||||
);
|
||||
|
||||
if (counterRef.current) {
|
||||
observer.observe(counterRef.current);
|
||||
}
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isVisible) return;
|
||||
|
||||
let start = 0;
|
||||
const increment = end / (duration / 16);
|
||||
const timer = setInterval(() => {
|
||||
start += increment;
|
||||
if (start >= end) {
|
||||
setCount(end);
|
||||
clearInterval(timer);
|
||||
} else {
|
||||
setCount(Math.floor(start));
|
||||
}
|
||||
}, 16);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [end, duration, isVisible]);
|
||||
|
||||
return (
|
||||
<span ref={counterRef} className={className}>
|
||||
{prefix}
|
||||
{count.toLocaleString()}
|
||||
{suffix}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
|
||||
interface BadgeProps {
|
||||
children: React.ReactNode;
|
||||
variant?: 'emerald' | 'indigo' | 'cyan' | 'purple' | 'amber' | 'neutral' | 'tangerine' | 'sky';
|
||||
showDot?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const Badge: React.FC<BadgeProps> = ({
|
||||
children,
|
||||
variant = 'tangerine',
|
||||
showDot = true,
|
||||
className = ''
|
||||
}) => {
|
||||
const styles = {
|
||||
tangerine: {
|
||||
bg: 'bg-orange-950/70 text-orange-300 border-orange-500/40',
|
||||
dot: 'bg-orange-400'
|
||||
},
|
||||
sky: {
|
||||
bg: 'bg-sky-950/70 text-sky-300 border-sky-500/40',
|
||||
dot: 'bg-sky-400'
|
||||
},
|
||||
emerald: {
|
||||
bg: 'bg-sky-950/70 text-sky-300 border-sky-500/40',
|
||||
dot: 'bg-sky-400'
|
||||
},
|
||||
indigo: {
|
||||
bg: 'bg-orange-950/70 text-orange-300 border-orange-500/40',
|
||||
dot: 'bg-orange-400'
|
||||
},
|
||||
cyan: {
|
||||
bg: 'bg-sky-950/70 text-sky-300 border-sky-500/40',
|
||||
dot: 'bg-sky-400'
|
||||
},
|
||||
purple: {
|
||||
bg: 'bg-sky-950/70 text-sky-300 border-sky-500/40',
|
||||
dot: 'bg-sky-400'
|
||||
},
|
||||
amber: {
|
||||
bg: 'bg-orange-950/70 text-orange-300 border-orange-500/40',
|
||||
dot: 'bg-amber-400'
|
||||
},
|
||||
neutral: {
|
||||
bg: 'bg-slate-800/80 text-slate-300 border-slate-700/60',
|
||||
dot: 'bg-slate-400'
|
||||
}
|
||||
};
|
||||
|
||||
const current = styles[variant];
|
||||
|
||||
return (
|
||||
<span className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium border backdrop-blur-md shadow-inner tracking-wide ${current.bg} ${className}`}>
|
||||
{showDot && (
|
||||
<span className="relative flex h-2 w-2">
|
||||
<span className={`animate-ping absolute inline-flex h-full w-full rounded-full opacity-75 ${current.dot}`} />
|
||||
<span className={`relative inline-flex rounded-full h-2 w-2 ${current.dot}`} />
|
||||
</span>
|
||||
)}
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
|
||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'glow';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
isLoading?: boolean;
|
||||
icon?: React.ReactNode;
|
||||
iconPosition?: 'left' | 'right';
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Button: React.FC<ButtonProps> = ({
|
||||
variant = 'primary',
|
||||
size = 'md',
|
||||
isLoading = false,
|
||||
icon,
|
||||
iconPosition = 'right',
|
||||
children,
|
||||
className = '',
|
||||
disabled,
|
||||
...props
|
||||
}) => {
|
||||
const baseStyles = 'inline-flex items-center justify-center font-medium transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-orange-500/50 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer whitespace-nowrap rounded-xl';
|
||||
|
||||
const sizeStyles = {
|
||||
sm: 'px-3.5 py-1.5 text-xs gap-1.5',
|
||||
md: 'px-5 py-2.5 text-sm gap-2',
|
||||
lg: 'px-7 py-3.5 text-base gap-2.5 shadow-lg shadow-orange-950/20'
|
||||
};
|
||||
|
||||
const variantStyles = {
|
||||
primary: 'bg-orange-600 hover:bg-orange-500 text-white shadow-lg shadow-orange-600/25 border border-orange-500/30 hover:border-orange-400/50 hover:shadow-orange-500/35 active:scale-[0.98]',
|
||||
secondary: 'bg-slate-800/90 hover:bg-slate-700/90 text-slate-100 border border-slate-700/60 hover:border-slate-600 active:scale-[0.98]',
|
||||
outline: 'bg-transparent text-slate-200 border border-slate-700 hover:border-orange-500/50 hover:bg-orange-950/20 active:scale-[0.98]',
|
||||
ghost: 'bg-transparent text-slate-300 hover:text-white hover:bg-slate-800/60 active:scale-[0.98]',
|
||||
glow: 'relative bg-gradient-to-r from-orange-500 via-amber-500 to-sky-400 text-slate-950 font-extrabold shadow-xl shadow-orange-500/25 hover:shadow-orange-500/40 border border-amber-200/40 hover:scale-[1.02] active:scale-[0.98]'
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.button
|
||||
whileHover={{ y: -1 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
className={`${baseStyles} ${sizeStyles[size]} ${variantStyles[variant]} ${className}`}
|
||||
disabled={disabled || isLoading}
|
||||
{...props}
|
||||
>
|
||||
{isLoading && <Loader2 className="w-4 h-4 animate-spin text-current" />}
|
||||
{!isLoading && icon && iconPosition === 'left' && <span className="inline-flex">{icon}</span>}
|
||||
<span>{children}</span>
|
||||
{!isLoading && icon && iconPosition === 'right' && <span className="inline-flex">{icon}</span>}
|
||||
</motion.button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
|
||||
interface GlassCardProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
hoverEffect?: boolean;
|
||||
gradientBorder?: boolean;
|
||||
onClick?: () => void;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export const GlassCard: React.FC<GlassCardProps> = ({
|
||||
children,
|
||||
className = '',
|
||||
hoverEffect = true,
|
||||
gradientBorder = false,
|
||||
onClick,
|
||||
id
|
||||
}) => {
|
||||
return (
|
||||
<motion.div
|
||||
id={id}
|
||||
whileHover={hoverEffect ? { y: -4, transition: { duration: 0.2 } } : undefined}
|
||||
onClick={onClick}
|
||||
className={`relative rounded-2xl bg-slate-900/70 backdrop-blur-xl border border-slate-800/80 p-6 shadow-xl shadow-black/40 overflow-hidden transition-all duration-300 ${
|
||||
hoverEffect ? 'hover:border-orange-500/40 hover:shadow-2xl hover:shadow-orange-950/30' : ''
|
||||
} ${
|
||||
gradientBorder ? 'before:absolute before:inset-0 before:p-[1px] before:bg-gradient-to-b before:from-orange-500/40 before:via-sky-500/20 before:to-transparent before:rounded-2xl before:-z-10' : ''
|
||||
} ${className}`}
|
||||
>
|
||||
{/* Subtle top glow line */}
|
||||
<div className="absolute top-0 left-0 right-0 h-[1px] bg-gradient-to-r from-transparent via-orange-500/30 to-transparent opacity-60 pointer-events-none" />
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import { Badge } from './Badge';
|
||||
|
||||
interface SectionHeadingProps {
|
||||
badgeText?: string;
|
||||
badgeVariant?: 'emerald' | 'indigo' | 'cyan' | 'purple' | 'amber' | 'neutral' | 'tangerine' | 'sky';
|
||||
title: string;
|
||||
gradientTitle?: string;
|
||||
subtitle?: string;
|
||||
align?: 'left' | 'center' | 'right';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const SectionHeading: React.FC<SectionHeadingProps> = ({
|
||||
badgeText,
|
||||
badgeVariant = 'tangerine',
|
||||
title,
|
||||
gradientTitle,
|
||||
subtitle,
|
||||
align = 'center',
|
||||
className = ''
|
||||
}) => {
|
||||
const alignmentClass = {
|
||||
left: 'text-left items-start',
|
||||
center: 'text-center items-center',
|
||||
right: 'text-right items-end'
|
||||
}[align];
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col mb-12 ${alignmentClass} ${className}`}>
|
||||
{badgeText && (
|
||||
<div className="mb-4">
|
||||
<Badge variant={badgeVariant}>{badgeText}</Badge>
|
||||
</div>
|
||||
)}
|
||||
<h2 className="text-3xl sm:text-4xl lg:text-5xl font-bold tracking-tight text-white max-w-3xl leading-tight">
|
||||
{title}{' '}
|
||||
{gradientTitle && (
|
||||
<span className="bg-gradient-to-r from-orange-400 via-amber-300 to-sky-400 bg-clip-text text-transparent">
|
||||
{gradientTitle}
|
||||
</span>
|
||||
)}
|
||||
</h2>
|
||||
{subtitle && (
|
||||
<p className="mt-4 text-base sm:text-lg text-slate-400 max-w-2xl leading-relaxed font-normal">
|
||||
{subtitle}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user